feat(ai-companion): 新增评论点赞功能及点赞状态查询

This commit is contained in:
2026-01-26 21:31:32 +08:00
parent 5a58c4ff38
commit 1523ea0fbd
13 changed files with 364 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
{
"active": true,
"started_at": "2026-01-26T13:01:18.447Z",
"original_prompt": "刚刚回滚了代码现在AI陪聊角色评论需要使用KeyboardAiCompanionCommentLikeService添加一个评论点赞接口用来记录点赞和取消点赞。 ulw",
"reinforcement_count": 2,
"last_checked_at": "2026-01-26T13:21:19.022Z"
}

View File

@@ -32,6 +32,7 @@ public enum ErrorCode {
COMPANION_ID_EMPTY(40012, "AI陪聊角色ID不能为空"), COMPANION_ID_EMPTY(40012, "AI陪聊角色ID不能为空"),
COMMENT_CONTENT_EMPTY(40013, "评论内容不能为空"), COMMENT_CONTENT_EMPTY(40013, "评论内容不能为空"),
COMMENT_NOT_FOUND(40014, "评论不存在"), COMMENT_NOT_FOUND(40014, "评论不存在"),
COMMENT_ID_EMPTY(40015, "评论ID不能为空"),
TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"), TOKEN_NOT_FOUND(40102, "未能读取到有效用户令牌"),
TOKEN_INVALID(40103, "令牌无效"), TOKEN_INVALID(40103, "令牌无效"),
TOKEN_TIMEOUT(40104, "令牌已过期"), TOKEN_TIMEOUT(40104, "令牌已过期"),

View File

@@ -113,7 +113,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/chat/voice", "/chat/voice",
"/chat/audio/*", "/chat/audio/*",
"/ai-companion/page", "/ai-companion/page",
"/chat/history" "/chat/history",
"/ai-companion/comment/add"
}; };
} }
@Bean @Bean

View File

@@ -8,9 +8,11 @@ import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils; import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.exception.BusinessException; import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.dto.comment.CommentAddReq; import com.yolo.keyborad.model.dto.comment.CommentAddReq;
import com.yolo.keyborad.model.dto.comment.CommentLikeReq;
import com.yolo.keyborad.model.dto.comment.CommentPageReq; import com.yolo.keyborad.model.dto.comment.CommentPageReq;
import com.yolo.keyborad.model.vo.CommentVO; import com.yolo.keyborad.model.vo.CommentVO;
import com.yolo.keyborad.service.KeyboardAiCompanionCommentService; import com.yolo.keyborad.service.KeyboardAiCompanionCommentService;
import com.yolo.keyborad.service.KeyboardAiCompanionCommentLikeService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -30,6 +32,9 @@ public class AiCompanionCommentController {
@Resource @Resource
private KeyboardAiCompanionCommentService commentService; private KeyboardAiCompanionCommentService commentService;
@Resource
private KeyboardAiCompanionCommentLikeService commentLikeService;
@PostMapping("/add") @PostMapping("/add")
@Operation(summary = "发表评论", description = "用户对AI陪聊角色发表评论") @Operation(summary = "发表评论", description = "用户对AI陪聊角色发表评论")
public BaseResponse<Long> addComment(@RequestBody CommentAddReq req) { public BaseResponse<Long> addComment(@RequestBody CommentAddReq req) {
@@ -47,14 +52,27 @@ public class AiCompanionCommentController {
} }
@PostMapping("/page") @PostMapping("/page")
@Operation(summary = "分页查询评论", description = "分页查询AI陪聊角色的评论列表") @Operation(summary = "分页查询评论", description = "分页查询AI陪聊角色的评论列表,包含当前用户是否已点赞状态")
public BaseResponse<IPage<CommentVO>> pageComments(@RequestBody CommentPageReq req) { public BaseResponse<IPage<CommentVO>> pageComments(@RequestBody CommentPageReq req) {
if (req.getCompanionId() == null) { if (req.getCompanionId() == null) {
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY); throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
} }
IPage<CommentVO> result = commentService.pageComments(req.getCompanionId(), Long userId = StpUtil.getLoginIdAsLong();
IPage<CommentVO> result = commentService.pageCommentsWithLikeStatus(userId, req.getCompanionId(),
req.getPageNum(), req.getPageSize()); req.getPageNum(), req.getPageSize());
return ResultUtils.success(result); return ResultUtils.success(result);
} }
@PostMapping("/like")
@Operation(summary = "点赞/取消点赞", description = "对评论进行点赞或取消点赞操作返回true表示点赞成功false表示取消点赞成功")
public BaseResponse<Boolean> toggleLike(@RequestBody CommentLikeReq req) {
if (req.getCommentId() == null) {
throw new BusinessException(ErrorCode.COMMENT_ID_EMPTY);
}
Long userId = StpUtil.getLoginIdAsLong();
boolean result = commentLikeService.toggleLike(userId, req.getCommentId());
return ResultUtils.success(result);
}
} }

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yolo.keyborad.model.entity.KeyboardAiCompanionCommentLike;
/*
* @author: ziin
* @date: 2026/1/26 20:57
*/
public interface KeyboardAiCompanionCommentLikeMapper extends BaseMapper<KeyboardAiCompanionCommentLike> {
}

View File

@@ -0,0 +1,16 @@
package com.yolo.keyborad.model.dto.comment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/*
* @author: ziin
* @date: 2026/1/26
*/
@Data
@Schema(description = "评论点赞请求")
public class CommentLikeReq {
@Schema(description = "评论ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long commentId;
}

View File

@@ -0,0 +1,64 @@
package com.yolo.keyborad.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Date;
import lombok.Data;
/*
* @author: ziin
* @date: 2026/1/26 20:57
*/
/**
* 用户对AI陪伴角色评论的点赞记录表用于记录点赞与取消点赞行为
*/
@Schema(description = "用户对AI陪伴角色评论的点赞记录表用于记录点赞与取消点赞行为")
@Data
@TableName(value = "keyboard_ai_companion_comment_like")
public class KeyboardAiCompanionCommentLike {
/**
* 评论点赞记录唯一ID
*/
@TableId(value = "id", type = IdType.AUTO)
@Schema(description = "评论点赞记录唯一ID")
private Long id;
/**
* 被点赞的评论ID
*/
@TableField(value = "comment_id")
@Schema(description = "被点赞的评论ID")
private Long commentId;
/**
* 点赞用户ID
*/
@TableField(value = "user_id")
@Schema(description = "点赞用户ID")
private Long userId;
/**
* 点赞状态1=已点赞0=已取消
*/
@TableField(value = "\"status\"")
@Schema(description = "点赞状态1=已点赞0=已取消")
private Short status;
/**
* 点赞记录创建时间
*/
@TableField(value = "created_at")
@Schema(description = "点赞记录创建时间")
private Date createdAt;
/**
* 点赞状态更新时间
*/
@TableField(value = "updated_at")
@Schema(description = "点赞状态更新时间")
private Date updatedAt;
}

View File

@@ -40,6 +40,9 @@ public class CommentVO {
@Schema(description = "点赞数") @Schema(description = "点赞数")
private Integer likeCount; private Integer likeCount;
@Schema(description = "当前用户是否已点赞")
private Boolean liked;
@Schema(description = "评论创建时间") @Schema(description = "评论创建时间")
private Date createdAt; private Date createdAt;
} }

View File

@@ -0,0 +1,41 @@
package com.yolo.keyborad.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yolo.keyborad.model.entity.KeyboardAiCompanionCommentLike;
import java.util.List;
import java.util.Set;
/*
* @author: ziin
* @date: 2026/1/26
*/
public interface KeyboardAiCompanionCommentLikeService extends IService<KeyboardAiCompanionCommentLike> {
/**
* 检查用户是否已点赞该评论
*
* @param userId 用户ID
* @param commentId 评论ID
* @return 是否已点赞
*/
boolean hasLiked(Long userId, Long commentId);
/**
* 批量检查用户是否已点赞评论
*
* @param userId 用户ID
* @param commentIds 评论ID列表
* @return 已点赞的评论ID集合
*/
Set<Long> getLikedCommentIds(Long userId, List<Long> commentIds);
/**
* 点赞或取消点赞评论
*
* @param userId 用户ID
* @param commentId 评论ID
* @return true=点赞成功false=取消点赞成功
*/
boolean toggleLike(Long userId, Long commentId);
}

View File

@@ -32,4 +32,15 @@ public interface KeyboardAiCompanionCommentService extends IService<KeyboardAiCo
* @return 分页结果 * @return 分页结果
*/ */
IPage<CommentVO> pageComments(Long companionId, Integer pageNum, Integer pageSize); IPage<CommentVO> pageComments(Long companionId, Integer pageNum, Integer pageSize);
/**
* 分页查询评论(带用户点赞状态)
*
* @param userId 当前用户ID
* @param companionId AI陪伴角色ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 分页结果
*/
IPage<CommentVO> pageCommentsWithLikeStatus(Long userId, Long companionId, Integer pageNum, Integer pageSize);
} }

View File

@@ -0,0 +1,111 @@
package com.yolo.keyborad.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.mapper.KeyboardAiCompanionCommentLikeMapper;
import com.yolo.keyborad.mapper.KeyboardAiCompanionCommentMapper;
import com.yolo.keyborad.model.entity.KeyboardAiCompanionComment;
import com.yolo.keyborad.model.entity.KeyboardAiCompanionCommentLike;
import com.yolo.keyborad.service.KeyboardAiCompanionCommentLikeService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/*
* @author: ziin
* @date: 2026/1/26
*/
@Service
public class KeyboardAiCompanionCommentLikeServiceImpl extends ServiceImpl<KeyboardAiCompanionCommentLikeMapper, KeyboardAiCompanionCommentLike> implements KeyboardAiCompanionCommentLikeService {
@Resource
private KeyboardAiCompanionCommentMapper commentMapper;
@Override
public boolean hasLiked(Long userId, Long commentId) {
LambdaQueryWrapper<KeyboardAiCompanionCommentLike> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardAiCompanionCommentLike::getUserId, userId)
.eq(KeyboardAiCompanionCommentLike::getCommentId, commentId)
.eq(KeyboardAiCompanionCommentLike::getStatus, (short) 1);
return this.count(queryWrapper) > 0;
}
@Override
public Set<Long> getLikedCommentIds(Long userId, List<Long> commentIds) {
if (commentIds == null || commentIds.isEmpty()) {
return new HashSet<>();
}
LambdaQueryWrapper<KeyboardAiCompanionCommentLike> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardAiCompanionCommentLike::getUserId, userId)
.in(KeyboardAiCompanionCommentLike::getCommentId, commentIds)
.eq(KeyboardAiCompanionCommentLike::getStatus, (short) 1);
List<KeyboardAiCompanionCommentLike> likes = this.list(queryWrapper);
return likes.stream()
.map(KeyboardAiCompanionCommentLike::getCommentId)
.collect(Collectors.toSet());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleLike(Long userId, Long commentId) {
// 检查评论是否存在
KeyboardAiCompanionComment comment = commentMapper.selectById(commentId);
if (comment == null || comment.getStatus() != 1) {
throw new BusinessException(ErrorCode.COMMENT_NOT_FOUND);
}
// 查找现有点赞记录
LambdaQueryWrapper<KeyboardAiCompanionCommentLike> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardAiCompanionCommentLike::getUserId, userId)
.eq(KeyboardAiCompanionCommentLike::getCommentId, commentId);
KeyboardAiCompanionCommentLike existingLike = this.getOne(queryWrapper);
Date now = new Date();
boolean isLiked;
if (existingLike != null) {
// 切换点赞状态
if (existingLike.getStatus() == 1) {
// 取消点赞
existingLike.setStatus((short) 0);
existingLike.setUpdatedAt(now);
this.updateById(existingLike);
// 减少点赞数
comment.setLikeCount(Math.max(0, comment.getLikeCount() - 1));
isLiked = false;
} else {
// 重新点赞
existingLike.setStatus((short) 1);
existingLike.setUpdatedAt(now);
this.updateById(existingLike);
// 增加点赞数
comment.setLikeCount(comment.getLikeCount() + 1);
isLiked = true;
}
} else {
// 新增点赞记录
KeyboardAiCompanionCommentLike like = new KeyboardAiCompanionCommentLike();
like.setUserId(userId);
like.setCommentId(commentId);
like.setStatus((short) 1);
like.setCreatedAt(now);
like.setUpdatedAt(now);
this.save(like);
// 增加点赞数
comment.setLikeCount(comment.getLikeCount() + 1);
isLiked = true;
}
// 更新评论点赞数
commentMapper.updateById(comment);
return isLiked;
}
}

View File

@@ -9,6 +9,7 @@ import com.yolo.keyborad.model.entity.KeyboardAiCompanionComment;
import com.yolo.keyborad.model.entity.KeyboardUser; import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.vo.CommentVO; import com.yolo.keyborad.model.vo.CommentVO;
import com.yolo.keyborad.service.KeyboardAiCompanionCommentService; import com.yolo.keyborad.service.KeyboardAiCompanionCommentService;
import com.yolo.keyborad.service.KeyboardAiCompanionCommentLikeService;
import com.yolo.keyborad.service.UserService; import com.yolo.keyborad.service.UserService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,6 +17,7 @@ import org.springframework.stereotype.Service;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/* /*
@@ -28,6 +30,9 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
@Resource @Resource
private UserService userService; private UserService userService;
@Resource
private KeyboardAiCompanionCommentLikeService commentLikeService;
@Override @Override
public Long addComment(Long userId, Long companionId, String content, Long parentId, Long rootId) { public Long addComment(Long userId, Long companionId, String content, Long parentId, Long rootId) {
KeyboardAiCompanionComment comment = new KeyboardAiCompanionComment(); KeyboardAiCompanionComment comment = new KeyboardAiCompanionComment();
@@ -88,4 +93,57 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
return vo; return vo;
}); });
} }
@Override
public IPage<CommentVO> pageCommentsWithLikeStatus(Long userId, Long companionId, Integer pageNum, Integer pageSize) {
Page<KeyboardAiCompanionComment> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<KeyboardAiCompanionComment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardAiCompanionComment::getCompanionId, companionId)
.eq(KeyboardAiCompanionComment::getStatus, 1)
.isNull(KeyboardAiCompanionComment::getParentId)
.orderByDesc(KeyboardAiCompanionComment::getCreatedAt);
IPage<KeyboardAiCompanionComment> entityPage = this.page(page, queryWrapper);
// 获取所有用户ID
List<Long> userIds = entityPage.getRecords().stream()
.map(KeyboardAiCompanionComment::getUserId)
.distinct()
.collect(Collectors.toList());
// 批量查询用户信息
Map<Long, KeyboardUser> userMap = Map.of();
if (!userIds.isEmpty()) {
List<KeyboardUser> users = userService.listByIds(userIds);
userMap = users.stream().collect(Collectors.toMap(KeyboardUser::getId, u -> u));
}
// 获取当前用户已点赞的评论ID
List<Long> commentIds = entityPage.getRecords().stream()
.map(KeyboardAiCompanionComment::getId)
.collect(Collectors.toList());
Set<Long> likedCommentIds = commentLikeService.getLikedCommentIds(userId, commentIds);
// 转换为VO
Map<Long, KeyboardUser> finalUserMap = userMap;
return entityPage.convert(entity -> {
CommentVO vo = new CommentVO();
vo.setId(entity.getId());
vo.setCompanionId(entity.getCompanionId());
vo.setUserId(entity.getUserId());
vo.setParentId(entity.getParentId());
vo.setRootId(entity.getRootId());
vo.setContent(entity.getContent());
vo.setLikeCount(entity.getLikeCount());
vo.setCreatedAt(entity.getCreatedAt());
vo.setLiked(likedCommentIds.contains(entity.getId()));
// 填充用户信息
KeyboardUser user = finalUserMap.get(entity.getUserId());
if (user != null) {
vo.setUserName(user.getNickName());
vo.setUserAvatar(user.getAvatarUrl());
}
return vo;
});
}
} }

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yolo.keyborad.mapper.KeyboardAiCompanionCommentLikeMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardAiCompanionCommentLike">
<!--@mbg.generated-->
<!--@Table keyboard_ai_companion_comment_like-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="comment_id" jdbcType="BIGINT" property="commentId" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="status" jdbcType="SMALLINT" property="status" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, comment_id, user_id, "status", created_at, updated_at
</sql>
</mapper>