feat: 落地聊天记录异步持久化基础设施
- 新增 chatlog 模块、AnalyticalDB 公共层与 common-mq Redis Streams 实现 - 建立 Redis 热态、MySQL 热数据、AnalyticalDB 历史查询与同步链路 - 收紧聊天记录幂等、摘要时序与持久化失败语义
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface ChatHistoryManageService {
|
||||
|
||||
ChatSessionPage queryUserSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query);
|
||||
|
||||
ChatSessionPage queryAdminSessions(ChatSessionFilterQuery query);
|
||||
|
||||
ChatSessionSummary getUserSession(BigInteger userId, BigInteger sessionId);
|
||||
|
||||
ChatSessionSummary getAdminSession(BigInteger sessionId);
|
||||
|
||||
ChatHistoryPage queryUserMessages(BigInteger userId, BigInteger sessionId, ChatPageQuery query);
|
||||
|
||||
ChatHistoryPage queryAdminMessages(BigInteger sessionId, ChatPageQuery query);
|
||||
|
||||
void renameUserSession(BigInteger userId, BigInteger sessionId, String title, BigInteger operatorId);
|
||||
|
||||
void deleteUserSession(BigInteger userId, BigInteger sessionId, BigInteger operatorId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface ChatHistoryQueryService {
|
||||
|
||||
ChatHistoryPage queryHistoryMessages(BigInteger sessionId, ChatPageQuery query);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.chatlog.domain.entity.ChatPersistDeadLetter;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.mapper.ChatPersistDeadLetterMapper;
|
||||
import tech.easyflow.chatlog.support.ChatConstants;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
import tech.easyflow.common.mq.core.MQDeadLetterHandler;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class ChatPersistDeadLetterService implements MQDeadLetterHandler {
|
||||
|
||||
private final ChatPersistDeadLetterMapper deadLetterMapper;
|
||||
private final ChatJsonSupport chatJsonSupport;
|
||||
|
||||
public ChatPersistDeadLetterService(ChatPersistDeadLetterMapper deadLetterMapper,
|
||||
ChatJsonSupport chatJsonSupport) {
|
||||
this.deadLetterMapper = deadLetterMapper;
|
||||
this.chatJsonSupport = chatJsonSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String topic) {
|
||||
return ChatConstants.CHAT_PERSIST_TOPIC.equals(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MQMessage message, String reason) {
|
||||
Date now = new Date();
|
||||
ChatPersistEvent event = chatJsonSupport.fromJson(message.getBody(), ChatPersistEvent.class);
|
||||
|
||||
ChatPersistDeadLetter deadLetter = new ChatPersistDeadLetter();
|
||||
deadLetter.setTopic(message.getTopic());
|
||||
deadLetter.setStreamKey(message.getStreamKey());
|
||||
deadLetter.setStreamMessageId(message.getStreamMessageId());
|
||||
deadLetter.setEventId(event == null ? message.getMessageId() : event.getEventId());
|
||||
deadLetter.setSessionId(event == null ? null : event.getSessionId());
|
||||
deadLetter.setPayload(message.getBody());
|
||||
deadLetter.setRetryCount(message.getRetryCount());
|
||||
deadLetter.setErrorMessage(reason);
|
||||
deadLetter.setFirstFailedAt(now);
|
||||
deadLetter.setLastFailedAt(now);
|
||||
deadLetter.setStatus("OPEN");
|
||||
deadLetter.setCreated(now);
|
||||
deadLetter.setModified(now);
|
||||
deadLetterMapper.insert(deadLetter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.cache.ChatHotStateService;
|
||||
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEventType;
|
||||
import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload;
|
||||
import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ChatPersistDispatcher {
|
||||
|
||||
private final ChatHotStateService chatHotStateService;
|
||||
private final ChatPersistEventProducer eventProducer;
|
||||
private final ChatJsonSupport chatJsonSupport;
|
||||
|
||||
public ChatPersistDispatcher(ChatHotStateService chatHotStateService,
|
||||
ChatPersistEventProducer eventProducer,
|
||||
ChatJsonSupport chatJsonSupport) {
|
||||
this.chatHotStateService = chatHotStateService;
|
||||
this.eventProducer = eventProducer;
|
||||
this.chatJsonSupport = chatJsonSupport;
|
||||
}
|
||||
|
||||
public ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command) {
|
||||
ChatSessionSummary summary = chatHotStateService.touchSession(command);
|
||||
eventProducer.send(buildEvent(
|
||||
UUID.randomUUID().toString(),
|
||||
ChatPersistEventType.SESSION_PREPARED,
|
||||
command.getSessionId(),
|
||||
command.getUserId(),
|
||||
command.getAssistantId(),
|
||||
command.getOperateAt(),
|
||||
chatJsonSupport.toJson(command)
|
||||
));
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void appendUserMessage(ChatAppendMessageCommand command) {
|
||||
appendMessage(command, ChatPersistEventType.USER_MESSAGE_APPENDED);
|
||||
}
|
||||
|
||||
public void appendAssistantMessage(ChatAppendMessageCommand command) {
|
||||
appendMessage(command, ChatPersistEventType.ASSISTANT_MESSAGE_APPENDED);
|
||||
}
|
||||
|
||||
public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId) {
|
||||
Date operateAt = new Date();
|
||||
chatHotStateService.renameSession(sessionId, userId, title, operatorId, operateAt);
|
||||
|
||||
ChatSessionRenamePayload payload = new ChatSessionRenamePayload();
|
||||
payload.setSessionId(sessionId);
|
||||
payload.setUserId(userId);
|
||||
payload.setTitle(title);
|
||||
payload.setOperatorId(operatorId);
|
||||
payload.setOperateAt(operateAt);
|
||||
eventProducer.send(buildEvent(
|
||||
UUID.randomUUID().toString(),
|
||||
ChatPersistEventType.SESSION_RENAMED,
|
||||
sessionId,
|
||||
userId,
|
||||
BigInteger.ZERO,
|
||||
operateAt,
|
||||
chatJsonSupport.toJson(payload)
|
||||
));
|
||||
}
|
||||
|
||||
public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId) {
|
||||
Date operateAt = new Date();
|
||||
chatHotStateService.deleteSession(sessionId, userId, operatorId, operateAt);
|
||||
|
||||
ChatSessionDeletePayload payload = new ChatSessionDeletePayload();
|
||||
payload.setSessionId(sessionId);
|
||||
payload.setUserId(userId);
|
||||
payload.setOperatorId(operatorId);
|
||||
payload.setOperateAt(operateAt);
|
||||
eventProducer.send(buildEvent(
|
||||
UUID.randomUUID().toString(),
|
||||
ChatPersistEventType.SESSION_DELETED,
|
||||
sessionId,
|
||||
userId,
|
||||
BigInteger.ZERO,
|
||||
operateAt,
|
||||
chatJsonSupport.toJson(payload)
|
||||
));
|
||||
}
|
||||
|
||||
private void appendMessage(ChatAppendMessageCommand command, ChatPersistEventType eventType) {
|
||||
chatHotStateService.appendMessage(command);
|
||||
eventProducer.send(buildEvent(
|
||||
eventId("message", command.getMessageId()),
|
||||
eventType,
|
||||
command.getSessionId(),
|
||||
command.getUserId(),
|
||||
command.getAssistantId(),
|
||||
command.getCreated(),
|
||||
chatJsonSupport.toJson(command)
|
||||
));
|
||||
}
|
||||
|
||||
private ChatPersistEvent buildEvent(String eventId,
|
||||
ChatPersistEventType eventType,
|
||||
BigInteger sessionId,
|
||||
BigInteger userId,
|
||||
BigInteger assistantId,
|
||||
Date occurredAt,
|
||||
String payload) {
|
||||
ChatPersistEvent event = new ChatPersistEvent();
|
||||
event.setEventId(eventId);
|
||||
event.setEventType(eventType);
|
||||
event.setSessionId(sessionId);
|
||||
event.setUserId(userId);
|
||||
event.setAssistantId(assistantId);
|
||||
event.setOccurredAt(occurredAt == null ? new Date() : occurredAt);
|
||||
event.setTraceId(resolveTraceId(eventId));
|
||||
event.setPayload(payload);
|
||||
return event;
|
||||
}
|
||||
|
||||
private String resolveTraceId(String fallback) {
|
||||
String traceId = MDC.get("traceId");
|
||||
if (traceId == null || traceId.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return traceId;
|
||||
}
|
||||
|
||||
private String eventId(String prefix, BigInteger id) {
|
||||
return prefix + "-" + (id == null ? UUID.randomUUID() : id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.support.ChatConstants;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQSubscription;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ChatPersistEventConsumer implements MQConsumerHandler {
|
||||
|
||||
private final MQProperties mqProperties;
|
||||
private final ChatJsonSupport chatJsonSupport;
|
||||
private final ChatPersistMySqlApplyService applyService;
|
||||
|
||||
public ChatPersistEventConsumer(MQProperties mqProperties,
|
||||
ChatJsonSupport chatJsonSupport,
|
||||
ChatPersistMySqlApplyService applyService) {
|
||||
this.mqProperties = mqProperties;
|
||||
this.chatJsonSupport = chatJsonSupport;
|
||||
this.applyService = applyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MQSubscription subscription() {
|
||||
MQSubscription subscription = new MQSubscription();
|
||||
subscription.setTopic(ChatConstants.CHAT_PERSIST_TOPIC);
|
||||
subscription.setConsumerGroup(ChatConstants.CHAT_PERSIST_GROUP);
|
||||
subscription.setShardCount(Math.max(mqProperties.getRedis().getChatPersistShardCount(), 1));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(List<MQMessage> messages) {
|
||||
List<ChatPersistEvent> events = new ArrayList<>(messages.size());
|
||||
for (MQMessage message : messages) {
|
||||
ChatPersistEvent event = chatJsonSupport.fromJson(message.getBody(), ChatPersistEvent.class);
|
||||
if (event != null) {
|
||||
events.add(event);
|
||||
}
|
||||
}
|
||||
applyService.apply(events);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.support.ChatConstants;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQProducer;
|
||||
|
||||
@Service
|
||||
public class ChatPersistEventProducer {
|
||||
|
||||
private final MQProducer mqProducer;
|
||||
private final ChatJsonSupport chatJsonSupport;
|
||||
|
||||
public ChatPersistEventProducer(MQProducer mqProducer, ChatJsonSupport chatJsonSupport) {
|
||||
this.mqProducer = mqProducer;
|
||||
this.chatJsonSupport = chatJsonSupport;
|
||||
}
|
||||
|
||||
public void send(ChatPersistEvent event) {
|
||||
MQMessage message = new MQMessage();
|
||||
message.setMessageId(event.getEventId());
|
||||
message.setTopic(ChatConstants.CHAT_PERSIST_TOPIC);
|
||||
message.setKey(event.getSessionId() == null ? event.getEventId() : event.getSessionId().toString());
|
||||
message.setBody(chatJsonSupport.toJson(event));
|
||||
message.setCreatedAt(event.getOccurredAt());
|
||||
if (event.getTraceId() != null && !event.getTraceId().isBlank()) {
|
||||
message.getHeaders().put("traceId", event.getTraceId());
|
||||
}
|
||||
mqProducer.send(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionSummaryCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEvent;
|
||||
import tech.easyflow.chatlog.domain.event.ChatPersistEventType;
|
||||
import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload;
|
||||
import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class ChatPersistMySqlApplyService {
|
||||
|
||||
private final MySqlChatSessionRepository sessionRepository;
|
||||
private final MySqlChatLogRepository logRepository;
|
||||
private final MySqlChatLogTableManager tableManager;
|
||||
private final ChatJsonSupport chatJsonSupport;
|
||||
|
||||
public ChatPersistMySqlApplyService(MySqlChatSessionRepository sessionRepository,
|
||||
MySqlChatLogRepository logRepository,
|
||||
MySqlChatLogTableManager tableManager,
|
||||
ChatJsonSupport chatJsonSupport) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.logRepository = logRepository;
|
||||
this.tableManager = tableManager;
|
||||
this.chatJsonSupport = chatJsonSupport;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void apply(List<ChatPersistEvent> events) {
|
||||
if (events == null || events.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<BigInteger, ChatSessionUpsertCommand> sessionUpserts = new LinkedHashMap<>();
|
||||
List<ChatAppendMessageCommand> appendCommands = new ArrayList<>();
|
||||
Map<BigInteger, ChatSessionSummaryCommand> summaryCommands = new LinkedHashMap<>();
|
||||
List<ChatSessionRenamePayload> renamePayloads = new ArrayList<>();
|
||||
List<ChatSessionDeletePayload> deletePayloads = new ArrayList<>();
|
||||
Set<YearMonth> months = new LinkedHashSet<>();
|
||||
|
||||
for (ChatPersistEvent event : events) {
|
||||
if (event == null || event.getEventType() == null) {
|
||||
continue;
|
||||
}
|
||||
switch (event.getEventType()) {
|
||||
case SESSION_PREPARED -> {
|
||||
ChatSessionUpsertCommand command = chatJsonSupport.fromJson(event.getPayload(), ChatSessionUpsertCommand.class);
|
||||
if (command != null && command.getSessionId() != null) {
|
||||
sessionUpserts.put(command.getSessionId(), command);
|
||||
}
|
||||
}
|
||||
case USER_MESSAGE_APPENDED, ASSISTANT_MESSAGE_APPENDED -> {
|
||||
ChatAppendMessageCommand command = chatJsonSupport.fromJson(event.getPayload(), ChatAppendMessageCommand.class);
|
||||
if (command == null || command.getSessionId() == null || command.getMessageId() == null) {
|
||||
continue;
|
||||
}
|
||||
appendCommands.add(command);
|
||||
months.add(resolveMonth(command.getCreated()));
|
||||
accumulateSummary(summaryCommands, command);
|
||||
}
|
||||
case SESSION_RENAMED -> {
|
||||
ChatSessionRenamePayload payload = chatJsonSupport.fromJson(event.getPayload(), ChatSessionRenamePayload.class);
|
||||
if (payload != null && payload.getSessionId() != null) {
|
||||
renamePayloads.add(payload);
|
||||
}
|
||||
}
|
||||
case SESSION_DELETED -> {
|
||||
ChatSessionDeletePayload payload = chatJsonSupport.fromJson(event.getPayload(), ChatSessionDeletePayload.class);
|
||||
if (payload != null && payload.getSessionId() != null) {
|
||||
deletePayloads.add(payload);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionUpserts.isEmpty()) {
|
||||
sessionRepository.createOrTouchBatch(new ArrayList<>(sessionUpserts.values()));
|
||||
}
|
||||
if (!months.isEmpty()) {
|
||||
for (YearMonth month : months) {
|
||||
tableManager.ensureMonthTable(month);
|
||||
}
|
||||
}
|
||||
List<ChatAppendMessageCommand> insertedCommands = List.of();
|
||||
if (!appendCommands.isEmpty()) {
|
||||
insertedCommands = logRepository.appendMessages(appendCommands);
|
||||
}
|
||||
if (!insertedCommands.isEmpty()) {
|
||||
summaryCommands.clear();
|
||||
for (ChatAppendMessageCommand insertedCommand : insertedCommands) {
|
||||
accumulateSummary(summaryCommands, insertedCommand);
|
||||
}
|
||||
sessionRepository.updateSummaries(new ArrayList<>(summaryCommands.values()));
|
||||
}
|
||||
if (!renamePayloads.isEmpty()) {
|
||||
sessionRepository.renameSessions(renamePayloads);
|
||||
}
|
||||
if (!deletePayloads.isEmpty()) {
|
||||
sessionRepository.deleteSessions(deletePayloads);
|
||||
}
|
||||
}
|
||||
|
||||
private void accumulateSummary(Map<BigInteger, ChatSessionSummaryCommand> summaryCommands,
|
||||
ChatAppendMessageCommand command) {
|
||||
ChatSessionSummaryCommand summary = summaryCommands.computeIfAbsent(command.getSessionId(), key -> {
|
||||
ChatSessionSummaryCommand created = new ChatSessionSummaryCommand();
|
||||
created.setSessionId(command.getSessionId());
|
||||
created.setUserId(command.getUserId());
|
||||
created.setMessageIncrement(0);
|
||||
return created;
|
||||
});
|
||||
summary.setMessageIncrement(summary.getMessageIncrement() + 1);
|
||||
if (summary.getLastMessageAt() == null || !command.getCreated().before(summary.getLastMessageAt())) {
|
||||
summary.setLastSenderId(command.getSenderId());
|
||||
summary.setLastSenderName(command.getSenderName());
|
||||
summary.setLastMessagePreview(trimPreview(command.getContentText()));
|
||||
summary.setLastMessageAt(command.getCreated());
|
||||
summary.setOperatorId(command.getCreatedBy());
|
||||
}
|
||||
}
|
||||
|
||||
private YearMonth resolveMonth(Date createdAt) {
|
||||
Date created = createdAt == null ? new Date() : createdAt;
|
||||
return YearMonth.from(created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
|
||||
}
|
||||
|
||||
private String trimPreview(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.length() <= 200 ? text : text.substring(0, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface ChatSessionCommandService {
|
||||
|
||||
ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command);
|
||||
|
||||
void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId);
|
||||
|
||||
void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatSessionQueryService {
|
||||
|
||||
List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query);
|
||||
|
||||
long countSessions(BigInteger userId, BigInteger assistantId);
|
||||
|
||||
ChatSessionPage pageSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query);
|
||||
|
||||
ChatSessionSummary getSessionSummary(BigInteger sessionId);
|
||||
|
||||
List<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint;
|
||||
|
||||
public interface ChatSyncCheckpointService extends IService<ChatSyncCheckpoint> {
|
||||
|
||||
ChatSyncCheckpoint getOrCreate(String syncCode, String shardKey);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSyncResult;
|
||||
|
||||
public interface ChatSyncService {
|
||||
|
||||
ChatSyncResult syncSessions();
|
||||
|
||||
ChatSyncResult syncLogs();
|
||||
|
||||
ChatSyncResult repairLogs();
|
||||
|
||||
void maintainMysqlTables();
|
||||
|
||||
void startupCheck();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface PublicChatSessionRestoreService {
|
||||
|
||||
PublicChatSessionRestoreResult restoreSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId, Integer limit);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
||||
import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository;
|
||||
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||
import tech.easyflow.chatlog.service.ChatHistoryQueryService;
|
||||
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@Service
|
||||
public class ChatHistoryManageServiceImpl implements ChatHistoryManageService {
|
||||
|
||||
private final ChatSessionQueryService chatSessionQueryService;
|
||||
private final ChatSessionCommandService chatSessionCommandService;
|
||||
private final ChatHistoryQueryService chatHistoryQueryService;
|
||||
private final ChatAnalyticalDBRepository chatAnalyticalDBRepository;
|
||||
|
||||
public ChatHistoryManageServiceImpl(ChatSessionQueryService chatSessionQueryService,
|
||||
ChatSessionCommandService chatSessionCommandService,
|
||||
ChatHistoryQueryService chatHistoryQueryService,
|
||||
ChatAnalyticalDBRepository chatAnalyticalDBRepository) {
|
||||
this.chatSessionQueryService = chatSessionQueryService;
|
||||
this.chatSessionCommandService = chatSessionCommandService;
|
||||
this.chatHistoryQueryService = chatHistoryQueryService;
|
||||
this.chatAnalyticalDBRepository = chatAnalyticalDBRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionPage queryUserSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||
return chatSessionQueryService.pageSessions(userId, assistantId, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionPage queryAdminSessions(ChatSessionFilterQuery query) {
|
||||
return chatAnalyticalDBRepository.pageSessions(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary getUserSession(BigInteger userId, BigInteger sessionId) {
|
||||
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||
if (summary == null || summary.getIsDeleted() != null && summary.getIsDeleted() == 1) {
|
||||
throw new BusinessException("会话不存在");
|
||||
}
|
||||
if (!summary.getUserId().equals(userId)) {
|
||||
throw new BusinessException("无权访问该会话");
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary getAdminSession(BigInteger sessionId) {
|
||||
ChatSessionSummary summary = chatAnalyticalDBRepository.getSession(sessionId);
|
||||
if (summary == null || summary.getIsDeleted() != null && summary.getIsDeleted() == 1) {
|
||||
throw new BusinessException("会话不存在");
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatHistoryPage queryUserMessages(BigInteger userId, BigInteger sessionId, ChatPageQuery query) {
|
||||
getUserSession(userId, sessionId);
|
||||
return chatHistoryQueryService.queryHistoryMessages(sessionId, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatHistoryPage queryAdminMessages(BigInteger sessionId, ChatPageQuery query) {
|
||||
getAdminSession(sessionId);
|
||||
return chatHistoryQueryService.queryHistoryMessages(sessionId, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renameUserSession(BigInteger userId, BigInteger sessionId, String title, BigInteger operatorId) {
|
||||
if (!StringUtils.hasText(title)) {
|
||||
throw new BusinessException("标题不能为空");
|
||||
}
|
||||
getUserSession(userId, sessionId);
|
||||
chatSessionCommandService.renameSession(sessionId, userId, title.trim(), operatorId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUserSession(BigInteger userId, BigInteger sessionId, BigInteger operatorId) {
|
||||
getUserSession(userId, sessionId);
|
||||
chatSessionCommandService.deleteSession(sessionId, userId, operatorId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository;
|
||||
import tech.easyflow.chatlog.service.ChatHistoryQueryService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@Service
|
||||
public class ChatHistoryQueryServiceImpl implements ChatHistoryQueryService {
|
||||
|
||||
private final ChatAnalyticalDBRepository chatAnalyticalDBRepository;
|
||||
|
||||
public ChatHistoryQueryServiceImpl(ChatAnalyticalDBRepository chatAnalyticalDBRepository) {
|
||||
this.chatAnalyticalDBRepository = chatAnalyticalDBRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatHistoryPage queryHistoryMessages(BigInteger sessionId, ChatPageQuery query) {
|
||||
return chatAnalyticalDBRepository.queryHistory(sessionId, query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.service.ChatPersistDispatcher;
|
||||
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@Service
|
||||
public class ChatSessionCommandServiceImpl implements ChatSessionCommandService {
|
||||
|
||||
private final ChatPersistDispatcher chatPersistDispatcher;
|
||||
|
||||
public ChatSessionCommandServiceImpl(ChatPersistDispatcher chatPersistDispatcher) {
|
||||
this.chatPersistDispatcher = chatPersistDispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command) {
|
||||
return chatPersistDispatcher.createOrTouchSession(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId) {
|
||||
chatPersistDispatcher.renameSession(sessionId, userId, title, operatorId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId) {
|
||||
chatPersistDispatcher.deleteSession(sessionId, userId, operatorId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.cache.ChatHotStateService;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ChatSessionQueryServiceImpl implements ChatSessionQueryService {
|
||||
|
||||
private final MySqlChatSessionRepository sessionRepository;
|
||||
private final MySqlChatLogRepository logRepository;
|
||||
private final MySqlChatLogTableManager tableManager;
|
||||
private final ChatHotStateService chatHotStateService;
|
||||
|
||||
public ChatSessionQueryServiceImpl(MySqlChatSessionRepository sessionRepository,
|
||||
MySqlChatLogRepository logRepository,
|
||||
MySqlChatLogTableManager tableManager,
|
||||
ChatHotStateService chatHotStateService) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.logRepository = logRepository;
|
||||
this.tableManager = tableManager;
|
||||
this.chatHotStateService = chatHotStateService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||
if (assistantId == null) {
|
||||
List<BigInteger> sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize());
|
||||
if (!sessionIds.isEmpty()) {
|
||||
List<ChatSessionSummary> cached = chatHotStateService.getSessionSummaries(sessionIds);
|
||||
if (cached.size() == sessionIds.size()) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
List<ChatSessionSummary> sessions = sessionRepository.listSessions(userId, null, query);
|
||||
chatHotStateService.cacheSessionSummaries(sessions);
|
||||
return sessions;
|
||||
}
|
||||
List<ChatSessionSummary> sessions = sessionRepository.listSessions(userId, assistantId, query);
|
||||
chatHotStateService.cacheSessionSummaries(sessions);
|
||||
return sessions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countSessions(BigInteger userId, BigInteger assistantId) {
|
||||
return sessionRepository.countSessions(userId, assistantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionPage pageSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||
ChatSessionPage page = new ChatSessionPage();
|
||||
page.setPageNumber(query.getPageNumber());
|
||||
page.setPageSize(query.getPageSize());
|
||||
|
||||
if (assistantId == null && chatHotStateService.hasSessionIndex(userId)) {
|
||||
List<BigInteger> sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize());
|
||||
if (sessionIds.isEmpty()) {
|
||||
page.setTotal(chatHotStateService.countSessions(userId));
|
||||
page.setRecords(List.of());
|
||||
return page;
|
||||
}
|
||||
List<ChatSessionSummary> cached = chatHotStateService.getSessionSummaries(sessionIds);
|
||||
if (cached.size() == sessionIds.size()) {
|
||||
page.setTotal(chatHotStateService.countSessions(userId));
|
||||
page.setRecords(cached);
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
page.setTotal(sessionRepository.countSessions(userId, assistantId));
|
||||
page.setRecords(listSessions(userId, assistantId, query));
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSessionSummary getSessionSummary(BigInteger sessionId) {
|
||||
ChatSessionSummary cached = chatHotStateService.getSessionSummary(sessionId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
ChatSessionSummary summary = sessionRepository.findBySessionId(sessionId);
|
||||
if (summary != null) {
|
||||
chatHotStateService.cacheSessionSummary(summary);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit) {
|
||||
List<ChatMessageRecord> cached = chatHotStateService.getSessionTail(sessionId);
|
||||
if (cached != null) {
|
||||
return cached.subList(0, Math.min(limit, cached.size()));
|
||||
}
|
||||
List<ChatMessageRecord> records = logRepository.listRecentTail(sessionId, tableManager.listRecentExistingMonths(3), limit);
|
||||
chatHotStateService.setSessionTail(sessionId, records);
|
||||
return records;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint;
|
||||
import tech.easyflow.chatlog.mapper.ChatSyncCheckpointMapper;
|
||||
import tech.easyflow.chatlog.service.ChatSyncCheckpointService;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class ChatSyncCheckpointServiceImpl extends ServiceImpl<ChatSyncCheckpointMapper, ChatSyncCheckpoint>
|
||||
implements ChatSyncCheckpointService {
|
||||
|
||||
@Override
|
||||
public ChatSyncCheckpoint getOrCreate(String syncCode, String shardKey) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(ChatSyncCheckpoint::getSyncCode, syncCode)
|
||||
.eq(ChatSyncCheckpoint::getShardKey, shardKey);
|
||||
ChatSyncCheckpoint checkpoint = getMapper().selectOneByQuery(wrapper);
|
||||
if (checkpoint != null) {
|
||||
return checkpoint;
|
||||
}
|
||||
ChatSyncCheckpoint created = new ChatSyncCheckpoint();
|
||||
created.setSyncCode(syncCode);
|
||||
created.setShardKey(shardKey);
|
||||
created.setStatus("READY");
|
||||
created.setModified(new Date());
|
||||
save(created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.config.ChatSyncProperties;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSyncResult;
|
||||
import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint;
|
||||
import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
|
||||
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
|
||||
import tech.easyflow.chatlog.service.ChatSyncCheckpointService;
|
||||
import tech.easyflow.chatlog.service.ChatSyncService;
|
||||
import tech.easyflow.chatlog.support.ChatConstants;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class ChatSyncServiceImpl implements ChatSyncService {
|
||||
|
||||
private final MySqlChatSessionRepository sessionRepository;
|
||||
private final MySqlChatLogRepository logRepository;
|
||||
private final MySqlChatLogTableManager tableManager;
|
||||
private final ChatAnalyticalDBRepository analyticalDBRepository;
|
||||
private final ChatSyncCheckpointService checkpointService;
|
||||
private final ChatSyncProperties syncProperties;
|
||||
|
||||
public ChatSyncServiceImpl(MySqlChatSessionRepository sessionRepository,
|
||||
MySqlChatLogRepository logRepository,
|
||||
MySqlChatLogTableManager tableManager,
|
||||
ChatAnalyticalDBRepository analyticalDBRepository,
|
||||
ChatSyncCheckpointService checkpointService,
|
||||
ChatSyncProperties syncProperties) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.logRepository = logRepository;
|
||||
this.tableManager = tableManager;
|
||||
this.analyticalDBRepository = analyticalDBRepository;
|
||||
this.checkpointService = checkpointService;
|
||||
this.syncProperties = syncProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSyncResult syncSessions() {
|
||||
ChatSyncResult result = new ChatSyncResult();
|
||||
result.setSyncCode(ChatConstants.CHECKPOINT_SYNC_CODE_SESSION);
|
||||
if (!analyticalDBRepository.enabled()) {
|
||||
return result;
|
||||
}
|
||||
int totalRows = 0;
|
||||
ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_SESSION, "default");
|
||||
List<ChatSessionSummary> rows = sessionRepository.loadModifiedAfter(
|
||||
checkpoint.getCursorTime(),
|
||||
checkpoint.getCursorId(),
|
||||
syncProperties.getBatchSize()
|
||||
);
|
||||
if (!rows.isEmpty()) {
|
||||
analyticalDBRepository.upsertSessions(rows);
|
||||
ChatSessionSummary last = rows.get(rows.size() - 1);
|
||||
checkpoint.setCursorTime(last.getModified());
|
||||
checkpoint.setCursorId(last.getId());
|
||||
checkpoint.setCursorTable(ChatConstants.SESSION_TABLE);
|
||||
checkpoint.setLastBatchSize(rows.size());
|
||||
checkpoint.setLastSuccessTime(new Date());
|
||||
checkpoint.setStatus("SUCCESS");
|
||||
checkpoint.setModified(new Date());
|
||||
checkpointService.updateById(checkpoint);
|
||||
totalRows += rows.size();
|
||||
}
|
||||
result.setSyncedRows(totalRows);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSyncResult syncLogs() {
|
||||
ChatSyncResult result = new ChatSyncResult();
|
||||
result.setSyncCode(ChatConstants.CHECKPOINT_SYNC_CODE_LOG);
|
||||
if (!analyticalDBRepository.enabled()) {
|
||||
return result;
|
||||
}
|
||||
ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_LOG, "default");
|
||||
List<YearMonth> months = tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths());
|
||||
Set<LocalDate> touchedDates = new LinkedHashSet<>();
|
||||
int totalRows = 0;
|
||||
for (YearMonth month : months) {
|
||||
String table = "chat_log_" + month.toString().replace("-", "");
|
||||
if (checkpoint.getCursorTable() != null && checkpoint.getCursorTable().compareTo(table) > 0) {
|
||||
continue;
|
||||
}
|
||||
List<ChatMessageRecord> rows = logRepository.loadIncremental(
|
||||
table,
|
||||
table.equals(checkpoint.getCursorTable()) ? checkpoint.getCursorTime() : null,
|
||||
table.equals(checkpoint.getCursorTable()) ? checkpoint.getCursorId() : BigInteger.ZERO,
|
||||
syncProperties.getBatchSize()
|
||||
);
|
||||
if (rows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
analyticalDBRepository.appendLogs(rows);
|
||||
ChatMessageRecord last = rows.get(rows.size() - 1);
|
||||
checkpoint.setCursorTable(table);
|
||||
checkpoint.setCursorTime(last.getCreated());
|
||||
checkpoint.setCursorId(last.getId());
|
||||
checkpoint.setLastBatchSize(rows.size());
|
||||
checkpoint.setLastSuccessTime(new Date());
|
||||
checkpoint.setStatus("SUCCESS");
|
||||
checkpoint.setModified(new Date());
|
||||
checkpointService.updateById(checkpoint);
|
||||
totalRows += rows.size();
|
||||
rows.stream()
|
||||
.map(item -> item.getCreated().toInstant().atZone(ZoneId.systemDefault()).toLocalDate())
|
||||
.forEach(touchedDates::add);
|
||||
}
|
||||
analyticalDBRepository.refreshDws(touchedDates);
|
||||
result.setSyncedRows(totalRows);
|
||||
result.setTouchedDates(new ArrayList<>(touchedDates.stream().map(LocalDate::toString).toList()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatSyncResult repairLogs() {
|
||||
ChatSyncResult result = new ChatSyncResult();
|
||||
result.setSyncCode("chat_log_repair");
|
||||
if (!analyticalDBRepository.enabled()) {
|
||||
return result;
|
||||
}
|
||||
Date startTime = Date.from(LocalDate.now()
|
||||
.minusDays(syncProperties.getRepairLookbackDays())
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant());
|
||||
List<ChatMessageRecord> rows = logRepository.loadRepairRows(
|
||||
tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths()),
|
||||
startTime
|
||||
);
|
||||
analyticalDBRepository.appendLogs(rows);
|
||||
Set<LocalDate> dates = new LinkedHashSet<>();
|
||||
rows.stream().map(item -> item.getCreated().toInstant().atZone(ZoneId.systemDefault()).toLocalDate())
|
||||
.forEach(dates::add);
|
||||
analyticalDBRepository.refreshDws(dates);
|
||||
result.setSyncedRows(rows.size());
|
||||
result.setTouchedDates(new ArrayList<>(dates.stream().map(LocalDate::toString).toList()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maintainMysqlTables() {
|
||||
tableManager.ensureCurrentAndNextMonth();
|
||||
clearExpiredSessions();
|
||||
if (!analyticalDBRepository.enabled()) {
|
||||
return;
|
||||
}
|
||||
ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_LOG, "default");
|
||||
YearMonth threshold = YearMonth.now().minusMonths(syncProperties.getRetentionMonths());
|
||||
for (int i = syncProperties.getRetentionMonths() + 1; i <= 24; i++) {
|
||||
YearMonth month = YearMonth.now().minusMonths(i);
|
||||
if (checkpoint.getCursorTable() != null && checkpoint.getCursorTable().compareTo("chat_log_" + month.toString().replace("-", "")) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (month.isBefore(threshold) && tableManager.tableExists("chat_log_" + month.toString().replace("-", ""))) {
|
||||
tableManager.dropMonthTable(month);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startupCheck() {
|
||||
tableManager.ensureCurrentAndNextMonth();
|
||||
if (analyticalDBRepository.enabled()) {
|
||||
analyticalDBRepository.selfCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearExpiredSessions() {
|
||||
Date expireBefore = Date.from(LocalDate.now()
|
||||
.minusMonths(syncProperties.getRetentionMonths())
|
||||
.atStartOfDay(ZoneId.systemDefault())
|
||||
.toInstant());
|
||||
while (sessionRepository.deleteExpiredSessions(expireBefore, syncProperties.getBatchSize()) > 0) {
|
||||
// loop until all expired session hot data is purged
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.service.ChatPersistDispatcher;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeListener;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Order(100)
|
||||
public class ChatlogRuntimeListener implements ChatRuntimeListener {
|
||||
|
||||
private final SnowFlakeIDKeyGenerator idGenerator = new SnowFlakeIDKeyGenerator();
|
||||
|
||||
private final ChatPersistDispatcher chatPersistDispatcher;
|
||||
private final ChatSessionQueryService chatSessionQueryService;
|
||||
|
||||
public ChatlogRuntimeListener(ChatPersistDispatcher chatPersistDispatcher,
|
||||
ChatSessionQueryService chatSessionQueryService) {
|
||||
this.chatPersistDispatcher = chatPersistDispatcher;
|
||||
this.chatSessionQueryService = chatSessionQueryService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionPrepared(ChatRuntimeContext context) {
|
||||
try {
|
||||
ChatSessionUpsertCommand command = new ChatSessionUpsertCommand();
|
||||
command.setSessionId(context.getSessionId());
|
||||
command.setTenantId(defaultNumber(context.getTenantId()));
|
||||
command.setDeptId(defaultNumber(context.getDeptId()));
|
||||
command.setUserId(defaultNumber(context.getUserId()));
|
||||
command.setUserAccount(context.getUserAccount());
|
||||
command.setAssistantId(defaultNumber(context.getAssistantId()));
|
||||
command.setAssistantCode(context.getAssistantCode());
|
||||
command.setAssistantName(context.getAssistantName());
|
||||
command.setTitle(context.getSessionTitle());
|
||||
command.setOperatorId(defaultNumber(context.getUserId()));
|
||||
chatPersistDispatcher.createOrTouchSession(command);
|
||||
} catch (RuntimeException ex) {
|
||||
throw persistFailed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
try {
|
||||
chatPersistDispatcher.appendUserMessage(toAppendCommand(context, message));
|
||||
} catch (RuntimeException ex) {
|
||||
throw persistFailed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
try {
|
||||
chatPersistDispatcher.appendAssistantMessage(toAppendCommand(context, message));
|
||||
} catch (RuntimeException ex) {
|
||||
throw persistFailed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||
if (context == null || context.getSessionId() == null || limit <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ChatMessageRecord> records = new ArrayList<>(chatSessionQueryService.getRecentTail(context.getSessionId(), limit));
|
||||
Collections.reverse(records);
|
||||
List<ChatRuntimeMessage> messages = new ArrayList<>(records.size());
|
||||
for (ChatMessageRecord record : records) {
|
||||
if (record.getContentText() == null || record.getContentText().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
ChatRuntimeMessage message = new ChatRuntimeMessage();
|
||||
message.setMessageId(record.getId());
|
||||
message.setRole(record.getSenderRole());
|
||||
message.setContentType(record.getContentType());
|
||||
message.setContentText(record.getContentText());
|
||||
message.setContentPayload(record.getContentPayload());
|
||||
message.setCreatedAt(record.getCreated());
|
||||
message.setSenderId(record.getSenderId());
|
||||
message.setSenderName(record.getSenderName());
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private ChatAppendMessageCommand toAppendCommand(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
ChatAppendMessageCommand command = new ChatAppendMessageCommand();
|
||||
command.setMessageId(message.getMessageId() == null ? BigInteger.valueOf(idGenerator.nextId()) : message.getMessageId());
|
||||
command.setTenantId(defaultNumber(context.getTenantId()));
|
||||
command.setDeptId(defaultNumber(context.getDeptId()));
|
||||
command.setSessionId(context.getSessionId());
|
||||
command.setUserId(defaultNumber(context.getUserId()));
|
||||
command.setAssistantId(defaultNumber(context.getAssistantId()));
|
||||
command.setSenderId(defaultNumber(message.getSenderId()));
|
||||
command.setSenderName(message.getSenderName());
|
||||
command.setSenderRole(message.getRole());
|
||||
command.setContentType(message.getContentType());
|
||||
command.setContentText(message.getContentText());
|
||||
command.setContentPayload(message.getContentPayload());
|
||||
command.setCreatedBy(defaultNumber(context.getUserId()));
|
||||
command.setCreated(message.getCreatedAt());
|
||||
return command;
|
||||
}
|
||||
|
||||
private BigInteger defaultNumber(BigInteger value) {
|
||||
return value == null ? BigInteger.ZERO : value;
|
||||
}
|
||||
|
||||
private BusinessException persistFailed(RuntimeException ex) {
|
||||
if (ex instanceof BusinessException businessException) {
|
||||
return businessException;
|
||||
}
|
||||
return new BusinessException("聊天记录持久化失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.config.ChatCacheProperties;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.chatlog.service.PublicChatSessionRestoreService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class PublicChatSessionRestoreServiceImpl implements PublicChatSessionRestoreService {
|
||||
|
||||
private final ChatSessionQueryService chatSessionQueryService;
|
||||
private final ChatCacheProperties chatCacheProperties;
|
||||
|
||||
public PublicChatSessionRestoreServiceImpl(ChatSessionQueryService chatSessionQueryService,
|
||||
ChatCacheProperties chatCacheProperties) {
|
||||
this.chatSessionQueryService = chatSessionQueryService;
|
||||
this.chatCacheProperties = chatCacheProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicChatSessionRestoreResult restoreSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId, Integer limit) {
|
||||
PublicChatSessionRestoreResult result = new PublicChatSessionRestoreResult();
|
||||
result.setConversationId(sessionId == null ? null : sessionId.toString());
|
||||
if (userId == null || assistantId == null || sessionId == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||
return result;
|
||||
}
|
||||
if (!Objects.equals(summary.getUserId(), userId)) {
|
||||
return result;
|
||||
}
|
||||
if (!Objects.equals(summary.getAssistantId(), assistantId)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
List<ChatMessageRecord> tailMessages = new ArrayList<>(chatSessionQueryService.getRecentTail(sessionId, resolveLimit(limit)));
|
||||
Collections.reverse(tailMessages);
|
||||
|
||||
result.setSessionExists(true);
|
||||
result.setSession(summary);
|
||||
result.setMessages(tailMessages);
|
||||
return result;
|
||||
}
|
||||
|
||||
private int resolveLimit(Integer limit) {
|
||||
int defaultLimit = Math.max(chatCacheProperties.getTailSize(), 1);
|
||||
if (limit == null || limit <= 0) {
|
||||
return defaultLimit;
|
||||
}
|
||||
return Math.min(limit, defaultLimit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user