mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
fix(conversation): cascade-clean orphan rows on conversation delete
This commit is contained in:
parent
da6fa5089d
commit
73d69c1dfe
@ -1,16 +1,25 @@
|
|||||||
package vip.mate.workspace.conversation;
|
package vip.mate.workspace.conversation;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
import vip.mate.agent.model.AgentEntity;
|
import vip.mate.agent.model.AgentEntity;
|
||||||
import vip.mate.approval.ApprovalPlaceholderUtil;
|
import vip.mate.approval.ApprovalPlaceholderUtil;
|
||||||
import vip.mate.approval.MetadataDecision;
|
import vip.mate.approval.MetadataDecision;
|
||||||
import vip.mate.agent.repository.AgentMapper;
|
import vip.mate.agent.repository.AgentMapper;
|
||||||
|
import vip.mate.approval.model.ToolApprovalEntity;
|
||||||
|
import vip.mate.approval.repository.ToolApprovalMapper;
|
||||||
|
import vip.mate.channel.model.ChannelSessionEntity;
|
||||||
|
import vip.mate.channel.repository.ChannelSessionMapper;
|
||||||
|
import vip.mate.task.model.AsyncTaskEntity;
|
||||||
|
import vip.mate.task.repository.AsyncTaskMapper;
|
||||||
import vip.mate.workspace.conversation.model.ConversationEntity;
|
import vip.mate.workspace.conversation.model.ConversationEntity;
|
||||||
import vip.mate.workspace.conversation.model.MessageContentPart;
|
import vip.mate.workspace.conversation.model.MessageContentPart;
|
||||||
import vip.mate.workspace.conversation.model.MessageEntity;
|
import vip.mate.workspace.conversation.model.MessageEntity;
|
||||||
@ -49,6 +58,9 @@ public class ConversationService {
|
|||||||
private final MessageMapper messageMapper;
|
private final MessageMapper messageMapper;
|
||||||
private final AgentMapper agentMapper;
|
private final AgentMapper agentMapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ToolApprovalMapper toolApprovalMapper;
|
||||||
|
private final AsyncTaskMapper asyncTaskMapper;
|
||||||
|
private final ChannelSessionMapper channelSessionMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户的会话列表(返回 VO,包含 agentName/agentIcon/status)
|
* 获取用户的会话列表(返回 VO,包含 agentName/agentIcon/status)
|
||||||
@ -421,15 +433,68 @@ public class ConversationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话(同时删除消息和附件文件)
|
* Delete a conversation and cascade-clean every row that referenced it.
|
||||||
|
* <p>
|
||||||
|
* Tables cleaned in the same transaction:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code mate_message} — chat history</li>
|
||||||
|
* <li>{@code mate_tool_approval} — pending approvals would otherwise
|
||||||
|
* point to a non-existent conversation and surface as ghost items
|
||||||
|
* in the approvals list</li>
|
||||||
|
* <li>{@code mate_async_task} — long-running task records keyed on
|
||||||
|
* this conversation</li>
|
||||||
|
* <li>{@code mate_channel_session} — channel-side session row (the
|
||||||
|
* column is UNIQUE; leaving it would block reuse of the same id)</li>
|
||||||
|
* <li>{@code mate_conversation} — the conversation itself</li>
|
||||||
|
* </ul>
|
||||||
|
* Child conversations (delegated turns) have their
|
||||||
|
* {@code parent_conversation_id} set to NULL rather than cascade-deleted,
|
||||||
|
* so the user keeps independent access to delegated work.
|
||||||
|
* <p>
|
||||||
|
* Audit / history tables ({@code mate_tool_guard_audit_log},
|
||||||
|
* {@code mate_cron_job_run}, {@code mate_skill.source_conversation_id},
|
||||||
|
* {@code mate_skill_usage_stat}) are intentionally left alone — those
|
||||||
|
* are append-only records that should outlive their source conversation.
|
||||||
|
* <p>
|
||||||
|
* Attachment file cleanup is registered as an after-commit hook so it
|
||||||
|
* runs only when the DB cascade actually persists, and an IO failure
|
||||||
|
* cannot roll back the database deletes.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteConversation(String conversationId) {
|
public void deleteConversation(String conversationId) {
|
||||||
conversationMapper.delete(new LambdaQueryWrapper<ConversationEntity>()
|
int messages = messageMapper.delete(new LambdaQueryWrapper<MessageEntity>()
|
||||||
.eq(ConversationEntity::getConversationId, conversationId));
|
|
||||||
messageMapper.delete(new LambdaQueryWrapper<MessageEntity>()
|
|
||||||
.eq(MessageEntity::getConversationId, conversationId));
|
.eq(MessageEntity::getConversationId, conversationId));
|
||||||
cleanAttachmentFiles(conversationId);
|
int approvals = toolApprovalMapper.delete(new LambdaQueryWrapper<ToolApprovalEntity>()
|
||||||
|
.eq(ToolApprovalEntity::getConversationId, conversationId));
|
||||||
|
int asyncTasks = asyncTaskMapper.delete(new LambdaQueryWrapper<AsyncTaskEntity>()
|
||||||
|
.eq(AsyncTaskEntity::getConversationId, conversationId));
|
||||||
|
int channelSessions = channelSessionMapper.delete(new LambdaQueryWrapper<ChannelSessionEntity>()
|
||||||
|
.eq(ChannelSessionEntity::getConversationId, conversationId));
|
||||||
|
int childrenUnlinked = conversationMapper.update(null, new LambdaUpdateWrapper<ConversationEntity>()
|
||||||
|
.set(ConversationEntity::getParentConversationId, null)
|
||||||
|
.eq(ConversationEntity::getParentConversationId, conversationId));
|
||||||
|
int conversations = conversationMapper.delete(new LambdaQueryWrapper<ConversationEntity>()
|
||||||
|
.eq(ConversationEntity::getConversationId, conversationId));
|
||||||
|
|
||||||
|
log.info("[Conversation] Deleted {}: messages={}, approvals={}, asyncTasks={},"
|
||||||
|
+ " channelSessions={}, childrenUnlinked={}, conversationRow={}",
|
||||||
|
conversationId, messages, approvals, asyncTasks,
|
||||||
|
channelSessions, childrenUnlinked, conversations);
|
||||||
|
|
||||||
|
registerAttachmentCleanupAfterCommit(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerAttachmentCleanupAfterCommit(String conversationId) {
|
||||||
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
cleanAttachmentFiles(conversationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cleanAttachmentFiles(conversationId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user