一、@JsonSerialize 解决前端 Long 雪花 ID 精度丢失 💡
1. 问题根源
雪花算法生成Long(64位)分布式 ID,JS Number 最大安全整数:2^53 ≈ 9007199254740991;当雪花 ID 数值超过该上限,前端 JSON 接收 Number 类型时自动丢失低位精度,ID 错乱查不到数据。
2. 解决方案
java
运行
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
- 作用:Jackson 序列化 JSON 时,强制把 Long 字段转为字符串返回前端,前端以 String 接收,彻底规避 JS 数值溢出丢失精度;
- 拓展:全局配置 Jackson 转换器,批量统一所有 Long 字段序列化策略,不用逐个字段加注解。
二、评论模块:无限层级树形评论实现(父子评论分级)
1. 数据库层级设计
表格
| level | 含义 | parentId 规则 |
|---|---|---|
| 1 | 一级根评论 | parentId=0 |
| 2 | 子评论(回复根评论 / 回复他人) | parentId = 父评论主键 ID |
articleId:绑定所属文章 ID;toUid:回复目标用户 ID;- 新增评论时:根评论 level=1,非根评论 level=2。
2. 发布评论 comment () 全逻辑
java
运行
@Override
public Result comment(CommentParam commentParam) {
//1. 从ThreadLocal获取当前登录用户
SysUser sysUser = UserThreadLocal.get();
Comment comment = new Comment();
comment.setArticleId(commentParam.getArticleId());
comment.setAuthorId(sysUser.getId());
comment.setContent(commentParam.getContent());
comment.setCreateDate(System.currentTimeMillis());
//2. 判断层级:parent为空/0 → 一级评论level=1,否则子评论level=2
Long parent = commentParam.getParent();
if(parent == null || parent == 0){
comment.setLevel(1);
}else{
comment.setLevel(2);
}
//三元赋值parentId,空默认0
comment.setParentId(parent == null ? 0 : parent);
//回复目标用户ID,无则赋值0
Long toUserid = commentParam.getToUserId();
comment.setToUid(toUserid == null ? 0 : toUserid);
//3. 插入评论库
commentMapper.insert(comment);
//4. 文章评论数自增:comment_counts = comment_counts +1(原生SQL自增,原子操作防并发)
UpdateWrapper<Article> updateWrapper = Wappers.update();
updateWrapper.eq("id",comment.getArticleId())
.setSql("comment_counts=comment_counts+1");
articleMapper.update(null,updateWrapper);
return Result.success(null);
}
⚠️
setSql("comment_counts=comment_counts+1")优势:SQL 层面原子自增,多线程并发评论不会出现计数少加。
3. 查询文章评论:递归组装树形 VO
3.1 步骤拆解
- 先查level=1 的一级根评论集合;
- 遍历每一条根评论,递归查询
parentId=根ID的子评论; - 子评论绑定到父 VO 的
childrens字段,形成树形结构返回前端。
java
运行
// 顶层查询:只查当前文章一级评论
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getArticleId,id)
.eq(Comment::getLevel,1);
List<Comment> comments = commentMapper.selectList(queryWrapper);
List<CommentVo> voList = copyList(comments);
java
运行
//单个评论转VO + 递归封装子评论
private CommentVo copy(Comment comment){
CommentVo vo = new CommentVo();
BeanUtils.copyProperties(comment,vo);
//封装评论发布者信息
UserVo author = sysUserService.findUserById(comment.getAuthorId());
vo.setAuthor(author);
//level=1:根据自身ID查子评论;level=2:根据toUid查子评论
Integer level = comment.getLevel();
List<CommentVo> childList;
if(level == 1){
childList = findCommentsByParentId(comment.getId());
}else{
childList = findCommentsByParentId(comment.getToUid());
}
vo.setChildrens(childList);
return vo;
}
//根据父ID查询所有子评论
private List<CommentVo> findCommentsByParentId(long pid){
LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Comment::getParentId,pid);
List<Comment> childComments = commentMapper.selectList(wrapper);
return copyList(childComments);
}
4. 优缺点与优化方案
✅ 优点:代码简洁,无限层级评论通用;❌ 缺点:N+1 查询问题,1 次查一级评论 + N 次循环查子评论,数据量大 DB 查询频繁;👉 优化:
- 一次性查出当前文章全量评论,内存中遍历组装树形,减少 DBIO;
- 热门文章用 Redis 缓存评论树,减轻 DB 压力。
三、配套复用知识点回顾
- ThreadLocal :发布评论从
UserThreadLocal.get()拿登录用户,不用前端传作者 ID,防篡改;拦截器鉴权成功存入、请求结束 remove 防内存泄漏; - MyBatis-Plus 原生 SQL 自增 :
setSql实现字段原子 + 1,替代 Java 先查后改,规避并发超量。