Compare commits

..

2 Commits

Author SHA1 Message Date
f28e6b7c39 fix(service): 延后VIP体验次数扣减至AI响应成功后 2026-01-27 21:11:45 +08:00
22e5041447 fix(ai-companion): 修复点赞状态与评论回复展示逻辑
- 分页查询接口新增当前用户点赞状态返回
- CommentVO 新增 replies 与 replyCount 字段支持嵌套回复
- 评论服务支持查询一级评论及其前 999 条回复
- 免登录白名单新增 /ai-companion/comment/page 接口
2026-01-27 19:42:44 +08:00
9 changed files with 191 additions and 50 deletions

View File

@@ -2,6 +2,6 @@
"active": true,
"started_at": "2026-01-26T13:01:18.447Z",
"original_prompt": "刚刚回滚了代码现在AI陪聊角色评论需要使用KeyboardAiCompanionCommentLikeService添加一个评论点赞接口用来记录点赞和取消点赞。 ulw",
"reinforcement_count": 8,
"last_checked_at": "2026-01-27T10:35:42.226Z"
"reinforcement_count": 10,
"last_checked_at": "2026-01-27T11:00:42.142Z"
}

View File

@@ -115,7 +115,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/ai-companion/page",
"/chat/history",
"/ai-companion/comment/add",
"/speech/transcribe"
"/speech/transcribe",
"/ai-companion/comment/page"
};
}
@Bean

View File

@@ -34,9 +34,10 @@ public class AiCompanionController {
private KeyboardAiCompanionLikeService aiCompanionLikeService;
@PostMapping("/page")
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表")
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表,包含点赞数、评论数和当前用户点赞状态")
public BaseResponse<IPage<AiCompanionVO>> pageList(@RequestBody PageDTO pageDTO) {
IPage<AiCompanionVO> result = aiCompanionService.pageList(pageDTO.getPageNum(), pageDTO.getPageSize());
Long userId = StpUtil.getLoginIdAsLong();
IPage<AiCompanionVO> result = aiCompanionService.pageListWithLikeStatus(userId, pageDTO.getPageNum(), pageDTO.getPageSize());
return ResultUtils.success(result);
}

View File

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

View File

@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
import java.util.List;
/*
* @author: ziin
@@ -45,4 +46,10 @@ public class CommentVO {
@Schema(description = "评论创建时间")
private Date createdAt;
@Schema(description = "回复列表仅一级评论有值默认返回前3条")
private List<CommentVO> replies;
@Schema(description = "回复总数")
private Integer replyCount;
}

View File

@@ -20,6 +20,16 @@ public interface KeyboardAiCompanionService extends IService<KeyboardAiCompanion
*/
IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize);
/**
* 分页查询已上线的AI陪聊角色带当前用户点赞状态
*
* @param userId 当前用户ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 分页结果
*/
IPage<AiCompanionVO> pageListWithLikeStatus(Long userId, Integer pageNum, Integer pageSize);
/**
* 根据AI人设ID获取系统提示词
*

View File

@@ -376,41 +376,29 @@ public class ChatServiceImpl implements ChatService {
public ChatMessageVO message(String content, String userId, Long companionId) {
log.info("同步对话请求, userId: {}, companionId: {}, content: {}", userId, companionId, content);
// ============ VIP等级检查 ============
// ============ VIP等级检查(先检查,不增加次数) ============
AppConfig appConfig = cfgHolder.getRef().get();
KeyboardUser user = userService.getById(Long.parseLong(userId));
// 获取VIP等级null视为0
int vipLevel = user != null && user.getVipLevel() != null ? user.getVipLevel() : 0;
// 如果VIP等级 <= 1需要限制每日体验次数
if (vipLevel <= 1) {
Integer dailyLimit = appConfig.getUserRegisterProperties().getVipFreeTrialTalk();
// 记录是否需要扣减体验次数VIP等级 <= 1 的用户需要扣减)
boolean needDeductQuota = vipLevel <= 1;
String redisKey = CHAT_DAILY_LIMIT_PREFIX + userId;
Integer dailyLimit = appConfig.getUserRegisterProperties().getVipFreeTrialTalk();
// 如果VIP等级 <= 1先检查每日体验次数是否用完
if (needDeductQuota) {
// 获取当前使用次数
String countStr = stringRedisTemplate.opsForValue().get(redisKey);
int currentCount = countStr != null ? Integer.parseInt(countStr) : 0;
// 检查是否超出限制
// 检查是否超出限制,超出直接返回
if (currentCount >= dailyLimit) {
log.warn("用户 {} VIP等级 {} 已达到每日体验次数限制 {}", userId, vipLevel, dailyLimit);
throw new BusinessException(ErrorCode.VIP_TRIAL_LIMIT_REACHED);
}
// 增加使用次数
Long newCount = stringRedisTemplate.opsForValue().increment(redisKey);
// 设置到午夜过期(只有第一次设置时需要设置过期时间)
if (newCount != null && newCount == 1) {
// 计算到今天午夜的剩余秒数
LocalDateTime now = LocalDateTime.now(ZoneId.of("America/New_York"));
LocalDateTime midnight = LocalDateTime.of(LocalDate.now(ZoneId.of("America/New_York")).plusDays(1), LocalTime.MIDNIGHT);
long secondsUntilMidnight = ChronoUnit.SECONDS.between(now, midnight);
stringRedisTemplate.expire(redisKey, secondsUntilMidnight, TimeUnit.SECONDS);
}
log.info("用户 {} VIP等级 {} 今日已使用 {}/{} 次", userId, vipLevel, newCount, dailyLimit);
}
long startTime = System.currentTimeMillis();
@@ -431,6 +419,22 @@ public class ChatServiceImpl implements ChatService {
// 保存用户消息和AI响应到聊天记录
saveChatMessages(Long.parseLong(userId), companionId, content, response);
// ============ 成功后扣减体验次数 ============
if (needDeductQuota) {
Long newCount = stringRedisTemplate.opsForValue().increment(redisKey);
// 设置到午夜过期(只有第一次设置时需要设置过期时间)
if (newCount != null && newCount == 1) {
// 计算到今天午夜的剩余秒数
LocalDateTime now = LocalDateTime.now(ZoneId.of("America/New_York"));
LocalDateTime midnight = LocalDateTime.of(LocalDate.now(ZoneId.of("America/New_York")).plusDays(1), LocalTime.MIDNIGHT);
long secondsUntilMidnight = ChronoUnit.SECONDS.between(now, midnight);
stringRedisTemplate.expire(redisKey, secondsUntilMidnight, TimeUnit.SECONDS);
}
log.info("用户 {} VIP等级 {} 今日已使用 {}/{} 次", userId, vipLevel, newCount, dailyLimit);
}
// 生成音频任务 ID
String audioId = UUID.randomUUID().toString().replace("-", "");

View File

@@ -14,6 +14,7 @@ import com.yolo.keyborad.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -104,12 +105,51 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
.orderByDesc(KeyboardAiCompanionComment::getCreatedAt);
IPage<KeyboardAiCompanionComment> entityPage = this.page(page, queryWrapper);
// 获取所有用户ID
List<Long> userIds = entityPage.getRecords().stream()
.map(KeyboardAiCompanionComment::getUserId)
.distinct()
// 获取所有一级评论ID
List<Long> topCommentIds = entityPage.getRecords().stream()
.map(KeyboardAiCompanionComment::getId)
.collect(Collectors.toList());
// 批量查询回复每条一级评论取前3条回复
Map<Long, List<KeyboardAiCompanionComment>> repliesMap = Map.of();
Map<Long, Long> replyCountMap = Map.of();
if (!topCommentIds.isEmpty()) {
// 查询所有回复
LambdaQueryWrapper<KeyboardAiCompanionComment> replyWrapper = new LambdaQueryWrapper<>();
replyWrapper.in(KeyboardAiCompanionComment::getRootId, topCommentIds)
.eq(KeyboardAiCompanionComment::getStatus, 1)
.orderByAsc(KeyboardAiCompanionComment::getCreatedAt);
List<KeyboardAiCompanionComment> allReplies = this.list(replyWrapper);
// 按rootId分组每组取前3条
repliesMap = allReplies.stream()
.collect(Collectors.groupingBy(KeyboardAiCompanionComment::getRootId));
// 统计回复数量
replyCountMap = repliesMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> (long) e.getValue().size()));
// 每组只保留前3条
repliesMap = repliesMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().limit(999).collect(Collectors.toList())
));
}
// 收集所有需要查询的用户ID一级评论 + 回复)
List<Long> userIds = new ArrayList<>(entityPage.getRecords().stream()
.map(KeyboardAiCompanionComment::getUserId)
.collect(Collectors.toSet()));
repliesMap.values().stream()
.flatMap(List::stream)
.map(KeyboardAiCompanionComment::getUserId)
.forEach(uid -> {
if (!userIds.contains(uid)) {
userIds.add(uid);
}
});
// 批量查询用户信息
Map<Long, KeyboardUser> userMap = Map.of();
if (!userIds.isEmpty()) {
@@ -117,15 +157,38 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
userMap = users.stream().collect(Collectors.toMap(KeyboardUser::getId, u -> u));
}
// 获取当前用户已点赞的评论ID
List<Long> commentIds = entityPage.getRecords().stream()
// 收集所有评论ID用于查询点赞状态一级评论 + 回复)
List<Long> allCommentIds = new ArrayList<>(topCommentIds);
repliesMap.values().stream()
.flatMap(List::stream)
.map(KeyboardAiCompanionComment::getId)
.collect(Collectors.toList());
Set<Long> likedCommentIds = commentLikeService.getLikedCommentIds(userId, commentIds);
.forEach(allCommentIds::add);
Set<Long> likedCommentIds = commentLikeService.getLikedCommentIds(userId, allCommentIds);
// 转换为VO
Map<Long, KeyboardUser> finalUserMap = userMap;
Map<Long, List<KeyboardAiCompanionComment>> finalRepliesMap = repliesMap;
Map<Long, Long> finalReplyCountMap = replyCountMap;
return entityPage.convert(entity -> {
CommentVO vo = convertToVO(entity, finalUserMap, likedCommentIds);
// 填充回复列表
List<KeyboardAiCompanionComment> replies = finalRepliesMap.getOrDefault(entity.getId(), List.of());
List<CommentVO> replyVOs = replies.stream()
.map(reply -> convertToVO(reply, finalUserMap, likedCommentIds))
.collect(Collectors.toList());
vo.setReplies(replyVOs);
vo.setReplyCount(finalReplyCountMap.getOrDefault(entity.getId(), 0L).intValue());
return vo;
});
}
/**
* 将评论实体转换为VO
*/
private CommentVO convertToVO(KeyboardAiCompanionComment entity, Map<Long, KeyboardUser> userMap, Set<Long> likedCommentIds) {
CommentVO vo = new CommentVO();
vo.setId(entity.getId());
vo.setCompanionId(entity.getCompanionId());
@@ -138,12 +201,11 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
vo.setLiked(likedCommentIds.contains(entity.getId()));
// 填充用户信息
KeyboardUser user = finalUserMap.get(entity.getUserId());
KeyboardUser user = userMap.get(entity.getUserId());
if (user != null) {
vo.setUserName(user.getNickName());
vo.setUserAvatar(user.getAvatarUrl());
}
return vo;
});
}
}

View File

@@ -20,6 +20,7 @@ import com.yolo.keyborad.service.KeyboardAiCompanionService;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/*
@@ -83,6 +84,58 @@ public class KeyboardAiCompanionServiceImpl extends ServiceImpl<KeyboardAiCompan
});
}
@Override
public IPage<AiCompanionVO> pageListWithLikeStatus(Long userId, Integer pageNum, Integer pageSize) {
Page<KeyboardAiCompanion> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<KeyboardAiCompanion> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardAiCompanion::getStatus, 1)
.eq(KeyboardAiCompanion::getVisibility, 1)
.orderByDesc(KeyboardAiCompanion::getSortOrder)
.orderByDesc(KeyboardAiCompanion::getPopularityScore);
IPage<KeyboardAiCompanion> entityPage = this.page(page, queryWrapper);
// 获取所有角色ID
List<Long> companionIds = entityPage.getRecords().stream()
.map(KeyboardAiCompanion::getId)
.collect(Collectors.toList());
// 批量统计点赞数
Map<Long, Long> likeCountMap = Map.of();
if (!companionIds.isEmpty()) {
LambdaQueryWrapper<KeyboardAiCompanionLike> likeWrapper = new LambdaQueryWrapper<>();
likeWrapper.in(KeyboardAiCompanionLike::getCompanionId, companionIds)
.eq(KeyboardAiCompanionLike::getStatus, (short) 1);
List<KeyboardAiCompanionLike> likes = companionLikeService.list(likeWrapper);
likeCountMap = likes.stream()
.collect(Collectors.groupingBy(KeyboardAiCompanionLike::getCompanionId, Collectors.counting()));
}
// 批量统计评论数
Map<Long, Long> commentCountMap = Map.of();
if (!companionIds.isEmpty()) {
LambdaQueryWrapper<KeyboardAiCompanionComment> commentWrapper = new LambdaQueryWrapper<>();
commentWrapper.in(KeyboardAiCompanionComment::getCompanionId, companionIds)
.eq(KeyboardAiCompanionComment::getStatus, (short) 1);
List<KeyboardAiCompanionComment> comments = companionCommentService.list(commentWrapper);
commentCountMap = comments.stream()
.collect(Collectors.groupingBy(KeyboardAiCompanionComment::getCompanionId, Collectors.counting()));
}
// 获取当前用户已点赞的角色ID
Set<Long> likedCompanionIds = companionLikeService.getLikedCompanionIds(userId, companionIds);
// 转换为VO并填充统计数据和点赞状态
Map<Long, Long> finalLikeCountMap = likeCountMap;
Map<Long, Long> finalCommentCountMap = commentCountMap;
return entityPage.convert(entity -> {
AiCompanionVO vo = BeanUtil.copyProperties(entity, AiCompanionVO.class);
vo.setLikeCount(finalLikeCountMap.getOrDefault(entity.getId(), 0L).intValue());
vo.setCommentCount(finalCommentCountMap.getOrDefault(entity.getId(), 0L).intValue());
vo.setLiked(likedCompanionIds.contains(entity.getId()));
return vo;
});
}
@Override
public String getSystemPromptById(Long companionId) {
KeyboardAiCompanion companion = this.getById(companionId);