Compare commits
6 Commits
392d9ecfe8
...
d3abe32e1a
| Author | SHA1 | Date | |
|---|---|---|---|
| d3abe32e1a | |||
| e7437a39b2 | |||
| ca2cd87d89 | |||
| 190fb95bb6 | |||
| 884e5f5da4 | |||
| 7f6edde956 |
@@ -57,67 +57,17 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
"/swagger-ui/**",
|
||||
"/favicon.ico",
|
||||
// 你的其他放行路径,例如登录接口
|
||||
"/demo/test",
|
||||
"/error",
|
||||
"/demo/talk",
|
||||
"/user/appleLogin",
|
||||
"/demo/embed",
|
||||
"/demo/testSaveEmbed",
|
||||
"/demo/testSearch",
|
||||
"/demo/testSearchText",
|
||||
"/file/upload",
|
||||
"/user/logout",
|
||||
"/tag/list",
|
||||
"/character/detail",
|
||||
"/user/login",
|
||||
"/character/listByUser",
|
||||
"/user/detail",
|
||||
"/user/register",
|
||||
"/user/updateInfo",
|
||||
"/character/updateUserCharacterSort",
|
||||
"/character/delUserCharacter",
|
||||
"/user/sendVerifyMail",
|
||||
"/user/verifyMailCode",
|
||||
"/character/listWithNotLogin",
|
||||
"/character/listByTagWithNotLogin",
|
||||
"/character/listByTag",
|
||||
"/character/detailWithNotLogin",
|
||||
"/character/addUserCharacter",
|
||||
"/character/list",
|
||||
"/user/resetPassWord",
|
||||
"/chat/talk",
|
||||
"/chat/save_embed",
|
||||
"/themes/listByStyle",
|
||||
"/wallet/balance",
|
||||
"/themes/purchase",
|
||||
"/themes/purchased",
|
||||
"/themes/purchase/list",
|
||||
"/themes/detail",
|
||||
"/themes/recommended",
|
||||
"/themes/search",
|
||||
"/user-themes/batch-delete",
|
||||
"/products/listByType",
|
||||
"/products/detail",
|
||||
"/products/inApp/list",
|
||||
"/products/subscription/list",
|
||||
"/purchase/handle",
|
||||
"/apple/notification",
|
||||
"/apple/receipt",
|
||||
"/apple/validate-receipt",
|
||||
"/user/inviteCode",
|
||||
"/user/bindInviteCode",
|
||||
"/themes/listAllStyles",
|
||||
"/wallet/transactions",
|
||||
"/themes/restore",
|
||||
"/chat/message",
|
||||
"/chat/voice",
|
||||
"/chat/audio/*",
|
||||
"/ai-companion/page",
|
||||
"/chat/history",
|
||||
"/ai-companion/comment/add",
|
||||
"/speech/transcribe",
|
||||
"/ai-companion/comment/page",
|
||||
"/ai-companion/liked"
|
||||
"/ai-companion/report",
|
||||
"/apple/notification"
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.mapper.QdrantPayloadMapper;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatHistoryDeleteReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatHistoryPageReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatMessageReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
@@ -19,7 +20,6 @@ import com.yolo.keyborad.model.vo.AudioTaskVO;
|
||||
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
||||
import com.yolo.keyborad.model.vo.ChatMessageVO;
|
||||
import com.yolo.keyborad.model.vo.ChatSessionVO;
|
||||
import com.yolo.keyborad.model.vo.ChatVoiceVO;
|
||||
import com.yolo.keyborad.service.ChatService;
|
||||
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
||||
import com.yolo.keyborad.service.KeyboardAiChatSessionService;
|
||||
@@ -138,6 +138,18 @@ public class ChatController {
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/history/delete")
|
||||
@Operation(summary = "删除聊天记录", description = "根据聊天记录ID逻辑删除聊天消息")
|
||||
public BaseResponse<Boolean> deleteHistory(@RequestBody ChatHistoryDeleteReq req) {
|
||||
if (req == null || req.getId() == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "聊天记录ID不能为空");
|
||||
}
|
||||
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
aiChatMessageService.deleteMessageById(userId, req.getId());
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/session/reset")
|
||||
@Operation(summary = "重置会话", description = "重置与AI角色的聊天会话,将当前会话设为不活跃并创建新会话,后续聊天记录将绑定到新会话")
|
||||
public BaseResponse<ChatSessionVO> resetSession(@RequestBody SessionResetReq req) {
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
package com.yolo.keyborad.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.yolo.keyborad.common.BaseResponse;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.common.ResultUtils;
|
||||
import com.yolo.keyborad.config.AppConfig;
|
||||
import com.yolo.keyborad.config.NacosAppConfigCenter;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
import com.yolo.keyborad.model.vo.SpeechToTextVO;
|
||||
import com.yolo.keyborad.service.DeepgramService;
|
||||
import com.yolo.keyborad.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 语音服务控制器
|
||||
*
|
||||
@@ -22,13 +37,71 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@Tag(name = "语音服务", description = "语音相关功能接口")
|
||||
public class SpeechController {
|
||||
|
||||
private static final String SPEECH_DAILY_LIMIT_PREFIX = "speech:daily:limit:";
|
||||
|
||||
@Resource
|
||||
private DeepgramService deepgramService;
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private final NacosAppConfigCenter.DynamicAppConfig cfgHolder;
|
||||
|
||||
public SpeechController(NacosAppConfigCenter.DynamicAppConfig cfgHolder) {
|
||||
this.cfgHolder = cfgHolder;
|
||||
}
|
||||
|
||||
@PostMapping("/transcribe")
|
||||
@Operation(summary = "语音转文字", description = "上传音频文件并转换为文本")
|
||||
public BaseResponse<SpeechToTextVO> transcribe(@RequestPart("file") MultipartFile file) {
|
||||
// 获取当前登录用户ID
|
||||
Long userId = StpUtil.getLoginIdAsLong();
|
||||
|
||||
// 查询用户信息
|
||||
KeyboardUser user = userService.getById(userId);
|
||||
// 获取VIP等级(null视为0)
|
||||
int vipLevel = user != null && user.getVipLevel() != null ? user.getVipLevel() : 0;
|
||||
|
||||
// vipLevel >= 2 不做限制,直接放行
|
||||
if (vipLevel < 2) {
|
||||
AppConfig appConfig = cfgHolder.getRef().get();
|
||||
String redisKey = SPEECH_DAILY_LIMIT_PREFIX + userId;
|
||||
Integer dailyLimit = appConfig.getUserRegisterProperties().getVipFreeTrialTalk();
|
||||
|
||||
// 获取当前使用次数
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用语音转文字服务
|
||||
SpeechToTextVO result = deepgramService.transcribe(file);
|
||||
|
||||
// 成功后扣减次数(仅 vipLevel < 2 的用户)
|
||||
if (vipLevel < 2) {
|
||||
String redisKey = SPEECH_DAILY_LIMIT_PREFIX + userId;
|
||||
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,
|
||||
cfgHolder.getRef().get().getUserRegisterProperties().getVipFreeTrialTalk());
|
||||
}
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yolo.keyborad.model.dto.chat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2026/2/4
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "删除聊天记录请求")
|
||||
public class ChatHistoryDeleteReq {
|
||||
|
||||
@Schema(description = "聊天记录ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long id;
|
||||
}
|
||||
|
||||
@@ -79,4 +79,8 @@ public class KeyboardAiChatMessage {
|
||||
@TableField(value = "session_id")
|
||||
@Schema(description = "会话Id")
|
||||
private Long sessionId;
|
||||
|
||||
@TableField(value = "deleted")
|
||||
@Schema(description = "是否删除")
|
||||
private Boolean deleted;
|
||||
}
|
||||
@@ -146,4 +146,8 @@ public class KeyboardAiCompanion {
|
||||
@TableField(value = "prologue_audio")
|
||||
@Schema(description="开场白音频")
|
||||
private String prologueAudio;
|
||||
|
||||
@TableField(value = "voice_id")
|
||||
@Schema(description="角色音频Id")
|
||||
private String voiceId;
|
||||
}
|
||||
@@ -108,4 +108,8 @@ public class KeyboardProductItems {
|
||||
@TableField(value = "duration_days")
|
||||
@Schema(description="订阅时长的具体天数")
|
||||
private Integer durationDays;
|
||||
|
||||
@TableField(value = "level")
|
||||
@Schema(description = "级别")
|
||||
private Integer level;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yolo.keyborad.model.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -66,4 +67,9 @@ public class AiCompanionVO {
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@TableField(value = "voice_id")
|
||||
@Schema(description="角色音频Id")
|
||||
private String voiceId;
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ public class CommentVO {
|
||||
@Schema(description = "父评论ID")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "回复给的用户ID(仅回复评论有值)")
|
||||
private Long replyToUserId;
|
||||
|
||||
@Schema(description = "回复给的用户昵称(仅回复评论有值)")
|
||||
private String replyToUserName;
|
||||
|
||||
@Schema(description = "根评论ID")
|
||||
private Long rootId;
|
||||
|
||||
|
||||
@@ -44,5 +44,7 @@ public class KeyboardProductItemRespVO {
|
||||
@Schema(description = "描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "级别")
|
||||
private Integer level;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,4 +41,12 @@ public interface KeyboardAiChatMessageService extends IService<KeyboardAiChatMes
|
||||
* @return 聊过天的AI角色ID列表(按最近聊天时间倒序)
|
||||
*/
|
||||
List<Long> getChattedCompanionIds(Long userId);
|
||||
|
||||
/**
|
||||
* 根据聊天记录ID逻辑删除消息
|
||||
*
|
||||
* @param userId 当前用户ID
|
||||
* @param messageId 聊天记录ID
|
||||
*/
|
||||
void deleteMessageById(Long userId, Long messageId);
|
||||
}
|
||||
|
||||
@@ -422,6 +422,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
// 检查VIP是否已过期
|
||||
if (user.getVipExpiry() != null && user.getVipExpiry().toInstant().isBefore(Instant.now())) {
|
||||
user.setIsVip(false);
|
||||
user.setVipLevel(null);
|
||||
userService.updateById(user);
|
||||
log.info("User VIP expired: userId={}", userId);
|
||||
}
|
||||
@@ -464,6 +465,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
} else {
|
||||
// 倒扣后已过期,取消VIP
|
||||
user.setIsVip(false);
|
||||
user.setVipLevel(null);
|
||||
user.setVipExpiry(Date.from(newExpiry)); // 保留倒扣后的时间,而不是设为null
|
||||
log.info("Subscription refunded, VIP expired after deduction: userId={}, newExpiry={}",
|
||||
userId, newExpiry);
|
||||
@@ -607,6 +609,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
|
||||
// 4. 更新用户VIP状态
|
||||
user.setIsVip(true);
|
||||
user.setVipLevel(product.getLevel());
|
||||
user.setVipExpiry(Date.from(newExpiry));
|
||||
|
||||
// 5. 保存用户信息到数据库
|
||||
@@ -764,6 +767,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
|
||||
|
||||
// 6. 更新用户VIP状态
|
||||
user.setIsVip(true); // 设置为VIP用户
|
||||
user.setVipLevel(product.getLevel());
|
||||
user.setVipExpiry(Date.from(newExpiry)); // 更新VIP过期时间
|
||||
|
||||
// 7. 保存用户信息到数据库
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.yolo.keyborad.config.NacosAppConfigCenter;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardCharacter;
|
||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||
@@ -406,8 +407,16 @@ public class ChatServiceImpl implements ChatService {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 获取AI人设的系统提示词
|
||||
String systemPrompt = aiCompanionService.getSystemPromptById(companionId);
|
||||
// 获取AI角色信息(用于系统提示词 + voiceId)
|
||||
KeyboardAiCompanion companion = aiCompanionService.getById(companionId);
|
||||
if (companion == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "AI陪聊角色不存在");
|
||||
}
|
||||
if (companion.getStatus() == null || companion.getStatus() != 1) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "AI陪聊角色已下线");
|
||||
}
|
||||
String systemPrompt = companion.getSystemPrompt();
|
||||
String voiceId = companion.getVoiceId();
|
||||
|
||||
// 获取最近20条聊天记录作为上下文
|
||||
List<KeyboardAiChatMessage> historyMessages = aiChatMessageService.getRecentMessages(
|
||||
@@ -444,8 +453,8 @@ public class ChatServiceImpl implements ChatService {
|
||||
// 初始化音频任务状态为 processing
|
||||
setAudioTaskStatus(audioId, AudioTaskVO.STATUS_PROCESSING, null, null);
|
||||
|
||||
// 异步执行 TTS + R2 上传
|
||||
CompletableFuture.runAsync(() -> processAudioAsync(audioId, response, userId));
|
||||
// 异步执行 TTS + R2 上传(优先使用角色配置的 voiceId)
|
||||
CompletableFuture.runAsync(() -> processAudioAsync(audioId, response, userId, voiceId));
|
||||
|
||||
return ChatMessageVO.builder()
|
||||
.aiResponse(response)
|
||||
@@ -515,14 +524,14 @@ public class ChatServiceImpl implements ChatService {
|
||||
/**
|
||||
* 异步处理音频:TTS 转换 + 上传 R2
|
||||
*/
|
||||
private void processAudioAsync(String audioId, String text, String userId) {
|
||||
private void processAudioAsync(String audioId, String text, String userId, String voiceId) {
|
||||
try {
|
||||
log.info("开始异步音频处理, audioId: {}", audioId);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 1. TTS 转换
|
||||
long ttsStart = System.currentTimeMillis();
|
||||
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text);
|
||||
TextToSpeechVO ttsResult = elevenLabsService.textToSpeechWithTimestamps(text, voiceId);
|
||||
long ttsDuration = System.currentTimeMillis() - ttsStart;
|
||||
log.info("TTS 完成, audioId: {}, 耗时: {}ms", audioId, ttsDuration);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.yolo.keyborad.mapper.KeyboardAiChatMessageMapper;
|
||||
import com.yolo.keyborad.common.ErrorCode;
|
||||
import com.yolo.keyborad.exception.BusinessException;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
||||
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
||||
@@ -46,8 +48,9 @@ public class KeyboardAiChatMessageServiceImpl extends ServiceImpl<KeyboardAiChat
|
||||
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
||||
.eq(KeyboardAiChatMessage::getCompanionId, companionId)
|
||||
.eq(KeyboardAiChatMessage::getSessionId, activeSession.getId())
|
||||
.orderByAsc(KeyboardAiChatMessage::getCreatedAt)
|
||||
.orderByAsc(KeyboardAiChatMessage::getId);
|
||||
.eq(KeyboardAiChatMessage::getDeleted,false)
|
||||
.orderByDesc(KeyboardAiChatMessage::getCreatedAt)
|
||||
.orderByDesc(KeyboardAiChatMessage::getId);
|
||||
IPage<KeyboardAiChatMessage> entityPage = this.page(page, queryWrapper);
|
||||
return entityPage.convert(entity -> BeanUtil.copyProperties(entity, ChatMessageHistoryVO.class));
|
||||
}
|
||||
@@ -110,4 +113,23 @@ public class KeyboardAiChatMessageServiceImpl extends ServiceImpl<KeyboardAiChat
|
||||
.distinct()
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMessageById(Long userId, Long messageId) {
|
||||
KeyboardAiChatMessage message = this.getById(messageId);
|
||||
if (message == null || Boolean.TRUE.equals(message.getDeleted())) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "聊天记录不存在");
|
||||
}
|
||||
if (!userId.equals(message.getUserId())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
|
||||
KeyboardAiChatMessage update = new KeyboardAiChatMessage();
|
||||
update.setId(messageId);
|
||||
update.setDeleted(true);
|
||||
boolean success = this.updateById(update);
|
||||
if (!success) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.yolo.keyborad.service.UserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -74,24 +75,10 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
|
||||
// 转换为VO
|
||||
Map<Long, KeyboardUser> finalUserMap = userMap;
|
||||
Map<Long, KeyboardAiCompanionComment> commentById = entityPage.getRecords().stream()
|
||||
.collect(Collectors.toMap(KeyboardAiCompanionComment::getId, c -> c));
|
||||
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());
|
||||
|
||||
// 填充用户信息
|
||||
KeyboardUser user = finalUserMap.get(entity.getUserId());
|
||||
if (user != null) {
|
||||
vo.setUserName(user.getNickName());
|
||||
vo.setUserAvatar(user.getAvatarUrl());
|
||||
}
|
||||
return vo;
|
||||
return convertToVO(entity, finalUserMap, null, commentById);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,13 +100,14 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
// 批量查询回复(每条一级评论取前3条回复)
|
||||
Map<Long, List<KeyboardAiCompanionComment>> repliesMap = Map.of();
|
||||
Map<Long, Long> replyCountMap = Map.of();
|
||||
List<KeyboardAiCompanionComment> allReplies = List.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);
|
||||
allReplies = this.list(replyWrapper);
|
||||
|
||||
// 按rootId分组,每组取前3条
|
||||
repliesMap = allReplies.stream()
|
||||
@@ -137,6 +125,11 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
));
|
||||
}
|
||||
|
||||
// 用于“回复给谁”的映射:commentId -> comment
|
||||
Map<Long, KeyboardAiCompanionComment> commentById = new HashMap<>();
|
||||
entityPage.getRecords().forEach(c -> commentById.put(c.getId(), c));
|
||||
allReplies.forEach(c -> commentById.put(c.getId(), c));
|
||||
|
||||
// 收集所有需要查询的用户ID(一级评论 + 回复)
|
||||
List<Long> userIds = new ArrayList<>(entityPage.getRecords().stream()
|
||||
.map(KeyboardAiCompanionComment::getUserId)
|
||||
@@ -171,12 +164,12 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
Map<Long, Long> finalReplyCountMap = replyCountMap;
|
||||
|
||||
return entityPage.convert(entity -> {
|
||||
CommentVO vo = convertToVO(entity, finalUserMap, likedCommentIds);
|
||||
CommentVO vo = convertToVO(entity, finalUserMap, likedCommentIds, commentById);
|
||||
|
||||
// 填充回复列表
|
||||
List<KeyboardAiCompanionComment> replies = finalRepliesMap.getOrDefault(entity.getId(), List.of());
|
||||
List<CommentVO> replyVOs = replies.stream()
|
||||
.map(reply -> convertToVO(reply, finalUserMap, likedCommentIds))
|
||||
.map(reply -> convertToVO(reply, finalUserMap, likedCommentIds, commentById))
|
||||
.collect(Collectors.toList());
|
||||
vo.setReplies(replyVOs);
|
||||
vo.setReplyCount(finalReplyCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||
@@ -188,7 +181,12 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
/**
|
||||
* 将评论实体转换为VO
|
||||
*/
|
||||
private CommentVO convertToVO(KeyboardAiCompanionComment entity, Map<Long, KeyboardUser> userMap, Set<Long> likedCommentIds) {
|
||||
private CommentVO convertToVO(
|
||||
KeyboardAiCompanionComment entity,
|
||||
Map<Long, KeyboardUser> userMap,
|
||||
Set<Long> likedCommentIds,
|
||||
Map<Long, KeyboardAiCompanionComment> commentById
|
||||
) {
|
||||
CommentVO vo = new CommentVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setCompanionId(entity.getCompanionId());
|
||||
@@ -198,7 +196,7 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
vo.setContent(entity.getContent());
|
||||
vo.setLikeCount(entity.getLikeCount());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setLiked(likedCommentIds.contains(entity.getId()));
|
||||
vo.setLiked(likedCommentIds != null && likedCommentIds.contains(entity.getId()));
|
||||
|
||||
// 填充用户信息
|
||||
KeyboardUser user = userMap.get(entity.getUserId());
|
||||
@@ -206,6 +204,19 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
||||
vo.setUserName(user.getNickName());
|
||||
vo.setUserAvatar(user.getAvatarUrl());
|
||||
}
|
||||
|
||||
// 填充“回复给谁”(仅回复评论有值)
|
||||
if (entity.getParentId() != null && commentById != null) {
|
||||
KeyboardAiCompanionComment parent = commentById.get(entity.getParentId());
|
||||
if (parent != null) {
|
||||
vo.setReplyToUserId(parent.getUserId());
|
||||
KeyboardUser replyToUser = userMap.get(parent.getUserId());
|
||||
if (replyToUser != null) {
|
||||
vo.setReplyToUserName(replyToUser.getNickName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,6 @@ public class KeyboardAiCompanionReportServiceImpl extends ServiceImpl<KeyboardAi
|
||||
throw new BusinessException(ErrorCode.REPORT_TYPE_EMPTY);
|
||||
}
|
||||
|
||||
// 校验每个 reportType 在有效范围内(1,2,3,4,5,99)
|
||||
List<Short> validTypes = List.of((short) 1, (short) 2, (short) 3, (short) 4, (short) 5, (short) 99);
|
||||
for (Short type : req.getReportTypes()) {
|
||||
if (!validTypes.contains(type)) {
|
||||
throw new BusinessException(ErrorCode.REPORT_TYPE_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
// 校验 AI 角色是否存在
|
||||
KeyboardAiCompanion companion = aiCompanionService.getById(req.getCompanionId());
|
||||
if (companion == null) {
|
||||
|
||||
Reference in New Issue
Block a user