feat(novel-front): 增加评论点赞/点踩功能

This commit is contained in:
xiongxiaoyang
2025-07-12 13:33:23 +08:00
parent 02fb819120
commit 3c409023e5
12 changed files with 733 additions and 12 deletions

View File

@@ -7,6 +7,7 @@ import com.java2nb.novel.entity.*;
import com.java2nb.novel.service.BookContentService;
import com.java2nb.novel.service.BookService;
import com.java2nb.novel.service.IpLocationService;
import com.java2nb.novel.service.LikeService;
import com.java2nb.novel.vo.*;
import io.github.xxyopen.model.page.PageBean;
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
@@ -35,6 +36,8 @@ public class BookController extends BaseController {
private final IpLocationService ipLocationService;
private final LikeService likeService;
/**
* 查询首页小说设置列表数据
*/
@@ -171,6 +174,30 @@ public class BookController extends BaseController {
return RestResult.ok();
}
/**
* 评价点赞/取消点赞
*/
@PostMapping("toggleCommentLike")
public RestResult<?> toggleCommentLike(Long commentId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleCommentLike(commentId, userDetails.getId()));
}
/**
* 评价点踩/取消点踩
*/
@PostMapping("toggleCommentUnLike")
public RestResult<?> toggleCommentUnLike(Long commentId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleCommentUnLike(commentId, userDetails.getId()));
}
/**
* 新增回复
*/
@@ -185,6 +212,30 @@ public class BookController extends BaseController {
return RestResult.ok();
}
/**
* 回复点赞/取消点赞
*/
@PostMapping("toggleReplyLike")
public RestResult<?> toggleReplyLike(Long replyId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleReplyLike(replyId, userDetails.getId()));
}
/**
* 回复点赞/取消点赞
*/
@PostMapping("toggleReplyUnLike")
public RestResult<?> toggleReplyUnLike(Long replyId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleReplyUnLike(replyId, userDetails.getId()));
}
/**
* 根据小说ID查询小说前十条最新更新目录集合
*/

View File

@@ -0,0 +1,69 @@
package com.java2nb.novel.service;
/**
* @author 11797
*/
public interface LikeService {
/**
* 评论点赞或取消点赞
* @param commentId 被点赞的评论ID
* @param userId 用户ID
* @return 返回点赞数量
*/
public Long toggleCommentLike(Long commentId, Long userId);
/**
* 评论点踩或取消点踩
* @param commentId 被点踩的评论ID
* @param userId 用户ID
* @return 返回点踩数量
*/
public Long toggleCommentUnLike(Long commentId, Long userId);
/**
* 获取评论的点赞数量
* @param commentId 评论ID
* @return 点赞数
*/
public Long getCommentLikesCount(Long commentId);
/**
* 获取评论的点踩赞数量
* @param commentId 评论ID
* @return 点踩数
*/
public Long getCommentUnLikesCount(Long commentId);
/**
* 回复点赞或取消点赞
* @param replyId 被点赞的回复ID
* @param userId 用户ID
* @return 返回点赞数量
*/
public Long toggleReplyLike(Long replyId, Long userId);
/**
* 回复点踩或取消点踩
* @param replyId 被点踩的回复ID
* @param userId 用户ID
* @return 返回点踩数量
*/
public Long toggleReplyUnLike(Long replyId, Long userId);
/**
* 获取回复的点赞数量
* @param replyId 回复ID
* @return 点赞数
*/
public Long getReplyLikesCount(Long replyId);
/**
* 获取回复的点踩数量
* @param replyId 回复ID
* @return 点踩数
*/
public Long getReplyUnLikesCount(Long replyId);
}

View File

@@ -15,6 +15,7 @@ import com.java2nb.novel.mapper.*;
import com.java2nb.novel.service.AuthorService;
import com.java2nb.novel.service.BookService;
import com.java2nb.novel.service.FileService;
import com.java2nb.novel.service.LikeService;
import com.java2nb.novel.vo.*;
import io.github.xxyopen.model.page.PageBean;
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
@@ -94,6 +95,8 @@ public class BookServiceImpl implements BookService {
private final FileService fileService;
private final LikeService likeService;
private final BookPriceProperties bookPriceConfig;
private final OpenAiImageModel openAiImageModel;
@@ -390,7 +393,12 @@ public class BookServiceImpl implements BookService {
@Override
public PageBean<BookCommentVO> listCommentByPage(Long userId, Long bookId, int page, int pageSize) {
PageHelper.startPage(page, pageSize);
return PageBuilder.build(bookCommentMapper.listCommentByPage(userId, bookId));
PageBean<BookCommentVO> pageBean = PageBuilder.build(bookCommentMapper.listCommentByPage(userId, bookId));
for (BookCommentVO bookCommentVO : pageBean.getList()) {
bookCommentVO.setLikesCount(likeService.getCommentLikesCount(bookCommentVO.getId()));
bookCommentVO.setUnLikesCount(likeService.getCommentUnLikesCount(bookCommentVO.getId()));
}
return pageBean;
}
@Transactional(rollbackFor = Exception.class)
@@ -901,7 +909,13 @@ public class BookServiceImpl implements BookService {
@Override
public PageBean<BookCommentReplyVO> listCommentReplyByPage(Long userId, Long commentId, int page, int pageSize) {
PageHelper.startPage(page, pageSize);
return PageBuilder.build(bookCommentReplyMapper.listCommentReplyByPage(userId, commentId));
PageBean<BookCommentReplyVO> pageBean = PageBuilder.build(
bookCommentReplyMapper.listCommentReplyByPage(userId, commentId));
pageBean.getList().forEach(commentReply -> {
commentReply.setLikesCount(likeService.getReplyLikesCount(commentReply.getId()));
commentReply.setUnLikesCount(likeService.getReplyUnLikesCount(commentReply.getId()));
});
return pageBean;
}
@Override

View File

@@ -0,0 +1,94 @@
package com.java2nb.novel.service.impl;
import com.java2nb.novel.service.LikeService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import org.springframework.stereotype.Service;
import java.util.Collections;
/**
* @author xiongxiaoyang
* @date 2025/7/12
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LikeServiceImpl implements LikeService {
private final RedisTemplate<Object, Object> redisTemplate;
private DefaultRedisScript<Long> toggleLikeScript;
private static final String COMMENT_LIKE_KEY_PREFIX = "like:comment:";
private static final String COMMENT_REPLY_LIKE_KEY_PREFIX = "like:comment:reply:";
private static final String COMMENT_UN_LIKE_KEY_PREFIX = "unlike:comment:";
private static final String COMMENT_REPLY_UN_LIKE_KEY_PREFIX = "unlike:comment:reply:";
@PostConstruct
public void init() {
// Lua 脚本保证原子性操作
String script = """
local key = KEYS[1]
local userId = ARGV[1]
local isLiked = redis.call('SISMEMBER', key, userId)
if isLiked == 1 then
redis.call('SREM', key, userId)
else
redis.call('SADD', key, userId)
end
return redis.call('SCARD', key)
""";
toggleLikeScript = new DefaultRedisScript<>();
toggleLikeScript.setScriptSource(new StaticScriptSource(script));
toggleLikeScript.setResultType(Long.class);
}
public Long toggleCommentLike(Long commentId, Long userId) {
return executeToggle(COMMENT_LIKE_KEY_PREFIX + commentId, userId);
}
@Override
public Long toggleCommentUnLike(Long commentId, Long userId) {
return executeToggle(COMMENT_UN_LIKE_KEY_PREFIX + commentId, userId);
}
public Long getCommentLikesCount(Long commentId) {
return redisTemplate.opsForSet().size(COMMENT_LIKE_KEY_PREFIX + commentId);
}
@Override
public Long getCommentUnLikesCount(Long commentId) {
return redisTemplate.opsForSet().size(COMMENT_UN_LIKE_KEY_PREFIX + commentId);
}
public Long toggleReplyLike(Long replyId, Long userId) {
return executeToggle(COMMENT_REPLY_LIKE_KEY_PREFIX + replyId, userId);
}
@Override
public Long toggleReplyUnLike(Long replyId, Long userId) {
return executeToggle(COMMENT_REPLY_UN_LIKE_KEY_PREFIX + replyId, userId);
}
public Long getReplyLikesCount(Long replyId) {
return redisTemplate.opsForSet().size(COMMENT_REPLY_LIKE_KEY_PREFIX + replyId);
}
@Override
public Long getReplyUnLikesCount(Long replyId) {
return redisTemplate.opsForSet().size(COMMENT_REPLY_UN_LIKE_KEY_PREFIX + replyId);
}
private Long executeToggle(String key, Long userId) {
return redisTemplate.execute(toggleLikeScript, Collections.singletonList(key), userId);
}
}

View File

@@ -23,6 +23,10 @@ public class BookCommentReplyVO extends BookCommentReply {
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private Long likesCount;
private Long unLikesCount;
@Override
public String toString() {
return super.toString();

View File

@@ -22,6 +22,10 @@ public class BookCommentVO extends BookComment {
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private Long likesCount;
private Long unLikesCount;
@Override
public String toString() {
return super.toString();

View File

@@ -139,8 +139,8 @@
comment.commentContent+
"</li><li class=\"other cf\">" +
"<span class=\"time fl\">"+comment.createTime+"</span>" +
"<span class=\"fr\"><a href=\"javascript:void(0);\" onclick=\"javascript:;\" class=\"zan\" style=\"padding-left: 10px\">踩<i class=\"num\">(0)</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:void(0);\" onclick=\"javascript:;\" class=\"zan\" style=\"padding-left: 10px\">赞<i class=\"num\">(0)</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:toggleCommentUnLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\">踩<i class=\"num\" id='unLikeCount"+comment.id+"'>("+comment.unLikesCount+")</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:toggleCommentLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\">赞<i class=\"num\" id='likeCount"+comment.id+"'>("+comment.likesCount+")</i></a></span>" +
"<span class=\"fr\"><a href=\"/book/reply-"+comment.id+".html\" class=\"zan\" style=\"padding-left: 10px\">回复<i class=\"num\">("+comment.replyCount+
")</i></a></span>" +
"</li>\t\t</ul>\t</div>");
@@ -188,6 +188,48 @@
})
}
function toggleCommentLike(commentId) {
$.ajax({
type: "post",
url: "/book/toggleCommentLike",
data: {'commentId': commentId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#likeCount"+commentId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
function toggleCommentUnLike(commentId) {
$.ajax({
type: "post",
url: "/book/toggleCommentUnLike",
data: {'commentId': commentId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#unLikeCount"+commentId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
</script>
</body>
</html>

View File

@@ -114,8 +114,9 @@
"</li><li class=\"other cf\">" +
"<span class=\"time fl\" style='padding-right: 10px'>" + (data.data.total - ((curr - 1) * limit + i)) + "楼</span>" +
"<span class=\"time fl\">" + comment.createTime + "</span>" +
"<span class=\"fr\"><a href=\"javascript:void(0);\" onclick=\"javascript:BookDetail.AddAgreeTotal(77,this);\" class=\"zan\" style=\"display: none;\"><i class=\"num\">(0)</i></a>" +
"</span></li>\t\t</ul>\t</div>");
"<span class=\"fr\"><a href=\"javascript:toggleCommentUnLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\"><i class=\"num\" id='unLikeCount"+comment.id+"'>("+comment.unLikesCount+")</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:toggleCommentLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\">赞<i class=\"num\" id='likeCount"+comment.id+"'>("+comment.likesCount+")</i></a></span>" +
"</li>\t\t</ul>\t</div>");
}
$("#commentPanel").html(commentListHtml);
@@ -160,6 +161,48 @@
})
}
function toggleCommentLike(replyId) {
$.ajax({
type: "post",
url: "/book/toggleReplyLike",
data: {'replyId': replyId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#likeCount"+replyId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
function toggleCommentUnLike(replyId) {
$.ajax({
type: "post",
url: "/book/toggleReplyUnLike",
data: {'replyId': replyId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#unLikeCount"+replyId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
</script>
</body>
</html>

View File

@@ -115,10 +115,10 @@
<li class="dec" th:utext="${comment.commentContent}"></li>
<li class="other cf"><span class="time fl"
th:text="${#calendars.format(comment.createTime, 'yyyy-MM-dd HH:mm:ss')}"></span><span
class="fr"><a href="javascript:void(0);" onclick="javascript:;" class="zan"
style="padding-left: 10px">踩<i class="num">(0)</i></a></span><span
class="fr"><a href="javascript:void(0);" onclick="javascript:;" class="zan"
style="padding-left: 10px">赞<i class="num">(0)</i></a></span><span
class="fr"><a th:href="'javascript:toggleCommentUnLike(\''+${comment.id}+'\')'" onclick="javascript:;" class="zan"
style="padding-left: 10px">踩<i class="num" th:id="'unLikeCount'+${comment.id}">([[${comment.unLikesCount}]])</i></a></span><span
class="fr"><a th:href="'javascript:toggleCommentLike(\''+${comment.id}+'\')'" class="zan"
style="padding-left: 10px">赞<i class="num" th:id="'likeCount'+${comment.id}">([[${comment.likesCount}]])</i></a></span><span
class="fr"><a th:href="'/book/reply-'+${comment.id}+'.html'" class="zan"
style="padding-left: 10px">回复<i class="num">([[${comment.replyCount}]])</i></a></span>
</li>
@@ -350,8 +350,8 @@
comment.commentContent +
"</li><li class=\"other cf\">" +
"<span class=\"time fl\">" + comment.createTime + "</span>" +
"<span class=\"fr\"><a href=\"javascript:void(0);\" onclick=\"javascript:;\" class=\"zan\" style=\"padding-left: 10px\">踩<i class=\"num\">(0)</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:void(0);\" onclick=\"javascript:;\" class=\"zan\" style=\"padding-left: 10px\">赞<i class=\"num\">(0)</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:toggleCommentUnLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\">踩<i class=\"num\" id='unLikeCount"+comment.id+"'>("+comment.unLikesCount+")</i></a></span>" +
"<span class=\"fr\"><a href=\"javascript:toggleCommentLike('"+comment.id+"')\" class=\"zan\" style=\"padding-left: 10px\">赞<i class=\"num\" id='likeCount"+comment.id+"'>("+comment.likesCount+")</i></a></span>" +
"<span class=\"fr\"><a href=\"/book/reply-"+comment.id+".html\" class=\"zan\" style=\"padding-left: 10px\">回复<i class=\"num\">("+comment.replyCount+
")</i></a></span>" +
"</li>\t\t</ul>\t</div>"
@@ -380,6 +380,47 @@
}
})
}
function toggleCommentLike(commentId) {
$.ajax({
type: "post",
url: "/book/toggleCommentLike",
data: {'commentId': commentId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#likeCount"+commentId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
function toggleCommentUnLike(commentId) {
$.ajax({
type: "post",
url: "/book/toggleCommentUnLike",
data: {'commentId': commentId},
dataType: "json",
success: function (data) {
if (data.code == 200) {
$("#unLikeCount"+commentId).text("("+data.data+")")
} else {
layer.alert(data.msg);
}
},
error: function () {
layer.alert('网络异常');
}
})
}
</script>
</body>
</html>