package com.vincent.rsf.server.ai.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.framework.common.Cools; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.ai.dto.AiChatMemoryDto; import com.vincent.rsf.server.ai.dto.AiChatMessageDto; import com.vincent.rsf.server.ai.dto.AiChatSessionDto; import com.vincent.rsf.server.ai.entity.AiChatMessage; import com.vincent.rsf.server.ai.entity.AiChatSession; import com.vincent.rsf.server.ai.mapper.AiChatMessageMapper; import com.vincent.rsf.server.ai.mapper.AiChatSessionMapper; import com.vincent.rsf.server.ai.service.AiChatMemoryService; import com.vincent.rsf.server.system.enums.StatusType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; @Service @RequiredArgsConstructor public class AiChatMemoryServiceImpl implements AiChatMemoryService { private final AiChatSessionMapper aiChatSessionMapper; private final AiChatMessageMapper aiChatMessageMapper; @Override public AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId) { ensureIdentity(userId, tenantId); String resolvedPromptCode = requirePromptCode(promptCode); AiChatSession session = sessionId == null ? findLatestSession(userId, tenantId, resolvedPromptCode) : getSession(sessionId, userId, tenantId, resolvedPromptCode); if (session == null) { return AiChatMemoryDto.builder() .sessionId(null) .persistedMessages(List.of()) .build(); } return AiChatMemoryDto.builder() .sessionId(session.getId()) .persistedMessages(listMessages(session.getId())) .build(); } @Override public List listSessions(Long userId, Long tenantId, String promptCode) { ensureIdentity(userId, tenantId); String resolvedPromptCode = requirePromptCode(promptCode); List sessions = aiChatSessionMapper.selectList(new LambdaQueryWrapper() .eq(AiChatSession::getUserId, userId) .eq(AiChatSession::getTenantId, tenantId) .eq(AiChatSession::getPromptCode, resolvedPromptCode) .eq(AiChatSession::getDeleted, 0) .eq(AiChatSession::getStatus, StatusType.ENABLE.val) .orderByDesc(AiChatSession::getLastMessageTime) .orderByDesc(AiChatSession::getId)); if (Cools.isEmpty(sessions)) { return List.of(); } List result = new ArrayList<>(); for (AiChatSession session : sessions) { result.add(AiChatSessionDto.builder() .sessionId(session.getId()) .title(session.getTitle()) .promptCode(session.getPromptCode()) .lastMessageTime(session.getLastMessageTime()) .build()); } return result; } @Override public AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed) { ensureIdentity(userId, tenantId); String resolvedPromptCode = requirePromptCode(promptCode); if (sessionId != null) { return getSession(sessionId, userId, tenantId, resolvedPromptCode); } Date now = new Date(); AiChatSession session = new AiChatSession() .setTitle(buildSessionTitle(titleSeed)) .setPromptCode(resolvedPromptCode) .setUserId(userId) .setTenantId(tenantId) .setLastMessageTime(now) .setStatus(StatusType.ENABLE.val) .setDeleted(0) .setCreateBy(userId) .setCreateTime(now) .setUpdateBy(userId) .setUpdateTime(now); aiChatSessionMapper.insert(session); return session; } @Override public void saveRound(AiChatSession session, Long userId, Long tenantId, List memoryMessages, String assistantContent) { if (session == null || session.getId() == null) { throw new CoolException("AI 会话不存在"); } ensureIdentity(userId, tenantId); List normalizedMessages = normalizeMessages(memoryMessages); if (normalizedMessages.isEmpty()) { throw new CoolException("本轮没有可保存的对话消息"); } int nextSeqNo = findNextSeqNo(session.getId()); Date now = new Date(); for (AiChatMessageDto message : normalizedMessages) { aiChatMessageMapper.insert(buildMessageEntity(session.getId(), nextSeqNo++, message.getRole(), message.getContent(), userId, tenantId, now)); } if (StringUtils.hasText(assistantContent)) { aiChatMessageMapper.insert(buildMessageEntity(session.getId(), nextSeqNo, "assistant", assistantContent, userId, tenantId, now)); } AiChatSession update = new AiChatSession() .setId(session.getId()) .setTitle(resolveUpdatedTitle(session.getTitle(), normalizedMessages)) .setLastMessageTime(now) .setUpdateBy(userId) .setUpdateTime(now); aiChatSessionMapper.updateById(update); } @Override public void removeSession(Long userId, Long tenantId, Long sessionId) { ensureIdentity(userId, tenantId); if (sessionId == null) { throw new CoolException("AI 会话 ID 不能为空"); } AiChatSession session = aiChatSessionMapper.selectOne(new LambdaQueryWrapper() .eq(AiChatSession::getId, sessionId) .eq(AiChatSession::getUserId, userId) .eq(AiChatSession::getTenantId, tenantId) .eq(AiChatSession::getDeleted, 0) .last("limit 1")); if (session == null) { throw new CoolException("AI 会话不存在或无权删除"); } Date now = new Date(); AiChatSession updateSession = new AiChatSession() .setId(sessionId) .setDeleted(1) .setUpdateBy(userId) .setUpdateTime(now); aiChatSessionMapper.updateById(updateSession); List messages = aiChatMessageMapper.selectList(new LambdaQueryWrapper() .eq(AiChatMessage::getSessionId, sessionId) .eq(AiChatMessage::getDeleted, 0)); for (AiChatMessage message : messages) { AiChatMessage updateMessage = new AiChatMessage() .setId(message.getId()) .setDeleted(1); aiChatMessageMapper.updateById(updateMessage); } } private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) { return aiChatSessionMapper.selectOne(new LambdaQueryWrapper() .eq(AiChatSession::getUserId, userId) .eq(AiChatSession::getTenantId, tenantId) .eq(AiChatSession::getPromptCode, promptCode) .eq(AiChatSession::getDeleted, 0) .eq(AiChatSession::getStatus, StatusType.ENABLE.val) .orderByDesc(AiChatSession::getLastMessageTime) .orderByDesc(AiChatSession::getId) .last("limit 1")); } private AiChatSession getSession(Long sessionId, Long userId, Long tenantId, String promptCode) { AiChatSession session = aiChatSessionMapper.selectOne(new LambdaQueryWrapper() .eq(AiChatSession::getId, sessionId) .eq(AiChatSession::getUserId, userId) .eq(AiChatSession::getTenantId, tenantId) .eq(AiChatSession::getPromptCode, promptCode) .eq(AiChatSession::getDeleted, 0) .eq(AiChatSession::getStatus, StatusType.ENABLE.val) .last("limit 1")); if (session == null) { throw new CoolException("AI 会话不存在或无权访问"); } return session; } private List listMessages(Long sessionId) { List records = aiChatMessageMapper.selectList(new LambdaQueryWrapper() .eq(AiChatMessage::getSessionId, sessionId) .eq(AiChatMessage::getDeleted, 0) .orderByAsc(AiChatMessage::getSeqNo) .orderByAsc(AiChatMessage::getId)); if (Cools.isEmpty(records)) { return List.of(); } List messages = new ArrayList<>(); for (AiChatMessage record : records) { if (!StringUtils.hasText(record.getContent())) { continue; } AiChatMessageDto item = new AiChatMessageDto(); item.setRole(record.getRole()); item.setContent(record.getContent()); messages.add(item); } return messages; } private List normalizeMessages(List memoryMessages) { List normalized = new ArrayList<>(); if (Cools.isEmpty(memoryMessages)) { return normalized; } for (AiChatMessageDto item : memoryMessages) { if (item == null || !StringUtils.hasText(item.getContent())) { continue; } String role = item.getRole() == null ? "user" : item.getRole().toLowerCase(); if ("system".equals(role)) { continue; } AiChatMessageDto normalizedItem = new AiChatMessageDto(); normalizedItem.setRole("assistant".equals(role) ? "assistant" : "user"); normalizedItem.setContent(item.getContent().trim()); normalized.add(normalizedItem); } return normalized; } private int findNextSeqNo(Long sessionId) { AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper() .eq(AiChatMessage::getSessionId, sessionId) .eq(AiChatMessage::getDeleted, 0) .orderByDesc(AiChatMessage::getSeqNo) .orderByDesc(AiChatMessage::getId) .last("limit 1")); return lastMessage == null || lastMessage.getSeqNo() == null ? 1 : lastMessage.getSeqNo() + 1; } private AiChatMessage buildMessageEntity(Long sessionId, int seqNo, String role, String content, Long userId, Long tenantId, Date createTime) { return new AiChatMessage() .setSessionId(sessionId) .setSeqNo(seqNo) .setRole(role) .setContent(content) .setUserId(userId) .setTenantId(tenantId) .setDeleted(0) .setCreateBy(userId) .setCreateTime(createTime); } private String resolveUpdatedTitle(String currentTitle, List memoryMessages) { if (StringUtils.hasText(currentTitle)) { return currentTitle; } for (AiChatMessageDto item : memoryMessages) { if ("user".equals(item.getRole()) && StringUtils.hasText(item.getContent())) { return buildSessionTitle(item.getContent()); } } return null; } private String buildSessionTitle(String titleSeed) { if (!StringUtils.hasText(titleSeed)) { throw new CoolException("AI 会话标题不能为空"); } String title = titleSeed.trim().replace("\r", " ").replace("\n", " "); return title.length() > 60 ? title.substring(0, 60) : title; } private void ensureIdentity(Long userId, Long tenantId) { if (userId == null) { throw new CoolException("当前登录用户不存在"); } if (tenantId == null) { throw new CoolException("当前租户不存在"); } } private String requirePromptCode(String promptCode) { if (!StringUtils.hasText(promptCode)) { throw new CoolException("Prompt 编码不能为空"); } return promptCode; } }