package com.vincent.rsf.server.ai.service.impl.conversation; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.ai.dto.AiChatMessageDto; import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest; 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.system.enums.StatusType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; @Service @RequiredArgsConstructor public class AiConversationCommandService { private final AiChatSessionMapper aiChatSessionMapper; private final AiChatMessageMapper aiChatMessageMapper; private final AiConversationQueryService aiConversationQueryService; private final AiMemoryProfileService aiMemoryProfileService; @Transactional(rollbackFor = Exception.class) public AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed) { aiConversationQueryService.ensureIdentity(userId, tenantId); String resolvedPromptCode = aiConversationQueryService.requirePromptCode(promptCode); if (sessionId != null) { return aiConversationQueryService.getSession(sessionId, userId, tenantId, resolvedPromptCode); } Date now = new Date(); AiChatSession session = new AiChatSession() .setTitle(aiConversationQueryService.buildSessionTitle(titleSeed)) .setPromptCode(resolvedPromptCode) .setUserId(userId) .setTenantId(tenantId) .setLastMessageTime(now) .setPinned(0) .setStatus(StatusType.ENABLE.val) .setDeleted(0) .setCreateBy(userId) .setCreateTime(now) .setUpdateBy(userId) .setUpdateTime(now); aiChatSessionMapper.insert(session); afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId)); return session; } @Transactional(rollbackFor = Exception.class) public void saveRound(AiChatSession session, Long userId, Long tenantId, List memoryMessages, String assistantContent) { if (session == null || session.getId() == null) { throw new CoolException("AI 会话不存在"); } aiConversationQueryService.ensureIdentity(userId, tenantId); List normalizedMessages = normalizeMessages(memoryMessages); if (normalizedMessages.isEmpty()) { throw new CoolException("本轮没有可保存的对话消息"); } int nextSeqNo = aiConversationQueryService.findNextSeqNo(session.getId()); Date now = new Date(); List records = new ArrayList<>(); for (AiChatMessageDto message : normalizedMessages) { records.add(buildMessageEntity(session.getId(), nextSeqNo++, message.getRole(), message.getContent(), userId, tenantId, now)); } if (StringUtils.hasText(assistantContent)) { records.add(buildMessageEntity(session.getId(), nextSeqNo, "assistant", assistantContent, userId, tenantId, now)); } aiChatMessageMapper.insertBatch(records); aiChatSessionMapper.updateById(new AiChatSession() .setId(session.getId()) .setTitle(aiConversationQueryService.resolveUpdatedTitle(session.getTitle(), normalizedMessages)) .setLastMessageTime(now) .setUpdateBy(userId) .setUpdateTime(now)); afterConversationMutationCommitted(() -> { aiConversationQueryService.evictConversationCaches(tenantId, userId); aiMemoryProfileService.scheduleMemoryProfileRefresh(session.getId(), userId, tenantId); }); } @Transactional(rollbackFor = Exception.class) public void removeSession(Long userId, Long tenantId, Long sessionId) { aiConversationQueryService.ensureIdentity(userId, tenantId); AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId); aiChatSessionMapper.updateById(new AiChatSession() .setId(session.getId()) .setDeleted(1) .setUpdateBy(userId) .setUpdateTime(new Date())); aiChatMessageMapper.softDeleteBySessionId(sessionId); afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId)); } @Transactional(rollbackFor = Exception.class) public AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request) { aiConversationQueryService.ensureIdentity(userId, tenantId); if (request == null || !StringUtils.hasText(request.getTitle())) { throw new CoolException("会话标题不能为空"); } AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId); aiChatSessionMapper.updateById(new AiChatSession() .setId(sessionId) .setTitle(aiConversationQueryService.buildSessionTitle(request.getTitle())) .setUpdateBy(userId) .setUpdateTime(new Date())); afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId)); return reloadSessionDto(sessionId, userId, tenantId, session.getPromptCode()); } @Transactional(rollbackFor = Exception.class) public AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request) { aiConversationQueryService.ensureIdentity(userId, tenantId); if (request == null || request.getPinned() == null) { throw new CoolException("置顶状态不能为空"); } AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId); aiChatSessionMapper.updateById(new AiChatSession() .setId(sessionId) .setPinned(Boolean.TRUE.equals(request.getPinned()) ? 1 : 0) .setUpdateBy(userId) .setUpdateTime(new Date())); afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId)); return reloadSessionDto(sessionId, userId, tenantId, session.getPromptCode()); } @Transactional(rollbackFor = Exception.class) public void clearSessionMemory(Long userId, Long tenantId, Long sessionId) { aiConversationQueryService.ensureIdentity(userId, tenantId); AiChatSession session = aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId); aiChatMessageMapper.softDeleteBySessionId(sessionId); aiChatSessionMapper.updateById(new AiChatSession() .setId(sessionId) .setMemorySummary(null) .setMemoryFacts(null) .setUpdateBy(userId) .setUpdateTime(new Date()) .setLastMessageTime(session.getCreateTime())); afterConversationMutationCommitted(() -> aiConversationQueryService.evictConversationCaches(tenantId, userId)); } @Transactional(rollbackFor = Exception.class) public void retainLatestRound(Long userId, Long tenantId, Long sessionId) { aiConversationQueryService.ensureIdentity(userId, tenantId); aiConversationQueryService.requireOwnedSession(sessionId, userId, tenantId); List records = aiConversationQueryService.listMessageRecords(sessionId); if (records.isEmpty()) { return; } List retained = aiConversationQueryService.tailMessageRecordsByRounds(records, 1); List retainedIds = retained.stream().map(AiChatMessage::getId).toList(); List deletedIds = records.stream() .map(AiChatMessage::getId) .filter(id -> !retainedIds.contains(id)) .toList(); if (!deletedIds.isEmpty()) { aiChatMessageMapper.softDeleteByIds(deletedIds); } afterConversationMutationCommitted(() -> { aiConversationQueryService.evictConversationCaches(tenantId, userId); aiMemoryProfileService.scheduleMemoryProfileRefresh(sessionId, userId, tenantId); }); } private AiChatSessionDto reloadSessionDto(Long sessionId, Long userId, Long tenantId, String promptCode) { return aiConversationQueryService.listSessions(userId, tenantId, promptCode, null).stream() .filter(item -> sessionId.equals(item.getSessionId())) .findFirst() .orElseThrow(() -> new CoolException("AI 会话不存在或无权访问")); } private List normalizeMessages(List memoryMessages) { List normalized = new ArrayList<>(); if (memoryMessages == null || memoryMessages.isEmpty()) { 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 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) .setContentLength(content == null ? 0 : content.length()) .setUserId(userId) .setTenantId(tenantId) .setDeleted(0) .setCreateBy(userId) .setCreateTime(createTime); } private void afterConversationMutationCommitted(Runnable action) { if (action == null) { return; } if (!TransactionSynchronizationManager.isSynchronizationActive()) { action.run(); return; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { action.run(); } }); } }