Compare commits
11 Commits
e68f1bea56
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 392d9ecfe8 | |||
| 6a773ee0ca | |||
| 7d23b6be0f | |||
| 408d4d4bc1 | |||
| ecab353802 | |||
| 234ea0c241 | |||
| e1aa1ce4e8 | |||
| c8d8046bf4 | |||
| 0e863288c8 | |||
| f28e6b7c39 | |||
| 22e5041447 |
@@ -2,6 +2,6 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"started_at": "2026-01-26T13:01:18.447Z",
|
"started_at": "2026-01-26T13:01:18.447Z",
|
||||||
"original_prompt": "刚刚回滚了代码,现在AI陪聊角色评论需要使用KeyboardAiCompanionCommentLikeService添加一个评论点赞接口,用来记录点赞和取消点赞。 ulw",
|
"original_prompt": "刚刚回滚了代码,现在AI陪聊角色评论需要使用KeyboardAiCompanionCommentLikeService添加一个评论点赞接口,用来记录点赞和取消点赞。 ulw",
|
||||||
"reinforcement_count": 8,
|
"reinforcement_count": 10,
|
||||||
"last_checked_at": "2026-01-27T10:35:42.226Z"
|
"last_checked_at": "2026-01-27T11:00:42.142Z"
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,10 @@ public enum ErrorCode {
|
|||||||
AUDIO_FILE_EMPTY(40016, "音频文件不能为空"),
|
AUDIO_FILE_EMPTY(40016, "音频文件不能为空"),
|
||||||
AUDIO_FILE_TOO_LARGE(40017, "音频文件过大"),
|
AUDIO_FILE_TOO_LARGE(40017, "音频文件过大"),
|
||||||
AUDIO_FORMAT_NOT_SUPPORTED(40018, "音频格式不支持"),
|
AUDIO_FORMAT_NOT_SUPPORTED(40018, "音频格式不支持"),
|
||||||
STT_SERVICE_ERROR(50031, "语音转文字服务异常");
|
STT_SERVICE_ERROR(50031, "语音转文字服务异常"),
|
||||||
|
REPORT_TYPE_INVALID(40020, "举报类型无效"),
|
||||||
|
REPORT_COMPANION_ID_EMPTY(40021, "被举报的AI角色ID不能为空"),
|
||||||
|
REPORT_TYPE_EMPTY(40022, "举报类型不能为空");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态码
|
* 状态码
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/ai-companion/page",
|
"/ai-companion/page",
|
||||||
"/chat/history",
|
"/chat/history",
|
||||||
"/ai-companion/comment/add",
|
"/ai-companion/comment/add",
|
||||||
"/speech/transcribe"
|
"/speech/transcribe",
|
||||||
|
"/ai-companion/comment/page",
|
||||||
|
"/ai-companion/liked"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import com.yolo.keyborad.common.ResultUtils;
|
|||||||
import com.yolo.keyborad.exception.BusinessException;
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
import com.yolo.keyborad.model.dto.PageDTO;
|
import com.yolo.keyborad.model.dto.PageDTO;
|
||||||
import com.yolo.keyborad.model.dto.companion.CompanionLikeReq;
|
import com.yolo.keyborad.model.dto.companion.CompanionLikeReq;
|
||||||
|
import com.yolo.keyborad.model.dto.companion.CompanionReportReq;
|
||||||
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
import com.yolo.keyborad.service.KeyboardAiCompanionLikeService;
|
import com.yolo.keyborad.service.KeyboardAiCompanionLikeService;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiCompanionReportService;
|
||||||
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
||||||
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;
|
||||||
@@ -17,6 +19,8 @@ import jakarta.annotation.Resource;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
* @date: 2026/1/26
|
* @date: 2026/1/26
|
||||||
@@ -33,10 +37,14 @@ public class AiCompanionController {
|
|||||||
@Resource
|
@Resource
|
||||||
private KeyboardAiCompanionLikeService aiCompanionLikeService;
|
private KeyboardAiCompanionLikeService aiCompanionLikeService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiCompanionReportService aiCompanionReportService;
|
||||||
|
|
||||||
@PostMapping("/page")
|
@PostMapping("/page")
|
||||||
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表")
|
@Operation(summary = "分页查询AI陪聊角色", description = "分页查询已上线的AI陪聊角色列表,包含点赞数、评论数和当前用户点赞状态")
|
||||||
public BaseResponse<IPage<AiCompanionVO>> pageList(@RequestBody PageDTO pageDTO) {
|
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);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,4 +59,39 @@ public class AiCompanionController {
|
|||||||
boolean result = aiCompanionLikeService.toggleLike(userId, req.getCompanionId());
|
boolean result = aiCompanionLikeService.toggleLike(userId, req.getCompanionId());
|
||||||
return ResultUtils.success(result);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/liked")
|
||||||
|
@Operation(summary = "获取当前用户点赞过的AI角色列表", description = "查询当前用户点赞过的所有AI角色,返回角色详细信息")
|
||||||
|
public BaseResponse<List<AiCompanionVO>> getLikedCompanions() {
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
List<AiCompanionVO> result = aiCompanionService.getLikedCompanions(userId);
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/chatted")
|
||||||
|
@Operation(summary = "获取当前用户聊过天的AI角色列表", description = "查询当前用户聊过天的所有AI角色,返回角色详细信息")
|
||||||
|
public BaseResponse<List<AiCompanionVO>> getChattedCompanions() {
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
List<AiCompanionVO> result = aiCompanionService.getChattedCompanions(userId);
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{companionId}")
|
||||||
|
@Operation(summary = "根据ID获取AI角色详情", description = "根据AI角色ID查询角色详细信息,包含点赞数、评论数和当前用户点赞状态")
|
||||||
|
public BaseResponse<AiCompanionVO> getCompanionById(@PathVariable Long companionId) {
|
||||||
|
if (companionId == null) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||||
|
}
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
AiCompanionVO result = aiCompanionService.getCompanionById(userId, companionId);
|
||||||
|
return ResultUtils.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/report")
|
||||||
|
@Operation(summary = "举报AI角色", description = "举报AI角色,支持多种举报类型(可多选):1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他")
|
||||||
|
public BaseResponse<Long> reportCompanion(@RequestBody CompanionReportReq req) {
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
Long reportId = aiCompanionReportService.reportCompanion(userId, req);
|
||||||
|
return ResultUtils.success(reportId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import com.yolo.keyborad.model.dto.chat.ChatMessageReq;
|
|||||||
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
import com.yolo.keyborad.model.dto.chat.ChatReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
import com.yolo.keyborad.model.dto.chat.ChatSaveReq;
|
||||||
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
import com.yolo.keyborad.model.dto.chat.ChatStreamMessage;
|
||||||
|
import com.yolo.keyborad.model.dto.chat.SessionResetReq;
|
||||||
import com.yolo.keyborad.model.vo.AudioTaskVO;
|
import com.yolo.keyborad.model.vo.AudioTaskVO;
|
||||||
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
||||||
import com.yolo.keyborad.model.vo.ChatMessageVO;
|
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.model.vo.ChatVoiceVO;
|
||||||
import com.yolo.keyborad.service.ChatService;
|
import com.yolo.keyborad.service.ChatService;
|
||||||
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiChatSessionService;
|
||||||
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
import com.yolo.keyborad.service.impl.QdrantVectorService;
|
||||||
import io.qdrant.client.grpc.JsonWithInt;
|
import io.qdrant.client.grpc.JsonWithInt;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -55,6 +58,9 @@ public class ChatController {
|
|||||||
@Resource
|
@Resource
|
||||||
private KeyboardAiChatMessageService aiChatMessageService;
|
private KeyboardAiChatMessageService aiChatMessageService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiChatSessionService aiChatSessionService;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/message")
|
@PostMapping("/message")
|
||||||
@Operation(summary = "同步对话", description = "发送消息给大模型,同步返回 AI 响应,异步生成音频")
|
@Operation(summary = "同步对话", description = "发送消息给大模型,同步返回 AI 响应,异步生成音频")
|
||||||
@@ -131,4 +137,24 @@ public class ChatController {
|
|||||||
userId, req.getCompanionId(), req.getPageNum(), req.getPageSize());
|
userId, req.getCompanionId(), req.getPageNum(), req.getPageSize());
|
||||||
return ResultUtils.success(result);
|
return ResultUtils.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/session/reset")
|
||||||
|
@Operation(summary = "重置会话", description = "重置与AI角色的聊天会话,将当前会话设为不活跃并创建新会话,后续聊天记录将绑定到新会话")
|
||||||
|
public BaseResponse<ChatSessionVO> resetSession(@RequestBody SessionResetReq req) {
|
||||||
|
if (req.getCompanionId() == null) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_ID_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long userId = StpUtil.getLoginIdAsLong();
|
||||||
|
var newSession = aiChatSessionService.resetSession(userId, req.getCompanionId());
|
||||||
|
|
||||||
|
ChatSessionVO vo = ChatSessionVO.builder()
|
||||||
|
.sessionId(newSession.getId())
|
||||||
|
.companionId(newSession.getCompanionId())
|
||||||
|
.resetVersion(newSession.getResetVersion())
|
||||||
|
.createdAt(newSession.getCreatedAt())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ResultUtils.success(vo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yolo.keyborad.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/28 16:20
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface KeyboardAiChatSessionMapper extends BaseMapper<KeyboardAiChatSession> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yolo.keyborad.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanionReport;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/29 16:17
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface KeyboardAiCompanionReportMapper extends BaseMapper<KeyboardAiCompanionReport> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.chat;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/28
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "会话重置请求")
|
||||||
|
public class SessionResetReq {
|
||||||
|
|
||||||
|
@Schema(description = "AI陪聊角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long companionId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.companion;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/29
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "AI角色举报请求")
|
||||||
|
public class CompanionReportReq {
|
||||||
|
|
||||||
|
@Schema(description = "AI角色ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long companionId;
|
||||||
|
|
||||||
|
@Schema(description = "举报类型列表:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,支持多选", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<Short> reportTypes;
|
||||||
|
|
||||||
|
@Schema(description = "详细描述")
|
||||||
|
private String reportDesc;
|
||||||
|
|
||||||
|
@Schema(description = "聊天上下文快照JSON")
|
||||||
|
private String chatContext;
|
||||||
|
|
||||||
|
@Schema(description = "图片证据URL")
|
||||||
|
private String evidenceImageUrl;
|
||||||
|
}
|
||||||
@@ -75,4 +75,8 @@ public class KeyboardAiChatMessage {
|
|||||||
@TableField(value = "created_at")
|
@TableField(value = "created_at")
|
||||||
@Schema(description="消息创建时间")
|
@Schema(description="消息创建时间")
|
||||||
private Date createdAt;
|
private Date createdAt;
|
||||||
|
|
||||||
|
@TableField(value = "session_id")
|
||||||
|
@Schema(description = "会话Id")
|
||||||
|
private Long sessionId;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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/28 16:20
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户与AI陪伴角色的聊天会话表,用于支持聊天重置与关系重启
|
||||||
|
*/
|
||||||
|
@Schema(description="用户与AI陪伴角色的聊天会话表,用于支持聊天重置与关系重启")
|
||||||
|
@Data
|
||||||
|
@TableName(value = "keyboard_ai_chat_session")
|
||||||
|
public class KeyboardAiChatSession {
|
||||||
|
/**
|
||||||
|
* 聊天会话唯一ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description="聊天会话唯一ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
@TableField(value = "user_id")
|
||||||
|
@Schema(description="用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 陪伴角色ID
|
||||||
|
*/
|
||||||
|
@TableField(value = "companion_id")
|
||||||
|
@Schema(description="陪伴角色ID")
|
||||||
|
private Long companionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话重置版本号,用于标识第几次重新开始陪伴关系
|
||||||
|
*/
|
||||||
|
@TableField(value = "reset_version")
|
||||||
|
@Schema(description="会话重置版本号,用于标识第几次重新开始陪伴关系")
|
||||||
|
private Integer resetVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为当前活跃会话(true=当前使用中)
|
||||||
|
*/
|
||||||
|
@TableField(value = "is_active")
|
||||||
|
@Schema(description="是否为当前活跃会话(true=当前使用中)")
|
||||||
|
private Boolean isActive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
@Schema(description="会话创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话结束时间(用户重置或系统关闭会话时记录)
|
||||||
|
*/
|
||||||
|
@TableField(value = "ended_at")
|
||||||
|
@Schema(description="会话结束时间(用户重置或系统关闭会话时记录)")
|
||||||
|
private Date endedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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/29 16:17
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI角色举报记录表
|
||||||
|
*/
|
||||||
|
@Schema(description="AI角色举报记录表")
|
||||||
|
@Data
|
||||||
|
@TableName(value = "keyboard_ai_companion_report")
|
||||||
|
public class KeyboardAiCompanionReport {
|
||||||
|
/**
|
||||||
|
* 举报记录唯一ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description="举报记录唯一ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被举报的AI角色ID(逻辑关联 keyboard_ai_companion.id,无物理外键)
|
||||||
|
*/
|
||||||
|
@TableField(value = "companion_id")
|
||||||
|
@Schema(description="被举报的AI角色ID(逻辑关联 keyboard_ai_companion.id,无物理外键)")
|
||||||
|
private Long companionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起举报的用户ID(逻辑关联用户表)
|
||||||
|
*/
|
||||||
|
@TableField(value = "user_id")
|
||||||
|
@Schema(description="发起举报的用户ID(逻辑关联用户表)")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,多选时逗号分隔
|
||||||
|
*/
|
||||||
|
@TableField(value = "report_type")
|
||||||
|
@Schema(description="举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他,多选时逗号分隔")
|
||||||
|
private String reportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户填写的详细举报描述
|
||||||
|
*/
|
||||||
|
@TableField(value = "report_desc")
|
||||||
|
@Schema(description="用户填写的详细举报描述")
|
||||||
|
private String reportDesc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 违规现场:举报时的聊天上下文快照(建议存JSON字符串),用于审核取证
|
||||||
|
*/
|
||||||
|
@TableField(value = "chat_context")
|
||||||
|
@Schema(description="违规现场:举报时的聊天上下文快照(建议存JSON字符串),用于审核取证")
|
||||||
|
private String chatContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片证据:用户上传的截图URL
|
||||||
|
*/
|
||||||
|
@TableField(value = "evidence_image_url")
|
||||||
|
@Schema(description="图片证据:用户上传的截图URL")
|
||||||
|
private String evidenceImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略
|
||||||
|
*/
|
||||||
|
@TableField(value = "\"status\"")
|
||||||
|
@Schema(description="处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略")
|
||||||
|
private Short status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员处理备注(记录处理理由或处罚措施)
|
||||||
|
*/
|
||||||
|
@TableField(value = "admin_remark")
|
||||||
|
@Schema(description="管理员处理备注(记录处理理由或处罚措施)")
|
||||||
|
private String adminRemark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 举报提交时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
@Schema(description="举报提交时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_at")
|
||||||
|
@Schema(description="最后更新时间")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -61,6 +61,9 @@ public class AiCompanionVO {
|
|||||||
@Schema(description = "评论总数")
|
@Schema(description = "评论总数")
|
||||||
private Integer commentCount;
|
private Integer commentCount;
|
||||||
|
|
||||||
|
@Schema(description = "当前用户是否已点赞")
|
||||||
|
private Boolean liked;
|
||||||
|
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间")
|
||||||
private Date createdAt;
|
private Date createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/main/java/com/yolo/keyborad/model/vo/ChatSessionVO.java
Normal file
29
src/main/java/com/yolo/keyborad/model/vo/ChatSessionVO.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.yolo.keyborad.model.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/28
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "会话信息VO")
|
||||||
|
public class ChatSessionVO {
|
||||||
|
|
||||||
|
@Schema(description = "会话ID")
|
||||||
|
private Long sessionId;
|
||||||
|
|
||||||
|
@Schema(description = "AI陪聊角色ID")
|
||||||
|
private Long companionId;
|
||||||
|
|
||||||
|
@Schema(description = "会话版本号")
|
||||||
|
private Integer resetVersion;
|
||||||
|
|
||||||
|
@Schema(description = "会话创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -45,4 +46,10 @@ public class CommentVO {
|
|||||||
|
|
||||||
@Schema(description = "评论创建时间")
|
@Schema(description = "评论创建时间")
|
||||||
private Date createdAt;
|
private Date createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "回复列表(仅一级评论有值,默认返回前3条)")
|
||||||
|
private List<CommentVO> replies;
|
||||||
|
|
||||||
|
@Schema(description = "回复总数")
|
||||||
|
private Integer replyCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,12 @@ public interface KeyboardAiChatMessageService extends IService<KeyboardAiChatMes
|
|||||||
* @return 聊天记录列表(时间正序)
|
* @return 聊天记录列表(时间正序)
|
||||||
*/
|
*/
|
||||||
List<KeyboardAiChatMessage> getRecentMessages(Long userId, Long companionId, int limit);
|
List<KeyboardAiChatMessage> getRecentMessages(Long userId, Long companionId, int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户聊过天的所有AI角色ID列表
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 聊过天的AI角色ID列表(按最近聊天时间倒序)
|
||||||
|
*/
|
||||||
|
List<Long> getChattedCompanionIds(Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.yolo.keyborad.service;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/28 16:20
|
||||||
|
*/
|
||||||
|
public interface KeyboardAiChatSessionService extends IService<KeyboardAiChatSession> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户与AI角色的活跃会话,如果不存在则创建新会话
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param companionId AI角色ID
|
||||||
|
* @return 活跃会话
|
||||||
|
*/
|
||||||
|
KeyboardAiChatSession getOrCreateActiveSession(Long userId, Long companionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户与AI角色的活跃会话ID,如果不存在则创建新会话
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param companionId AI角色ID
|
||||||
|
* @return 活跃会话ID
|
||||||
|
*/
|
||||||
|
Long getOrCreateActiveSessionId(Long userId, Long companionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置会话:将当前活跃会话设为不活跃,并创建新的活跃会话
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param companionId AI角色ID
|
||||||
|
* @return 新创建的活跃会话
|
||||||
|
*/
|
||||||
|
KeyboardAiChatSession resetSession(Long userId, Long companionId);
|
||||||
|
}
|
||||||
@@ -38,4 +38,12 @@ public interface KeyboardAiCompanionLikeService extends IService<KeyboardAiCompa
|
|||||||
* @return 已点赞的AI角色ID集合
|
* @return 已点赞的AI角色ID集合
|
||||||
*/
|
*/
|
||||||
Set<Long> getLikedCompanionIds(Long userId, List<Long> companionIds);
|
Set<Long> getLikedCompanionIds(Long userId, List<Long> companionIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户点赞过的所有AI角色ID列表
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 已点赞的AI角色ID列表
|
||||||
|
*/
|
||||||
|
List<Long> getAllLikedCompanionIds(Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.yolo.keyborad.service;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.dto.companion.CompanionReportReq;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanionReport;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/29 16:17
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface KeyboardAiCompanionReportService extends IService<KeyboardAiCompanionReport>{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 举报AI角色
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param req 举报请求
|
||||||
|
* @return 举报记录ID
|
||||||
|
*/
|
||||||
|
Long reportCompanion(Long userId, CompanionReportReq req);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
* @date: 2026/1/26 13:51
|
* @date: 2026/1/26 13:51
|
||||||
@@ -20,6 +22,16 @@ public interface KeyboardAiCompanionService extends IService<KeyboardAiCompanion
|
|||||||
*/
|
*/
|
||||||
IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize);
|
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获取系统提示词
|
* 根据AI人设ID获取系统提示词
|
||||||
*
|
*
|
||||||
@@ -27,4 +39,29 @@ public interface KeyboardAiCompanionService extends IService<KeyboardAiCompanion
|
|||||||
* @return 系统提示词
|
* @return 系统提示词
|
||||||
*/
|
*/
|
||||||
String getSystemPromptById(Long companionId);
|
String getSystemPromptById(Long companionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户点赞过的AI角色列表
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 点赞过的AI角色列表
|
||||||
|
*/
|
||||||
|
List<AiCompanionVO> getLikedCompanions(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户聊过天的AI角色列表
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 聊过天的AI角色列表
|
||||||
|
*/
|
||||||
|
List<AiCompanionVO> getChattedCompanions(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取AI角色详情(带点赞数、评论数和当前用户点赞状态)
|
||||||
|
*
|
||||||
|
* @param userId 当前用户ID
|
||||||
|
* @param companionId AI角色ID
|
||||||
|
* @return AI角色详情
|
||||||
|
*/
|
||||||
|
AiCompanionVO getCompanionById(Long userId, Long companionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
@Resource
|
@Resource
|
||||||
private KeyboardAiChatMessageService aiChatMessageService;
|
private KeyboardAiChatMessageService aiChatMessageService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiChatSessionService aiChatSessionService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ElevenLabsService elevenLabsService;
|
private ElevenLabsService elevenLabsService;
|
||||||
|
|
||||||
@@ -376,41 +379,29 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
public ChatMessageVO message(String content, String userId, Long companionId) {
|
public ChatMessageVO message(String content, String userId, Long companionId) {
|
||||||
log.info("同步对话请求, userId: {}, companionId: {}, content: {}", userId, companionId, content);
|
log.info("同步对话请求, userId: {}, companionId: {}, content: {}", userId, companionId, content);
|
||||||
|
|
||||||
// ============ VIP等级检查 ============
|
// ============ VIP等级检查(先检查,不增加次数) ============
|
||||||
AppConfig appConfig = cfgHolder.getRef().get();
|
AppConfig appConfig = cfgHolder.getRef().get();
|
||||||
KeyboardUser user = userService.getById(Long.parseLong(userId));
|
KeyboardUser user = userService.getById(Long.parseLong(userId));
|
||||||
|
|
||||||
// 获取VIP等级(null视为0)
|
// 获取VIP等级(null视为0)
|
||||||
int vipLevel = user != null && user.getVipLevel() != null ? user.getVipLevel() : 0;
|
int vipLevel = user != null && user.getVipLevel() != null ? user.getVipLevel() : 0;
|
||||||
|
|
||||||
// 如果VIP等级 <= 1,需要限制每日体验次数
|
// 记录是否需要扣减体验次数(VIP等级 <= 1 的用户需要扣减)
|
||||||
if (vipLevel <= 1) {
|
boolean needDeductQuota = vipLevel <= 1;
|
||||||
Integer dailyLimit = appConfig.getUserRegisterProperties().getVipFreeTrialTalk();
|
String redisKey = CHAT_DAILY_LIMIT_PREFIX + userId;
|
||||||
String redisKey = CHAT_DAILY_LIMIT_PREFIX + userId;
|
Integer dailyLimit = appConfig.getUserRegisterProperties().getVipFreeTrialTalk();
|
||||||
|
|
||||||
|
// 如果VIP等级 <= 1,先检查每日体验次数是否用完
|
||||||
|
if (needDeductQuota) {
|
||||||
// 获取当前使用次数
|
// 获取当前使用次数
|
||||||
String countStr = stringRedisTemplate.opsForValue().get(redisKey);
|
String countStr = stringRedisTemplate.opsForValue().get(redisKey);
|
||||||
int currentCount = countStr != null ? Integer.parseInt(countStr) : 0;
|
int currentCount = countStr != null ? Integer.parseInt(countStr) : 0;
|
||||||
|
|
||||||
// 检查是否超出限制
|
// 检查是否超出限制,超出直接返回
|
||||||
if (currentCount >= dailyLimit) {
|
if (currentCount >= dailyLimit) {
|
||||||
log.warn("用户 {} VIP等级 {} 已达到每日体验次数限制 {}", userId, vipLevel, dailyLimit);
|
log.warn("用户 {} VIP等级 {} 已达到每日体验次数限制 {}", userId, vipLevel, dailyLimit);
|
||||||
throw new BusinessException(ErrorCode.VIP_TRIAL_LIMIT_REACHED);
|
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();
|
long startTime = System.currentTimeMillis();
|
||||||
@@ -431,6 +422,22 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
// 保存用户消息和AI响应到聊天记录
|
// 保存用户消息和AI响应到聊天记录
|
||||||
saveChatMessages(Long.parseLong(userId), companionId, content, response);
|
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
|
// 生成音频任务 ID
|
||||||
String audioId = UUID.randomUUID().toString().replace("-", "");
|
String audioId = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
||||||
@@ -453,10 +460,14 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
private void saveChatMessages(Long userId, Long companionId, String userContent, String aiResponse) {
|
private void saveChatMessages(Long userId, Long companionId, String userContent, String aiResponse) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
|
|
||||||
|
// 获取或创建活跃会话
|
||||||
|
Long sessionId = aiChatSessionService.getOrCreateActiveSessionId(userId, companionId);
|
||||||
|
|
||||||
// 保存用户消息 (sender=1)
|
// 保存用户消息 (sender=1)
|
||||||
KeyboardAiChatMessage userMessage = new KeyboardAiChatMessage();
|
KeyboardAiChatMessage userMessage = new KeyboardAiChatMessage();
|
||||||
userMessage.setUserId(userId);
|
userMessage.setUserId(userId);
|
||||||
userMessage.setCompanionId(companionId);
|
userMessage.setCompanionId(companionId);
|
||||||
|
userMessage.setSessionId(sessionId);
|
||||||
userMessage.setSender((short) 1);
|
userMessage.setSender((short) 1);
|
||||||
userMessage.setContent(userContent);
|
userMessage.setContent(userContent);
|
||||||
userMessage.setCreatedAt(now);
|
userMessage.setCreatedAt(now);
|
||||||
@@ -465,13 +476,14 @@ public class ChatServiceImpl implements ChatService {
|
|||||||
KeyboardAiChatMessage aiMessage = new KeyboardAiChatMessage();
|
KeyboardAiChatMessage aiMessage = new KeyboardAiChatMessage();
|
||||||
aiMessage.setUserId(userId);
|
aiMessage.setUserId(userId);
|
||||||
aiMessage.setCompanionId(companionId);
|
aiMessage.setCompanionId(companionId);
|
||||||
|
aiMessage.setSessionId(sessionId);
|
||||||
aiMessage.setSender((short) 2);
|
aiMessage.setSender((short) 2);
|
||||||
aiMessage.setContent(aiResponse);
|
aiMessage.setContent(aiResponse);
|
||||||
aiMessage.setCreatedAt(now);
|
aiMessage.setCreatedAt(now);
|
||||||
|
|
||||||
// 批量保存
|
// 批量保存
|
||||||
aiChatMessageService.saveBatch(List.of(userMessage, aiMessage));
|
aiChatMessageService.saveBatch(List.of(userMessage, aiMessage));
|
||||||
log.info("聊天记录保存成功, userId: {}, companionId: {}", userId, companionId);
|
log.info("聊天记录保存成功, userId: {}, companionId: {}, sessionId: {}", userId, companionId, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.yolo.keyborad.mapper.KeyboardAiChatMessageMapper;
|
import com.yolo.keyborad.mapper.KeyboardAiChatMessageMapper;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
import com.yolo.keyborad.model.entity.KeyboardAiChatMessage;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||||
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
import com.yolo.keyborad.model.vo.ChatMessageHistoryVO;
|
||||||
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiChatSessionService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -21,22 +24,52 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class KeyboardAiChatMessageServiceImpl extends ServiceImpl<KeyboardAiChatMessageMapper, KeyboardAiChatMessage> implements KeyboardAiChatMessageService {
|
public class KeyboardAiChatMessageServiceImpl extends ServiceImpl<KeyboardAiChatMessageMapper, KeyboardAiChatMessage> implements KeyboardAiChatMessageService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiChatSessionService sessionService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<ChatMessageHistoryVO> pageHistory(Long userId, Long companionId, Integer pageNum, Integer pageSize) {
|
public IPage<ChatMessageHistoryVO> pageHistory(Long userId, Long companionId, Integer pageNum, Integer pageSize) {
|
||||||
|
// 获取当前活跃会话
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> sessionWrapper = new LambdaQueryWrapper<>();
|
||||||
|
sessionWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiChatSession::getIsActive, true);
|
||||||
|
KeyboardAiChatSession activeSession = sessionService.getOne(sessionWrapper);
|
||||||
|
|
||||||
|
// 如果没有活跃会话,返回空分页
|
||||||
|
if (activeSession == null) {
|
||||||
|
return new Page<ChatMessageHistoryVO>(pageNum, pageSize).setRecords(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
Page<KeyboardAiChatMessage> page = new Page<>(pageNum, pageSize);
|
Page<KeyboardAiChatMessage> page = new Page<>(pageNum, pageSize);
|
||||||
LambdaQueryWrapper<KeyboardAiChatMessage> queryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<KeyboardAiChatMessage> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
||||||
.eq(KeyboardAiChatMessage::getCompanionId, companionId)
|
.eq(KeyboardAiChatMessage::getCompanionId, companionId)
|
||||||
.orderByDesc(KeyboardAiChatMessage::getCreatedAt);
|
.eq(KeyboardAiChatMessage::getSessionId, activeSession.getId())
|
||||||
|
.orderByAsc(KeyboardAiChatMessage::getCreatedAt)
|
||||||
|
.orderByAsc(KeyboardAiChatMessage::getId);
|
||||||
IPage<KeyboardAiChatMessage> entityPage = this.page(page, queryWrapper);
|
IPage<KeyboardAiChatMessage> entityPage = this.page(page, queryWrapper);
|
||||||
return entityPage.convert(entity -> BeanUtil.copyProperties(entity, ChatMessageHistoryVO.class));
|
return entityPage.convert(entity -> BeanUtil.copyProperties(entity, ChatMessageHistoryVO.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<KeyboardAiChatMessage> getRecentMessages(Long userId, Long companionId, int limit) {
|
public List<KeyboardAiChatMessage> getRecentMessages(Long userId, Long companionId, int limit) {
|
||||||
|
// 获取当前活跃会话
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> sessionWrapper = new LambdaQueryWrapper<>();
|
||||||
|
sessionWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiChatSession::getIsActive, true);
|
||||||
|
KeyboardAiChatSession activeSession = sessionService.getOne(sessionWrapper);
|
||||||
|
|
||||||
|
// 如果没有活跃会话,返回空列表
|
||||||
|
if (activeSession == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<KeyboardAiChatMessage> queryWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<KeyboardAiChatMessage> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
||||||
.eq(KeyboardAiChatMessage::getCompanionId, companionId)
|
.eq(KeyboardAiChatMessage::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiChatMessage::getSessionId, activeSession.getId())
|
||||||
.orderByDesc(KeyboardAiChatMessage::getCreatedAt)
|
.orderByDesc(KeyboardAiChatMessage::getCreatedAt)
|
||||||
.last("LIMIT " + limit);
|
.last("LIMIT " + limit);
|
||||||
List<KeyboardAiChatMessage> messages = this.list(queryWrapper);
|
List<KeyboardAiChatMessage> messages = this.list(queryWrapper);
|
||||||
@@ -44,4 +77,37 @@ public class KeyboardAiChatMessageServiceImpl extends ServiceImpl<KeyboardAiChat
|
|||||||
Collections.reverse(messages);
|
Collections.reverse(messages);
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getChattedCompanionIds(Long userId) {
|
||||||
|
// 1. 查询用户所有活跃会话
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> sessionWrapper = new LambdaQueryWrapper<>();
|
||||||
|
sessionWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getIsActive, true);
|
||||||
|
List<KeyboardAiChatSession> activeSessions = sessionService.list(sessionWrapper);
|
||||||
|
|
||||||
|
// 2. 如果没有活跃会话,返回空列表
|
||||||
|
if (activeSessions == null || activeSessions.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提取活跃会话的 sessionId 列表
|
||||||
|
List<Long> activeSessionIds = activeSessions.stream()
|
||||||
|
.map(KeyboardAiChatSession::getId)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
// 4. 查询这些会话中的消息,获取 companionId,按最近聊天时间倒序
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatMessage> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(KeyboardAiChatMessage::getUserId, userId)
|
||||||
|
.in(KeyboardAiChatMessage::getSessionId, activeSessionIds)
|
||||||
|
.orderByDesc(KeyboardAiChatMessage::getCreatedAt);
|
||||||
|
|
||||||
|
List<KeyboardAiChatMessage> messages = this.list(queryWrapper);
|
||||||
|
|
||||||
|
// 5. 去重并保持顺序(按最近聊天时间)
|
||||||
|
return messages.stream()
|
||||||
|
.map(KeyboardAiChatMessage::getCompanionId)
|
||||||
|
.distinct()
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.yolo.keyborad.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.yolo.keyborad.mapper.KeyboardAiChatSessionMapper;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiChatSession;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiChatSessionService;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/28 16:20
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class KeyboardAiChatSessionServiceImpl extends ServiceImpl<KeyboardAiChatSessionMapper, KeyboardAiChatSession> implements KeyboardAiChatSessionService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyboardAiChatSession getOrCreateActiveSession(Long userId, Long companionId) {
|
||||||
|
// 查询当前活跃会话
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiChatSession::getIsActive, true);
|
||||||
|
KeyboardAiChatSession activeSession = this.getOne(queryWrapper);
|
||||||
|
|
||||||
|
if (activeSession != null) {
|
||||||
|
return activeSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不存在活跃会话,创建新会话
|
||||||
|
// 先查询该用户与该角色的最大版本号
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> maxVersionWrapper = new LambdaQueryWrapper<>();
|
||||||
|
maxVersionWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.orderByDesc(KeyboardAiChatSession::getResetVersion)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
KeyboardAiChatSession lastSession = this.getOne(maxVersionWrapper);
|
||||||
|
int newVersion = lastSession != null ? lastSession.getResetVersion() + 1 : 1;
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
KeyboardAiChatSession newSession = new KeyboardAiChatSession();
|
||||||
|
newSession.setUserId(userId);
|
||||||
|
newSession.setCompanionId(companionId);
|
||||||
|
newSession.setResetVersion(newVersion);
|
||||||
|
newSession.setIsActive(true);
|
||||||
|
newSession.setCreatedAt(new Date());
|
||||||
|
this.save(newSession);
|
||||||
|
|
||||||
|
return newSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getOrCreateActiveSessionId(Long userId, Long companionId) {
|
||||||
|
return getOrCreateActiveSession(userId, companionId).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyboardAiChatSession resetSession(Long userId, Long companionId) {
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// 查询当前活跃会话
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiChatSession::getIsActive, true);
|
||||||
|
KeyboardAiChatSession activeSession = this.getOne(queryWrapper);
|
||||||
|
|
||||||
|
// 如果存在活跃会话,将其设为不活跃
|
||||||
|
if (activeSession != null) {
|
||||||
|
activeSession.setIsActive(false);
|
||||||
|
activeSession.setEndedAt(now);
|
||||||
|
this.updateById(activeSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该用户与该角色的最大版本号
|
||||||
|
LambdaQueryWrapper<KeyboardAiChatSession> maxVersionWrapper = new LambdaQueryWrapper<>();
|
||||||
|
maxVersionWrapper.eq(KeyboardAiChatSession::getUserId, userId)
|
||||||
|
.eq(KeyboardAiChatSession::getCompanionId, companionId)
|
||||||
|
.orderByDesc(KeyboardAiChatSession::getResetVersion)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
KeyboardAiChatSession lastSession = this.getOne(maxVersionWrapper);
|
||||||
|
int newVersion = lastSession != null ? lastSession.getResetVersion() + 1 : 1;
|
||||||
|
|
||||||
|
// 创建新的活跃会话
|
||||||
|
KeyboardAiChatSession newSession = new KeyboardAiChatSession();
|
||||||
|
newSession.setUserId(userId);
|
||||||
|
newSession.setCompanionId(companionId);
|
||||||
|
newSession.setResetVersion(newVersion);
|
||||||
|
newSession.setIsActive(true);
|
||||||
|
newSession.setCreatedAt(now);
|
||||||
|
this.save(newSession);
|
||||||
|
|
||||||
|
return newSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import com.yolo.keyborad.service.UserService;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -104,12 +105,51 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
|||||||
.orderByDesc(KeyboardAiCompanionComment::getCreatedAt);
|
.orderByDesc(KeyboardAiCompanionComment::getCreatedAt);
|
||||||
IPage<KeyboardAiCompanionComment> entityPage = this.page(page, queryWrapper);
|
IPage<KeyboardAiCompanionComment> entityPage = this.page(page, queryWrapper);
|
||||||
|
|
||||||
// 获取所有用户ID
|
// 获取所有一级评论ID
|
||||||
List<Long> userIds = entityPage.getRecords().stream()
|
List<Long> topCommentIds = entityPage.getRecords().stream()
|
||||||
.map(KeyboardAiCompanionComment::getUserId)
|
.map(KeyboardAiCompanionComment::getId)
|
||||||
.distinct()
|
|
||||||
.collect(Collectors.toList());
|
.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();
|
Map<Long, KeyboardUser> userMap = Map.of();
|
||||||
if (!userIds.isEmpty()) {
|
if (!userIds.isEmpty()) {
|
||||||
@@ -117,33 +157,55 @@ public class KeyboardAiCompanionCommentServiceImpl extends ServiceImpl<KeyboardA
|
|||||||
userMap = users.stream().collect(Collectors.toMap(KeyboardUser::getId, u -> u));
|
userMap = users.stream().collect(Collectors.toMap(KeyboardUser::getId, u -> u));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前用户已点赞的评论ID
|
// 收集所有评论ID用于查询点赞状态(一级评论 + 回复)
|
||||||
List<Long> commentIds = entityPage.getRecords().stream()
|
List<Long> allCommentIds = new ArrayList<>(topCommentIds);
|
||||||
|
repliesMap.values().stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
.map(KeyboardAiCompanionComment::getId)
|
.map(KeyboardAiCompanionComment::getId)
|
||||||
.collect(Collectors.toList());
|
.forEach(allCommentIds::add);
|
||||||
Set<Long> likedCommentIds = commentLikeService.getLikedCommentIds(userId, commentIds);
|
Set<Long> likedCommentIds = commentLikeService.getLikedCommentIds(userId, allCommentIds);
|
||||||
|
|
||||||
// 转换为VO
|
// 转换为VO
|
||||||
Map<Long, KeyboardUser> finalUserMap = userMap;
|
Map<Long, KeyboardUser> finalUserMap = userMap;
|
||||||
return entityPage.convert(entity -> {
|
Map<Long, List<KeyboardAiCompanionComment>> finalRepliesMap = repliesMap;
|
||||||
CommentVO vo = new CommentVO();
|
Map<Long, Long> finalReplyCountMap = replyCountMap;
|
||||||
vo.setId(entity.getId());
|
|
||||||
vo.setCompanionId(entity.getCompanionId());
|
return entityPage.convert(entity -> {
|
||||||
vo.setUserId(entity.getUserId());
|
CommentVO vo = convertToVO(entity, finalUserMap, likedCommentIds);
|
||||||
vo.setParentId(entity.getParentId());
|
|
||||||
vo.setRootId(entity.getRootId());
|
// 填充回复列表
|
||||||
vo.setContent(entity.getContent());
|
List<KeyboardAiCompanionComment> replies = finalRepliesMap.getOrDefault(entity.getId(), List.of());
|
||||||
vo.setLikeCount(entity.getLikeCount());
|
List<CommentVO> replyVOs = replies.stream()
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
.map(reply -> convertToVO(reply, finalUserMap, likedCommentIds))
|
||||||
vo.setLiked(likedCommentIds.contains(entity.getId()));
|
.collect(Collectors.toList());
|
||||||
|
vo.setReplies(replyVOs);
|
||||||
|
vo.setReplyCount(finalReplyCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||||
|
|
||||||
// 填充用户信息
|
|
||||||
KeyboardUser user = finalUserMap.get(entity.getUserId());
|
|
||||||
if (user != null) {
|
|
||||||
vo.setUserName(user.getNickName());
|
|
||||||
vo.setUserAvatar(user.getAvatarUrl());
|
|
||||||
}
|
|
||||||
return vo;
|
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());
|
||||||
|
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 = userMap.get(entity.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
vo.setUserName(user.getNickName());
|
||||||
|
vo.setUserAvatar(user.getAvatarUrl());
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,4 +100,16 @@ public class KeyboardAiCompanionLikeServiceImpl extends ServiceImpl<KeyboardAiCo
|
|||||||
|
|
||||||
return isLiked;
|
return isLiked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> getAllLikedCompanionIds(Long userId) {
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionLike> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(KeyboardAiCompanionLike::getUserId, userId)
|
||||||
|
.eq(KeyboardAiCompanionLike::getStatus, (short) 1)
|
||||||
|
.select(KeyboardAiCompanionLike::getCompanionId);
|
||||||
|
List<KeyboardAiCompanionLike> likes = this.list(queryWrapper);
|
||||||
|
return likes.stream()
|
||||||
|
.map(KeyboardAiCompanionLike::getCompanionId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.yolo.keyborad.service.impl;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.common.ErrorCode;
|
||||||
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
|
import com.yolo.keyborad.model.dto.companion.CompanionReportReq;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanion;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.yolo.keyborad.mapper.KeyboardAiCompanionReportMapper;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCompanionReport;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiCompanionReportService;
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/1/29 16:17
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class KeyboardAiCompanionReportServiceImpl extends ServiceImpl<KeyboardAiCompanionReportMapper, KeyboardAiCompanionReport> implements KeyboardAiCompanionReportService{
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiCompanionService aiCompanionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long reportCompanion(Long userId, CompanionReportReq req) {
|
||||||
|
// 校验 companionId 不为空
|
||||||
|
if (req.getCompanionId() == null) {
|
||||||
|
throw new BusinessException(ErrorCode.REPORT_COMPANION_ID_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 reportTypes 不为空
|
||||||
|
if (req.getReportTypes() == null || req.getReportTypes().isEmpty()) {
|
||||||
|
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) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建举报记录
|
||||||
|
KeyboardAiCompanionReport report = new KeyboardAiCompanionReport();
|
||||||
|
report.setUserId(userId);
|
||||||
|
report.setCompanionId(req.getCompanionId());
|
||||||
|
|
||||||
|
// 将 List<Short> 转换为逗号分隔的字符串
|
||||||
|
String reportTypeStr = req.getReportTypes().stream()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
report.setReportType(reportTypeStr);
|
||||||
|
|
||||||
|
report.setReportDesc(req.getReportDesc());
|
||||||
|
report.setChatContext(req.getChatContext());
|
||||||
|
report.setEvidenceImageUrl(req.getEvidenceImageUrl());
|
||||||
|
report.setStatus((short) 0); // 待处理
|
||||||
|
report.setCreatedAt(new Date());
|
||||||
|
|
||||||
|
// 保存并返回 ID
|
||||||
|
this.save(report);
|
||||||
|
return report.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import com.yolo.keyborad.model.entity.KeyboardAiCompanionLike;
|
|||||||
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
import com.yolo.keyborad.model.vo.AiCompanionVO;
|
||||||
import com.yolo.keyborad.service.KeyboardAiCompanionCommentService;
|
import com.yolo.keyborad.service.KeyboardAiCompanionCommentService;
|
||||||
import com.yolo.keyborad.service.KeyboardAiCompanionLikeService;
|
import com.yolo.keyborad.service.KeyboardAiCompanionLikeService;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiChatMessageService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
@@ -20,6 +21,7 @@ import com.yolo.keyborad.service.KeyboardAiCompanionService;
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -35,6 +37,9 @@ public class KeyboardAiCompanionServiceImpl extends ServiceImpl<KeyboardAiCompan
|
|||||||
@Resource
|
@Resource
|
||||||
private KeyboardAiCompanionCommentService companionCommentService;
|
private KeyboardAiCompanionCommentService companionCommentService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private KeyboardAiChatMessageService chatMessageService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize) {
|
public IPage<AiCompanionVO> pageList(Integer pageNum, Integer pageSize) {
|
||||||
Page<KeyboardAiCompanion> page = new Page<>(pageNum, pageSize);
|
Page<KeyboardAiCompanion> page = new Page<>(pageNum, pageSize);
|
||||||
@@ -83,6 +88,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
|
@Override
|
||||||
public String getSystemPromptById(Long companionId) {
|
public String getSystemPromptById(Long companionId) {
|
||||||
KeyboardAiCompanion companion = this.getById(companionId);
|
KeyboardAiCompanion companion = this.getById(companionId);
|
||||||
@@ -94,4 +151,150 @@ public class KeyboardAiCompanionServiceImpl extends ServiceImpl<KeyboardAiCompan
|
|||||||
}
|
}
|
||||||
return companion.getSystemPrompt();
|
return companion.getSystemPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AiCompanionVO> getLikedCompanions(Long userId) {
|
||||||
|
// 获取用户点赞过的所有AI角色ID
|
||||||
|
List<Long> likedCompanionIds = companionLikeService.getAllLikedCompanionIds(userId);
|
||||||
|
if (likedCompanionIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询这些AI角色的详细信息(只返回已上线且可见的)
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanion> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.in(KeyboardAiCompanion::getId, likedCompanionIds)
|
||||||
|
.eq(KeyboardAiCompanion::getStatus, 1)
|
||||||
|
.eq(KeyboardAiCompanion::getVisibility, 1)
|
||||||
|
.orderByDesc(KeyboardAiCompanion::getSortOrder)
|
||||||
|
.orderByDesc(KeyboardAiCompanion::getPopularityScore);
|
||||||
|
List<KeyboardAiCompanion> companions = this.list(queryWrapper);
|
||||||
|
|
||||||
|
if (companions.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际查询到的角色ID
|
||||||
|
List<Long> companionIds = companions.stream()
|
||||||
|
.map(KeyboardAiCompanion::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 批量统计点赞数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionLike> likeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
likeWrapper.in(KeyboardAiCompanionLike::getCompanionId, companionIds)
|
||||||
|
.eq(KeyboardAiCompanionLike::getStatus, (short) 1);
|
||||||
|
List<KeyboardAiCompanionLike> likes = companionLikeService.list(likeWrapper);
|
||||||
|
Map<Long, Long> likeCountMap = likes.stream()
|
||||||
|
.collect(Collectors.groupingBy(KeyboardAiCompanionLike::getCompanionId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 批量统计评论数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionComment> commentWrapper = new LambdaQueryWrapper<>();
|
||||||
|
commentWrapper.in(KeyboardAiCompanionComment::getCompanionId, companionIds)
|
||||||
|
.eq(KeyboardAiCompanionComment::getStatus, (short) 1);
|
||||||
|
List<KeyboardAiCompanionComment> comments = companionCommentService.list(commentWrapper);
|
||||||
|
Map<Long, Long> commentCountMap = comments.stream()
|
||||||
|
.collect(Collectors.groupingBy(KeyboardAiCompanionComment::getCompanionId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 转换为VO并填充统计数据
|
||||||
|
return companions.stream().map(entity -> {
|
||||||
|
AiCompanionVO vo = BeanUtil.copyProperties(entity, AiCompanionVO.class);
|
||||||
|
vo.setLikeCount(likeCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||||
|
vo.setCommentCount(commentCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||||
|
vo.setLiked(true); // 用户已点赞
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AiCompanionVO> getChattedCompanions(Long userId) {
|
||||||
|
// 获取用户聊过天的所有AI角色ID
|
||||||
|
List<Long> chattedCompanionIds = chatMessageService.getChattedCompanionIds(userId);
|
||||||
|
if (chattedCompanionIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询这些AI角色的详细信息(只返回已上线且可见的)
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanion> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.in(KeyboardAiCompanion::getId, chattedCompanionIds)
|
||||||
|
.eq(KeyboardAiCompanion::getStatus, 1)
|
||||||
|
.eq(KeyboardAiCompanion::getVisibility, 1);
|
||||||
|
List<KeyboardAiCompanion> companions = this.list(queryWrapper);
|
||||||
|
|
||||||
|
if (companions.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际查询到的角色ID
|
||||||
|
List<Long> companionIds = companions.stream()
|
||||||
|
.map(KeyboardAiCompanion::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 批量统计点赞数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionLike> likeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
likeWrapper.in(KeyboardAiCompanionLike::getCompanionId, companionIds)
|
||||||
|
.eq(KeyboardAiCompanionLike::getStatus, (short) 1);
|
||||||
|
List<KeyboardAiCompanionLike> likes = companionLikeService.list(likeWrapper);
|
||||||
|
Map<Long, Long> likeCountMap = likes.stream()
|
||||||
|
.collect(Collectors.groupingBy(KeyboardAiCompanionLike::getCompanionId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 批量统计评论数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionComment> commentWrapper = new LambdaQueryWrapper<>();
|
||||||
|
commentWrapper.in(KeyboardAiCompanionComment::getCompanionId, companionIds)
|
||||||
|
.eq(KeyboardAiCompanionComment::getStatus, (short) 1);
|
||||||
|
List<KeyboardAiCompanionComment> comments = companionCommentService.list(commentWrapper);
|
||||||
|
Map<Long, Long> commentCountMap = comments.stream()
|
||||||
|
.collect(Collectors.groupingBy(KeyboardAiCompanionComment::getCompanionId, Collectors.counting()));
|
||||||
|
|
||||||
|
// 获取当前用户已点赞的角色ID
|
||||||
|
Set<Long> likedCompanionIds = companionLikeService.getLikedCompanionIds(userId, companionIds);
|
||||||
|
|
||||||
|
// 转换为VO并填充统计数据,保持原有顺序(按最近聊天时间)
|
||||||
|
Map<Long, KeyboardAiCompanion> companionMap = companions.stream()
|
||||||
|
.collect(Collectors.toMap(KeyboardAiCompanion::getId, c -> c));
|
||||||
|
|
||||||
|
return chattedCompanionIds.stream()
|
||||||
|
.filter(companionMap::containsKey)
|
||||||
|
.map(id -> {
|
||||||
|
KeyboardAiCompanion entity = companionMap.get(id);
|
||||||
|
AiCompanionVO vo = BeanUtil.copyProperties(entity, AiCompanionVO.class);
|
||||||
|
vo.setLikeCount(likeCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||||
|
vo.setCommentCount(commentCountMap.getOrDefault(entity.getId(), 0L).intValue());
|
||||||
|
vo.setLiked(likedCompanionIds.contains(entity.getId()));
|
||||||
|
return vo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AiCompanionVO getCompanionById(Long userId, Long companionId) {
|
||||||
|
// 查询AI角色
|
||||||
|
KeyboardAiCompanion companion = this.getById(companionId);
|
||||||
|
if (companion == null) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (companion.getStatus() != 1 || companion.getVisibility() != 1) {
|
||||||
|
throw new BusinessException(ErrorCode.COMPANION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计点赞数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionLike> likeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
likeWrapper.eq(KeyboardAiCompanionLike::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiCompanionLike::getStatus, (short) 1);
|
||||||
|
long likeCount = companionLikeService.count(likeWrapper);
|
||||||
|
|
||||||
|
// 统计评论数
|
||||||
|
LambdaQueryWrapper<KeyboardAiCompanionComment> commentWrapper = new LambdaQueryWrapper<>();
|
||||||
|
commentWrapper.eq(KeyboardAiCompanionComment::getCompanionId, companionId)
|
||||||
|
.eq(KeyboardAiCompanionComment::getStatus, (short) 1);
|
||||||
|
long commentCount = companionCommentService.count(commentWrapper);
|
||||||
|
|
||||||
|
// 获取当前用户点赞状态
|
||||||
|
boolean liked = companionLikeService.hasLiked(userId, companionId);
|
||||||
|
|
||||||
|
// 转换为VO
|
||||||
|
AiCompanionVO vo = BeanUtil.copyProperties(companion, AiCompanionVO.class);
|
||||||
|
vo.setLikeCount((int) likeCount);
|
||||||
|
vo.setCommentCount((int) commentCount);
|
||||||
|
vo.setLiked(liked);
|
||||||
|
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/resources/mapper/KeyboardAiChatSessionMapper.xml
Normal file
19
src/main/resources/mapper/KeyboardAiChatSessionMapper.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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.KeyboardAiChatSessionMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardAiChatSession">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
<!--@Table keyboard_ai_chat_session-->
|
||||||
|
<id column="id" jdbcType="BIGINT" property="id" />
|
||||||
|
<result column="user_id" jdbcType="BIGINT" property="userId" />
|
||||||
|
<result column="companion_id" jdbcType="BIGINT" property="companionId" />
|
||||||
|
<result column="reset_version" jdbcType="INTEGER" property="resetVersion" />
|
||||||
|
<result column="is_active" jdbcType="BOOLEAN" property="isActive" />
|
||||||
|
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
|
||||||
|
<result column="ended_at" jdbcType="TIMESTAMP" property="endedAt" />
|
||||||
|
</resultMap>
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
id, user_id, companion_id, reset_version, is_active, created_at, ended_at
|
||||||
|
</sql>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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.KeyboardAiCompanionReportMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardAiCompanionReport">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
<!--@Table keyboard_ai_companion_report-->
|
||||||
|
<id column="id" jdbcType="BIGINT" property="id" />
|
||||||
|
<result column="companion_id" jdbcType="BIGINT" property="companionId" />
|
||||||
|
<result column="user_id" jdbcType="BIGINT" property="userId" />
|
||||||
|
<result column="report_type" jdbcType="SMALLINT" property="reportType" />
|
||||||
|
<result column="report_desc" jdbcType="VARCHAR" property="reportDesc" />
|
||||||
|
<result column="chat_context" jdbcType="VARCHAR" property="chatContext" />
|
||||||
|
<result column="evidence_image_url" jdbcType="VARCHAR" property="evidenceImageUrl" />
|
||||||
|
<result column="status" jdbcType="SMALLINT" property="status" />
|
||||||
|
<result column="admin_remark" jdbcType="VARCHAR" property="adminRemark" />
|
||||||
|
<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, companion_id, user_id, report_type, report_desc, chat_context, evidence_image_url,
|
||||||
|
"status", admin_remark, created_at, updated_at
|
||||||
|
</sql>
|
||||||
|
</mapper>
|
||||||
Reference in New Issue
Block a user