Java 项目实战: 交友项目 - 喜欢与取消喜欢

概述

圈子的第三种互动「喜欢」是点赞的同构操作:同样的四步流程、同一张 Comment 表,只把 commentType 从 1 换成 3、hashKey 从 LIKE 换成 LOVE。这里讲清这种同构怎么复用,以及复制粘贴时最容易踩的那个坑,常量漏改(工程里真实存在,读侧 hasLiked 就被它坑了)。

纲要

  • 业务语义:喜欢与点赞的产品差异
  • 实现结构:接口、Service 方法、Redis 标记与点赞的逐项对照
  • 核心代码:loveComment / disloveComment(与点赞同构,只看差异点)
  • 复用链路:CommentApiImpl.save/delete/hasComment 按 commentType 分流的公共逻辑
  • 时序图:喜欢与取消喜欢的完整数据流
  • 工程实证:hasLiked 读侧用错 hashKey 的 bug 与修法
  • 同构操作的工程启示:三合一设计下「加一种互动类型」的成本
  • 坑表、验证步骤、常见误解澄清

业务语义:喜欢和点赞差在哪

动态列表每条动态下方有三个互动按钮:点赞、评论、喜欢。讲师在视频里说得很直白,喜欢和点赞「除了状态不一致,剩下都一样」,代码层面确实如此。但从产品角度,两者语义不同:

维度 点赞 喜欢
用户心理 轻量认可,「这条不错」 更强的偏好表达,「我对这条(发布者)感兴趣」
交互成本 随手一点,可反复切换 表态更重,列表里单独一类记录
数据落点 commentType=1,累计 likeCount commentType=3,累计 loveCount

有一个容易混淆的点要先澄清:交友项目里有两套「喜欢」。交友模块左滑右滑的「喜欢」是用户对用户 的表态,存在 USER_LIKE_SET_ 这套 Redis key 里,互相喜欢触发配对;这里圈子互动的「喜欢」是用户对动态 的表态,落在 Comment 表(commentType=3)。讲义和本章工程里没有把动态「喜欢」接入配对逻辑,所以这里的差异只到产品语义层面,不要把两套机制混为一谈。

与点赞的同构对照

先给对照表,这是这里的核心。每一处差异都只是一个枚举值或一个常量:

对照项 点赞 喜欢
接口路径 GET /movements/{id}/like GET /movements/{id}/love
取消接口 GET /movements/{id}/dislike GET /movements/{id}/unlove
Controller 方法 like() / dislike() love() / unlove()
Service 方法 likeComment() / dislikeComment() loveComment() / disloveComment()
互动类型 CommentType.LIKE(1) CommentType.LOVE(3)
计数字段 likeCount loveCount
Redis hashKey MOVEMENT_LIKE_ + userId MOVEMENT_LOVE_ + userId
重复操作异常 likeError()(000006 用户已点赞) loveError()(000008 用户已喜欢)
未操作取消异常 disLikeError()(000007 用户未点赞) disloveError()(000009 用户未喜欢)

这里改动涉及的文件分布(工程快照 05-圈子互动/01-代码/mf):

tree 复制代码
mf
├── mf-app-server
│   └── src/main/java/com/mf/server
│       ├── controller/MovementController.java    # 新增 /love、/unlove 接口
│       └── service
│           ├── CommentsService.java              # 新增 loveComment、disloveComment
│           └── MovementService.java              # 读侧 hasLiked(bug 所在)
├── mf-dubbo/mf-dubbo-mongo
│   └── src/main/java/com/mf/dubbo/api
│       └── CommentApiImpl.java                   # save/delete/hasComment 公共链路,零改动
├── mf-model
│   └── src/main/java/com/mf/model
│       ├── enums/CommentType.java                # LOVE(3) 枚举值
│       ├── mongo/Comment.java                    # 三合一互动表
│       ├── mongo/Movement.java                   # loveCount 计数 + statisCount 分流
│       └── vo/ErrorResult.java                   # loveError / disloveError
└── mf-commons
    └── src/main/java/com/mf/commons/utils
        └── Constants.java                        # MOVEMENT_LOVE_HASHKEY 常量

主 key 是同一个:MOVEMENTS_INTERACT_ + movementId。点赞和喜欢的状态存在同一个 Hash 里,靠 hashKey 前缀区分。枚举定义在 CommentType 中:

java 复制代码
package com.mf.model.enums;

/**
 * 评论类型:1-点赞,2-评论,3-喜欢
 */
public enum CommentType {

    LIKE(1), COMMENT(2), LOVE(3);

    int type;

    CommentType(int type) {
        this.type = type;
    }

    public int getType() {
        return type;
    }
}

接口层:Controller 两个方法

mf-app-server 模块的 MovementController,在点赞接口旁边追加两个方法:

java 复制代码
/**
 * 喜欢
 */
@GetMapping("/{id}/love")
public ResponseEntity love(@PathVariable("id") String movementId) {
    Integer likeCount = commentsService.loveComment(movementId);
    return ResponseEntity.ok(likeCount);
}

/**
 * 取消喜欢
 */
@GetMapping("/{id}/unlove")
public ResponseEntity unlove(@PathVariable("id") String movementId) {
    Integer likeCount = commentsService.disloveComment(movementId);
    return ResponseEntity.ok(likeCount);
}

返回值变量名叫 likeCount,实际存的是 loveCount,这是从点赞代码复制过来没改变量名的痕迹。不影响功能(返回给前端的数值是对的),但读代码的人容易误会,复制同构代码时这类命名残留值得顺手清理。

Service 层:loveComment 与 disloveComment

CommentsService 中的实现。结构与 likeComment 完全相同,只标注差异点,不逐行重复注释:

java 复制代码
//喜欢
public Integer loveComment(String movementId) {
    //1、判重:查该用户对该动态是否已有 type=3 的记录
    Boolean hasComment = commentApi.hasComment(movementId, UserHolder.getUserId(), CommentType.LOVE);
    //2、已喜欢则抛异常
    if(hasComment) {
        throw new BusinessException(ErrorResult.loveError());
    }
    //3、保存 type=3 的 Comment 到 MongoDB
    Comment comment = new Comment();
    comment.setPublishId(new ObjectId(movementId));
    comment.setCommentType(CommentType.LOVE.getType());
    comment.setUserId(UserHolder.getUserId());
    comment.setCreated(System.currentTimeMillis());
    Integer count = commentApi.save(comment);
    //4、写 Redis 标记:hashKey 用 LOVE 前缀,主 key 与点赞共用
    String key = Constants.MOVEMENTS_INTERACT_KEY + movementId;
    String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId();
    redisTemplate.opsForHash().put(key, hashKey, "1");
    return count;
}

//取消喜欢
public Integer disloveComment(String movementId) {
    //1、判重:未喜欢时取消没有意义,直接拒绝
    Boolean hasComment = commentApi.hasComment(movementId, UserHolder.getUserId(), CommentType.LOVE);
    if(!hasComment) {
        throw new BusinessException(ErrorResult.disloveError());
    }
    //2、删除 type=3 的 Comment,并拿回最新计数
    Comment comment = new Comment();
    comment.setPublishId(new ObjectId(movementId));
    comment.setCommentType(CommentType.LOVE.getType());
    comment.setUserId(UserHolder.getUserId());
    Integer count = commentApi.delete(comment);
    //3、删掉 Redis 里的 LOVE 标记
    String key = Constants.MOVEMENTS_INTERACT_KEY + movementId;
    String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId();
    redisTemplate.opsForHash().delete(key, hashKey);
    return count;
}

四步骨架:判重 → 落库 → 计数 → Redis 标记。和点赞唯一的区别集中在三处:CommentType.LOVE、MOVEMENT_LOVE_HASHKEY、loveError()/disloveError()。

公共链路:CommentApi 按 type 分流

Service 之下不用写任何新代码,CommentApi 的三个方法是三种互动共用的。这就是「三合一」设计的回报:

java 复制代码
//发布评论,并获取评论数量(文本评论/点赞/喜欢通用)
public Integer save(Comment comment) {
    //1、查询动态
    Movement movement = mongoTemplate.findById(comment.getPublishId(), Movement.class);
    //2、设置被评论人
    if(movement != null) {
        comment.setPublishUserId(movement.getUserId());
    }
    //3、保存 Comment 文档
    mongoTemplate.save(comment);
    //4、按类型对动态计数做 $inc:1→likeCount,2→commentCount,3→loveCount
    Query query = Query.query(Criteria.where("id").is(comment.getPublishId()));
    Update update = new Update();
    if(comment.getCommentType() == CommentType.LIKE.getType()) {
        update.inc("likeCount", 1);
    } else if (comment.getCommentType() == CommentType.COMMENT.getType()) {
        update.inc("commentCount", 1);
    } else {
        update.inc("loveCount", 1);
    }
    FindAndModifyOptions options = new FindAndModifyOptions();
    options.returnNew(true); //返回更新后的最新文档
    Movement modify = mongoTemplate.findAndModify(query, update, options, Movement.class);
    //5、返回最新计数
    return modify.statisCount(comment.getCommentType());
}

//判断某用户对某动态的某类互动是否已存在
public Boolean hasComment(String movementId, Long userId, CommentType commentType) {
    Criteria criteria = Criteria.where("userId").is(userId)
            .and("publishId").is(new ObjectId(movementId))
            .and("commentType").is(commentType.getType());
    Query query = Query.query(criteria);
    return mongoTemplate.exists(query, Comment.class);
}

statisCount 是 Movement 实体上的辅助方法,按类型取对应计数字段,type=3 时返回 loveCount。delete 方法与 save 对称:按 userId + publishId + commentType 三个条件 remove,再对 loveCount 做 -1 的 $inc。

判重走的是 Mongo 的 exists 查询,条件里有 commentType,所以「点过赞」不会阻止「喜欢」,两个状态天然隔离,至少写侧是隔离的,读侧马上会看到一个反例。

时序图

喜欢与取消喜欢的完整数据流:
Redis MongoDB CommentApi(Dubbo) CommentsService MovementController 客户端 Redis MongoDB CommentApi(Dubbo) CommentsService MovementController 客户端 #mermaid-svg-tLA4uZWWTxfwF1x5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tLA4uZWWTxfwF1x5 .error-icon{fill:#552222;}#mermaid-svg-tLA4uZWWTxfwF1x5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tLA4uZWWTxfwF1x5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tLA4uZWWTxfwF1x5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tLA4uZWWTxfwF1x5 .marker.cross{stroke:#333333;}#mermaid-svg-tLA4uZWWTxfwF1x5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tLA4uZWWTxfwF1x5 p{margin:0;}#mermaid-svg-tLA4uZWWTxfwF1x5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tLA4uZWWTxfwF1x5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tLA4uZWWTxfwF1x5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tLA4uZWWTxfwF1x5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tLA4uZWWTxfwF1x5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tLA4uZWWTxfwF1x5 .sequenceNumber{fill:white;}#mermaid-svg-tLA4uZWWTxfwF1x5 #sequencenumber{fill:#333;}#mermaid-svg-tLA4uZWWTxfwF1x5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tLA4uZWWTxfwF1x5 .messageText{fill:#333;stroke:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tLA4uZWWTxfwF1x5 .labelText,#mermaid-svg-tLA4uZWWTxfwF1x5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .loopText,#mermaid-svg-tLA4uZWWTxfwF1x5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tLA4uZWWTxfwF1x5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tLA4uZWWTxfwF1x5 .noteText,#mermaid-svg-tLA4uZWWTxfwF1x5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tLA4uZWWTxfwF1x5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tLA4uZWWTxfwF1x5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tLA4uZWWTxfwF1x5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tLA4uZWWTxfwF1x5 .actorPopupMenu{position:absolute;}#mermaid-svg-tLA4uZWWTxfwF1x5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tLA4uZWWTxfwF1x5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tLA4uZWWTxfwF1x5 .actor-man circle,#mermaid-svg-tLA4uZWWTxfwF1x5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tLA4uZWWTxfwF1x5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /movements/{id}/love loveComment(movementId) hasComment(movementId, userId, LOVE) exists(userId+publishId+type=3) false false save(commenttype=3) insert Comment inc loveCount +1 (findAndModify) 最新 loveCount HSET MOVEMENTS_INTERACT_{id} MOVEMENT_LOVE_{userId} "1" loveCount 200 loveCount GET /movements/{id}/unlove disloveComment(movementId) hasComment(...LOVE) exists(...) true delete(comment\[type=3\]) remove Comment inc loveCount -1 最新 loveCount HDEL MOVEMENTS_INTERACT_{id} MOVEMENT_LOVE_{userId} loveCount 200 loveCount

工程实证:hasLiked 读侧的 hashKey 用错了

讲义 2.4.3 要求「修改之前的查询圈子列表代码,从 redis 查询是否具有喜欢操作记录。同点赞」,即好友动态和推荐动态查询时,除了补一个 hasLiked 之类的喜欢标识,原有的点赞标识判断也要在。实际工程快照(MovementService)里是这样的:

java 复制代码
for (Movement movement : list) {
    UserInfo userInfo = map.get(movement.getUserId());
    if(userInfo != null) {
        MovementsVo vo = MovementsVo.init(userInfo, movement);
        //修复点赞状态的bug,判断hashKey是否存在
        String key = Constants.MOVEMENTS_INTERACT_KEY + movement.getId().toHexString();
        String hashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId();
        if(redisTemplate.opsForHash().hasKey(key, hashKey)) {
            vo.setHasLiked(1);
        }
        vos.add(vo);
    }
}

注释写着「修复点赞状态的 bug」,hashKey 却取的是 MOVEMENT_LOVE_HASHKEY(MOVEMENT_LOVE_ 前缀),不是 MOVEMENT_LIKE_HASHKEY(MOVEMENT_LIKE_ 前缀)。这正是从点赞代码复制过来改喜欢标识时,把判断点赞的 hashKey 顺手替换掉了,点赞的读侧判断被喜欢状态接管。

具体表现 :用户对某条动态只点了喜欢、没有点赞,刷新列表后这条动态的 hasLiked=1,前端把点赞按钮渲染成「已点赞」高亮。反过来,只点赞不喜欢的用户,点赞状态反而不显示。点赞和喜欢两个状态在读侧被焊死成同一个。

修法很直接,hasLiked 判断改回 LIKE 的 hashKey:

java 复制代码
// 点赞状态判断用 LIKE 的 hashKey
String likeHashKey = Constants.MOVEMENT_LIKE_HASHKEY + UserHolder.getUserId();
if(redisTemplate.opsForHash().hasKey(key, likeHashKey)) {
    vo.setHasLiked(1);
}
// 喜欢标识如需单独返回,再用 LOVE 的 hashKey 查一次,存入独立字段
String loveHashKey = Constants.MOVEMENT_LOVE_HASHKEY + UserHolder.getUserId();
if(redisTemplate.opsForHash().hasKey(key, loveHashKey)) {
    vo.setHasLiked(1); // 实际应替换为 vo.setHasLoved(1),Vo 需扩展字段
}

彻底的做法是给 MovementsVo 加 hasLoved 字段,两个状态各查各的 hashKey,互不干扰。只改一行的最小修法(第一段)能立刻止损,但喜欢标识就没地方放了,要不要加字段取决于产品是否真的展示「已喜欢」高亮。

这个 bug 是「复制粘贴同构代码」的经典事故:逻辑结构一模一样时,人眼对代码的扫读会自动跳过相同部分,最容易漏改的恰恰是那些不起眼的常量。两个常量只差四个字母(LIKE/LOVE),IDE 的语法检查完全拦不住。

工程启示:同构操作的成本账

回看三种互动类型落地的代码量:

方案 新增一种互动类型的成本
三合一 Comment 表(本项目) 1 个枚举值 + 2 个常量 + 2 个异常工厂方法 + 1 对 Controller 方法 + 1 对 Service 方法(复制改造)
分三张表(点赞表/喜欢表/评论表) 每种互动一套独立的领域模型、Repository、Service、Controller,三套 CRUD、三套判重、三套计数逻辑

三合一方案的判重、计数、删除逻辑全部收敛在 CommentApiImpl 一处,按 commentType 分流。新增「喜欢」没有动任何公共代码,只加了一个枚举分支。代价也有:所有分流点都是 if-else,类型多了以后更优雅的写法是给每种类型映射一个计数字段名(Map<CommentType, String>),消灭 if-else 链。

另一个隐性收益是查询侧的统一:findComments 按 publishId + commentType 分页查询,评论列表、点赞列表、喜欢列表用同一个方法,只是参数不同。

坑表

坑 后果 规避
复制同构代码漏改常量/枚举(这里主角,工程实证见上) 状态互相污染,且编译期无任何报警 复制后先全局搜旧常量名;或把差异抽成参数,让两处共用一份代码
重复喜欢无判重 loveCount 虚高,一个用户刷出多条 LOVE 记录 入口先 hasComment 判重,重复操作抛 loveError
计数键写错(loveCount 写成 likeCount) 喜欢数加到点赞数上,两列数据都废 CommentApiImpl 里统一分流,Service 层不直接碰计数字段
Redis 与 Mongo 脱节 Mongo 删了记录但 Redis 标记残留(或反之),读侧显示状态与数据库不符 两步操作放在同一方法内顺序执行;对一致性要求高的场景可先写 Redis 再落库,失败时补偿删除
变量名残留(如 Controller 里 likeCount 装着喜欢数) 不影响运行但误导维护者 复制后顺手改名,IDE 重命名一把搞定

验证

按讲师视频里的流程可完整复现。环境:MongoDB 与 Redis 已启动,服务以本地测试配置运行。

喜欢一条动态(客户端点喜欢按钮,或直接调接口):

bash 复制代码
curl "http://localhost:18080/movements/<movementId>/love" -H "Authorization: <token>"

查 Mongo:Comment 表多一条 commentType: 3 的记录;Movement 表对应动态的 loveCount 变 1:

javascript 复制代码
// mongosh
db.comment.find({ publishId: ObjectId("<movementId>"), commentType: 3 })
db.movement.find({ _id: ObjectId("<movementId>") }, { likeCount: 1, loveCount: 1 })

查 Redis:Hash 里多一个 MOVEMENT_LOVE_<userId> 的 hashKey:

bash 复制代码
redis-cli HKEYS "MOVEMENTS_INTERACT_<movementId>"
# 输出应包含 MOVEMENT_LIKE_xxx 与 MOVEMENT_LOVE_xxx 两类 hashKey(操作过的话)
redis-cli HGET "MOVEMENTS_INTERACT_<movementId>" "MOVEMENT_LOVE_<userId>"

取消喜欢回退:

bash 复制代码
curl "http://localhost:18080/movements/<movementId>/unlove" -H "Authorization: <token>"

再查一遍:Comment 记录消失、loveCount 回 0、Redis 的 LOVE hashKey 被删。

最后验证与点赞的状态隔离:对同一条动态只 执行 love(不调 like),然后检查 Redis,MOVEMENT_LIKE_<userId> 不存在,MOVEMENT_LOVE_<userId> 存在。按工程现状的 bug 代码,此时列表接口会错误地返回 hasLiked=1;按上文修法改回 MOVEMENT_LIKE_HASHKEY 后,hasLiked 保持 0,这一步同时验证了功能正确性和 bug 修复效果。

API 速览

接口 方法 说明 成功返回 重复/非法操作
/movements/{id}/love GET 喜欢动态 最新 loveCount 已喜欢:000008 用户已喜欢
/movements/{id}/unlove GET 取消喜欢 最新 loveCount 未喜欢:000009 用户未喜欢
/movements/{id}/like GET 点赞动态(对照) 最新 likeCount 已点赞:000006
/movements/{id}/dislike GET 取消点赞(对照) 最新 likeCount 未点赞:000007

官方文档

常见误解澄清

误解 事实
喜欢就是点赞的别名,可以只做一种 代码同构,但产品语义不同,数据上是两类记录(type=1 与 type=3),各自独立判重、独立计数
动态「喜欢」会触发交友配对 不会。配对用的是交友模块的 USER_LIKE_SET_ 体系(用户对用户),圈子互动的「喜欢」是用户对动态的 type=3 Comment,两套机制无关
喜欢状态存在单独的 Redis key 里 主 key 与点赞共用 MOVEMENTS_INTERACT_{movementId},只在 hashKey 前缀上区分 MOVEMENT_LIKE_ / MOVEMENT_LOVE_
写侧判重能保证读侧状态正确 不行。本项目写侧判重和状态隔离都是对的,读侧 hashKey 用错照样把两个状态搅在一起,读写两侧要分别验证
save() 返回的是评论总数 返回的是按本次 commentType 分流后的那一个计数字段(type=3 时是 loveCount),由 statisCount 从 findAndModify 的最新文档里取

总结

  • 喜欢与取消喜欢是点赞的同构操作:四步骨架(判重 → 落库 → 计数 → Redis 标记)逐字对应,差异只有 CommentType.LOVE、MOVEMENT_LOVE_HASHKEY、loveError/disloveError 三处。
  • 三合一 Comment 表让新增互动类型的成本降到「一个枚举值 + 一组常量 + 一对同构方法」,公共链路零改动;这是点赞/评论/喜欢合在一张表的核心回报。
  • 工程快照里读侧 hasLiked 误用 MOVEMENT_LOVE_HASHKEY,导致「只喜欢未点赞」的动态被标成已点赞,修法是读侧判断改回 MOVEMENT_LIKE_HASHKEY,若需展示喜欢标识则给 Vo 增加 hasLoved 字段。
  • 教训值得记住:复制同构代码时,逻辑不会错,错的往往是常量、枚举、字符串这类「数据差异点」,而它们恰好躲过编译检查和肉眼扫读。复制完先搜一遍旧标识符,比事后排查状态污染便宜得多。

圈子互动系列至此覆盖:概述与业务特点、发布评论、评论列表、动态点赞、取消点赞、喜欢与取消喜欢

相关推荐
摸鱼仙人~1 小时前
校招帮简历字段匹配架构分析报告
java·前端·架构
钛生智能1 小时前
OA 流程定制全参数详解(七):流程的生命周期——启用、停用、导出、导入
java·办公自动化·oa·协同办公·oa办公系统
悟道书生2 小时前
若依 RBAC 权限控制学习笔记
java·网络·笔记·sql·学习·vue
景熙55232 小时前
2.什么是业务后台系统
java·spring·tomcat·mybatis·jdbc
quantdash_cc2 小时前
如何检查滚动收益率计算中的窗口长度不足?——从数学定义到 Pandas 实现
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
古法老登瞎折腾2 小时前
大数据智能运维实战(八):从调度任务追到计算引擎——补齐排查链路
java·大数据·运维·spring ai·mcp
砚底藏山河2 小时前
python量化:实时行情WebSocket断线重连状态机
java·python·websocket·金融
衍生星球2 小时前
【Java 语言】— 方法(方法定义、调用、参数、返回值、方法重载)
java·开发语言·方法