2026-03-18 12:11:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* ProcessMessageUseCase - 处理消息用例
|
2026-03-19 00:56:41 +08:00
|
|
|
|
* 处理消息的业务逻辑,协调 Repository 和 SSEClient
|
2026-03-18 12:11:49 +08:00
|
|
|
|
* 纯业务逻辑,无UI依赖
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
import { messageRepository, userCacheRepository, CachedMessage } from '@/database';
|
2026-03-26 22:04:46 +08:00
|
|
|
|
import { wsClient, WSEventType } from '../../data/datasources/WSClient';
|
2026-04-12 18:14:29 +08:00
|
|
|
|
import { MessageMapper } from '@/data/mappers';
|
2026-03-18 12:11:49 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Message,
|
|
|
|
|
|
Conversation,
|
|
|
|
|
|
GroupNotice,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
isMessageFromCurrentUser,
|
|
|
|
|
|
shouldIncrementUnread,
|
|
|
|
|
|
buildGroupNoticeText,
|
|
|
|
|
|
} from '../entities/Message';
|
|
|
|
|
|
import { messageService } from '../../services/messageService';
|
|
|
|
|
|
import { api } from '../../services/api';
|
|
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const cachedMessageToMessage = (cached: CachedMessage): Message => ({
|
|
|
|
|
|
id: cached.id,
|
|
|
|
|
|
conversationId: cached.conversationId,
|
|
|
|
|
|
senderId: cached.senderId,
|
|
|
|
|
|
seq: cached.seq,
|
|
|
|
|
|
segments: cached.segments || [],
|
|
|
|
|
|
createdAt: cached.createdAt,
|
|
|
|
|
|
status: cached.status as Message['status'],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-18 12:11:49 +08:00
|
|
|
|
// 事件类型定义
|
|
|
|
|
|
export type MessageUseCaseEventType =
|
|
|
|
|
|
| 'message_received'
|
|
|
|
|
|
| 'message_recalled'
|
|
|
|
|
|
| 'typing_status'
|
|
|
|
|
|
| 'group_notice'
|
|
|
|
|
|
| 'connection_changed'
|
|
|
|
|
|
| 'error';
|
|
|
|
|
|
|
|
|
|
|
|
export interface MessageUseCaseEvent {
|
|
|
|
|
|
type: MessageUseCaseEventType;
|
|
|
|
|
|
payload: any;
|
|
|
|
|
|
timestamp: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type MessageUseCaseSubscriber = (event: MessageUseCaseEvent) => void;
|
|
|
|
|
|
|
|
|
|
|
|
// 已读状态保护记录
|
|
|
|
|
|
interface ReadStateRecord {
|
|
|
|
|
|
timestamp: number;
|
|
|
|
|
|
version: number;
|
|
|
|
|
|
lastReadSeq: number;
|
2026-04-12 18:14:29 +08:00
|
|
|
|
clearTimer?: ReturnType<typeof setTimeout>;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ProcessMessageUseCase {
|
|
|
|
|
|
private subscribers: Set<MessageUseCaseSubscriber> = new Set();
|
|
|
|
|
|
private unsubscribeFns: Array<() => void> = [];
|
|
|
|
|
|
private processedMessageIds: Set<string> = new Set();
|
|
|
|
|
|
private processedMessageIdsExpiry: Map<string, number> = new Map();
|
|
|
|
|
|
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
|
|
|
|
|
|
private readStateVersion = 0;
|
|
|
|
|
|
private pendingUserRequests: Map<string, Promise<any>> = new Map();
|
|
|
|
|
|
private currentUserId: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 常量
|
|
|
|
|
|
private readonly READ_STATE_PROTECTION_DELAY = 5000; // 5秒
|
|
|
|
|
|
private readonly MESSAGE_ID_EXPIRY = 5 * 60 * 1000; // 5分钟
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化用例
|
|
|
|
|
|
*/
|
|
|
|
|
|
initialize(currentUserId: string | null): void {
|
|
|
|
|
|
this.currentUserId = currentUserId;
|
2026-03-26 22:04:46 +08:00
|
|
|
|
this.setupWSListeners();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 销毁用例
|
|
|
|
|
|
*/
|
|
|
|
|
|
destroy(): void {
|
|
|
|
|
|
this.unsubscribeFns.forEach((fn) => fn());
|
|
|
|
|
|
this.unsubscribeFns = [];
|
|
|
|
|
|
this.subscribers.clear();
|
|
|
|
|
|
this.processedMessageIds.clear();
|
|
|
|
|
|
this.processedMessageIdsExpiry.clear();
|
|
|
|
|
|
this.pendingReadMap.clear();
|
|
|
|
|
|
this.pendingUserRequests.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-19 00:56:41 +08:00
|
|
|
|
* 设置SSE监听器
|
2026-03-18 12:11:49 +08:00
|
|
|
|
*/
|
2026-03-26 22:04:46 +08:00
|
|
|
|
private setupWSListeners(): void {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
// 监听私聊消息
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubChat = wsClient.on('chat', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleNewMessage(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听群聊消息
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubGroupMessage = wsClient.on('group_message', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleNewMessage(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听私聊已读回执
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubRead = wsClient.on('read', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleReadReceipt(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听群聊已读回执
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubGroupRead = wsClient.on('group_read', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleGroupReadReceipt(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听私聊消息撤回
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubRecall = wsClient.on('recall', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleRecallMessage(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听群聊消息撤回
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubGroupRecall = wsClient.on('group_recall', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleGroupRecallMessage(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听群聊输入状态
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubGroupTyping = wsClient.on('group_typing', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleGroupTyping(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听群通知
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubGroupNotice = wsClient.on('group_notice', (message) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.handleGroupNotice(message);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听连接状态
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubConnected = wsClient.on('connected', () => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'connection_changed',
|
|
|
|
|
|
payload: { connected: true },
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-26 22:04:46 +08:00
|
|
|
|
const unsubDisconnected = wsClient.on('disconnected', () => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'connection_changed',
|
|
|
|
|
|
payload: { connected: false },
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.unsubscribeFns = [
|
|
|
|
|
|
unsubChat,
|
|
|
|
|
|
unsubGroupMessage,
|
|
|
|
|
|
unsubRead,
|
|
|
|
|
|
unsubGroupRead,
|
|
|
|
|
|
unsubRecall,
|
|
|
|
|
|
unsubGroupRecall,
|
|
|
|
|
|
unsubGroupTyping,
|
|
|
|
|
|
unsubGroupNotice,
|
|
|
|
|
|
unsubConnected,
|
|
|
|
|
|
unsubDisconnected,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理新消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async handleNewMessage(message: any): Promise<void> {
|
|
|
|
|
|
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
|
|
|
|
|
|
|
|
|
|
|
|
// 消息去重检查
|
|
|
|
|
|
if (this.isMessageProcessed(id)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.markMessageAsProcessed(id);
|
|
|
|
|
|
|
|
|
|
|
|
// 构造消息对象
|
|
|
|
|
|
const newMessage = createMessage({
|
|
|
|
|
|
id,
|
|
|
|
|
|
conversationId: String(conversation_id),
|
|
|
|
|
|
senderId: sender_id,
|
|
|
|
|
|
seq,
|
|
|
|
|
|
segments: segments || [],
|
|
|
|
|
|
createdAt: created_at,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 异步获取发送者信息(如果是群聊且不是当前用户发送的)
|
|
|
|
|
|
if (message.type === 'group_message' && sender_id && sender_id !== this.currentUserId) {
|
|
|
|
|
|
this.enrichMessageWithSender(newMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 异步保存到本地数据库
|
|
|
|
|
|
const isRead = _isAck || sender_id === this.currentUserId;
|
2026-04-04 08:01:45 +08:00
|
|
|
|
messageRepository.saveMessage({
|
|
|
|
|
|
id: newMessage.id,
|
|
|
|
|
|
conversationId: newMessage.conversationId,
|
|
|
|
|
|
senderId: newMessage.senderId,
|
|
|
|
|
|
seq: newMessage.seq,
|
|
|
|
|
|
segments: newMessage.segments,
|
|
|
|
|
|
createdAt: newMessage.createdAt,
|
|
|
|
|
|
status: newMessage.status || 'normal',
|
|
|
|
|
|
isRead,
|
|
|
|
|
|
}).catch((error) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
console.error('[ProcessMessageUseCase] 保存消息失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 通知订阅者
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'message_received',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
message: newMessage,
|
|
|
|
|
|
isCurrentUser: sender_id === this.currentUserId,
|
|
|
|
|
|
isAck: _isAck,
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 丰富消息的发送者信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async enrichMessageWithSender(message: Message): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const user = await this.getSenderInfo(message.senderId);
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
message.sender = user;
|
|
|
|
|
|
// 重新通知,包含发送者信息
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'message_received',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
message,
|
|
|
|
|
|
isCurrentUser: false,
|
|
|
|
|
|
isUpdated: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 获取发送者信息失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取发送者信息(带缓存和去重)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async getSenderInfo(userId: string): Promise<any | null> {
|
|
|
|
|
|
// 先检查本地缓存
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const cachedUser = await userCacheRepository.get(userId);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
if (cachedUser) {
|
|
|
|
|
|
return cachedUser;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已有正在进行的请求
|
|
|
|
|
|
const pendingRequest = this.pendingUserRequests.get(userId);
|
|
|
|
|
|
if (pendingRequest) {
|
|
|
|
|
|
return pendingRequest;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发起新请求
|
|
|
|
|
|
const request = this.fetchUserInfo(userId);
|
|
|
|
|
|
this.pendingUserRequests.set(userId, request);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const user = await request;
|
|
|
|
|
|
return user;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.pendingUserRequests.delete(userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从服务器获取用户信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async fetchUserInfo(userId: string): Promise<any | null> {
|
|
|
|
|
|
try {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const response = await api.get<any>(`/users/${userId}`);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
if (response.code === 0 && response.data) {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
await userCacheRepository.save(response.data as any);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
return response.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[ProcessMessageUseCase] 获取用户信息失败: ${userId}`, error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查消息是否已处理
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isMessageProcessed(messageId: string): boolean {
|
|
|
|
|
|
return this.processedMessageIds.has(messageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记消息已处理
|
|
|
|
|
|
*/
|
|
|
|
|
|
private markMessageAsProcessed(messageId: string): void {
|
|
|
|
|
|
this.processedMessageIds.add(messageId);
|
|
|
|
|
|
this.processedMessageIdsExpiry.set(messageId, Date.now());
|
|
|
|
|
|
|
|
|
|
|
|
// 定期清理
|
|
|
|
|
|
if (this.processedMessageIds.size > 1000) {
|
|
|
|
|
|
this.cleanupProcessedMessageIds();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清理过期的消息ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
private cleanupProcessedMessageIds(): void {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
|
|
|
|
|
|
if (now - timestamp > this.MESSAGE_ID_EXPIRY) {
|
|
|
|
|
|
this.processedMessageIds.delete(id);
|
|
|
|
|
|
this.processedMessageIdsExpiry.delete(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理私聊已读回执
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleReadReceipt(message: any): void {
|
|
|
|
|
|
// 可以在这里处理对方已读的状态更新
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理群聊已读回执
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleGroupReadReceipt(message: any): void {
|
|
|
|
|
|
// 可以在这里处理群聊已读状态更新
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理私聊消息撤回
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleRecallMessage(message: any): Promise<void> {
|
|
|
|
|
|
const { conversation_id, message_id } = message;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据库状态
|
|
|
|
|
|
messageRepository
|
2026-04-04 08:01:45 +08:00
|
|
|
|
.updateStatus(message_id, 'recalled', true)
|
|
|
|
|
|
.catch((error: unknown) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 通知订阅者
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'message_recalled',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
conversationId: String(conversation_id),
|
|
|
|
|
|
messageId: message_id,
|
|
|
|
|
|
isGroup: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理群聊消息撤回
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleGroupRecallMessage(message: any): Promise<void> {
|
|
|
|
|
|
const { conversation_id, message_id } = message;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据库状态
|
|
|
|
|
|
messageRepository
|
2026-04-04 08:01:45 +08:00
|
|
|
|
.updateStatus(message_id, 'recalled', true)
|
|
|
|
|
|
.catch((error: unknown) => {
|
2026-03-18 12:11:49 +08:00
|
|
|
|
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 通知订阅者
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'message_recalled',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
conversationId: String(conversation_id),
|
|
|
|
|
|
messageId: message_id,
|
|
|
|
|
|
isGroup: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理群聊输入状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleGroupTyping(message: any): void {
|
|
|
|
|
|
const { group_id, user_id, is_typing } = message;
|
|
|
|
|
|
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'typing_status',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
groupId: String(group_id),
|
|
|
|
|
|
userId: user_id,
|
|
|
|
|
|
isTyping: is_typing,
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理群通知
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleGroupNotice(message: any): void {
|
|
|
|
|
|
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
|
|
|
|
|
|
const groupIdStr = String(group_id);
|
|
|
|
|
|
|
|
|
|
|
|
const notice: GroupNotice = {
|
|
|
|
|
|
type: notice_type,
|
|
|
|
|
|
groupId: groupIdStr,
|
|
|
|
|
|
data: data || {},
|
|
|
|
|
|
timestamp: timestamp || Date.now(),
|
2026-03-19 00:56:41 +08:00
|
|
|
|
messageId: message_id,
|
2026-03-18 12:11:49 +08:00
|
|
|
|
seq,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 通知订阅者
|
|
|
|
|
|
this.notifySubscribers({
|
|
|
|
|
|
type: 'group_notice',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
notice,
|
|
|
|
|
|
text: buildGroupNoticeText(notice),
|
|
|
|
|
|
},
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记会话已读
|
|
|
|
|
|
*/
|
|
|
|
|
|
async markAsRead(conversationId: string, seq: number): Promise<void> {
|
|
|
|
|
|
const existingRecord = this.pendingReadMap.get(conversationId);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用seq去重
|
|
|
|
|
|
if (existingRecord && seq <= existingRecord.lastReadSeq) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除可能存在的旧定时器
|
|
|
|
|
|
if (existingRecord?.clearTimer) {
|
|
|
|
|
|
clearTimeout(existingRecord.clearTimer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递增全局版本号
|
|
|
|
|
|
this.readStateVersion++;
|
|
|
|
|
|
const currentVersion = this.readStateVersion;
|
|
|
|
|
|
|
|
|
|
|
|
// 记录已读状态
|
|
|
|
|
|
this.pendingReadMap.set(conversationId, {
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
version: currentVersion,
|
|
|
|
|
|
lastReadSeq: seq,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据库
|
|
|
|
|
|
await messageRepository.markConversationAsRead(conversationId);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用API
|
|
|
|
|
|
try {
|
|
|
|
|
|
await messageService.markAsRead(conversationId, seq);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 标记已读API失败:', error);
|
|
|
|
|
|
this.pendingReadMap.delete(conversationId);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置延迟清除保护
|
|
|
|
|
|
const clearTimer = setTimeout(() => {
|
|
|
|
|
|
const record = this.pendingReadMap.get(conversationId);
|
|
|
|
|
|
if (record && record.version === currentVersion) {
|
|
|
|
|
|
this.pendingReadMap.delete(conversationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, this.READ_STATE_PROTECTION_DELAY);
|
|
|
|
|
|
|
|
|
|
|
|
this.pendingReadMap.set(conversationId, {
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
version: currentVersion,
|
|
|
|
|
|
lastReadSeq: seq,
|
|
|
|
|
|
clearTimer,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查会话是否有进行中的已读请求
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasPendingRead(conversationId: string): boolean {
|
|
|
|
|
|
return this.pendingReadMap.has(conversationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取进行中的已读记录
|
|
|
|
|
|
*/
|
|
|
|
|
|
getPendingReadRecord(conversationId: string): ReadStateRecord | undefined {
|
|
|
|
|
|
return this.pendingReadMap.get(conversationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async sendMessage(
|
|
|
|
|
|
conversationId: string,
|
|
|
|
|
|
segments: any[],
|
|
|
|
|
|
options?: { replyToId?: string }
|
|
|
|
|
|
): Promise<Message | null> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await messageService.sendMessage(conversationId, {
|
|
|
|
|
|
segments,
|
|
|
|
|
|
reply_to_id: options?.replyToId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response) {
|
|
|
|
|
|
const message = createMessage({
|
|
|
|
|
|
id: response.id,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
senderId: this.currentUserId || '',
|
|
|
|
|
|
seq: response.seq,
|
|
|
|
|
|
segments,
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到本地数据库
|
2026-04-04 08:01:45 +08:00
|
|
|
|
await messageRepository.saveMessage({
|
|
|
|
|
|
id: message.id,
|
|
|
|
|
|
conversationId: message.conversationId,
|
|
|
|
|
|
senderId: message.senderId,
|
|
|
|
|
|
seq: message.seq,
|
|
|
|
|
|
segments: message.segments,
|
|
|
|
|
|
createdAt: message.createdAt,
|
|
|
|
|
|
status: message.status || 'normal',
|
|
|
|
|
|
isRead: true,
|
|
|
|
|
|
});
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
return message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 发送消息失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取本地消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getLocalMessages(conversationId: string, limit: number = 20): Promise<Message[]> {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const cached = await messageRepository.getByConversation(conversationId, limit);
|
|
|
|
|
|
return cached.map(cachedMessageToMessage);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取历史消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getHistoryMessages(
|
|
|
|
|
|
conversationId: string,
|
|
|
|
|
|
beforeSeq: number,
|
|
|
|
|
|
limit: number = 20
|
|
|
|
|
|
): Promise<Message[]> {
|
|
|
|
|
|
// 先从本地获取
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const localMessages = await messageRepository.getBeforeSeq(
|
2026-03-18 12:11:49 +08:00
|
|
|
|
conversationId,
|
|
|
|
|
|
beforeSeq,
|
|
|
|
|
|
limit
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (localMessages.length >= limit) {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
return localMessages.map(cachedMessageToMessage);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 本地数据不足,从服务端获取
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await messageService.getMessages(
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
beforeSeq,
|
|
|
|
|
|
limit
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (response?.messages && response.messages.length > 0) {
|
|
|
|
|
|
const serverMessages = response.messages.map((m: any) =>
|
|
|
|
|
|
createMessage({
|
|
|
|
|
|
id: m.id,
|
|
|
|
|
|
conversationId: m.conversation_id || conversationId,
|
|
|
|
|
|
senderId: m.sender_id,
|
|
|
|
|
|
seq: m.seq,
|
|
|
|
|
|
segments: m.segments || [],
|
|
|
|
|
|
createdAt: m.created_at,
|
|
|
|
|
|
status: m.status || 'normal',
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到本地
|
2026-04-12 18:14:29 +08:00
|
|
|
|
await messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(serverMessages, conversationId)
|
|
|
|
|
|
);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
return serverMessages;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 获取历史消息失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
return localMessages.map(cachedMessageToMessage);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从服务端同步消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async syncMessagesFromServer(
|
|
|
|
|
|
conversationId: string,
|
|
|
|
|
|
afterSeq?: number
|
|
|
|
|
|
): Promise<Message[]> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await messageService.getMessages(conversationId, afterSeq);
|
|
|
|
|
|
|
|
|
|
|
|
if (response?.messages && response.messages.length > 0) {
|
|
|
|
|
|
const messages = response.messages.map((m: any) =>
|
|
|
|
|
|
createMessage({
|
|
|
|
|
|
id: m.id,
|
|
|
|
|
|
conversationId: m.conversation_id || conversationId,
|
|
|
|
|
|
senderId: m.sender_id,
|
|
|
|
|
|
seq: m.seq,
|
|
|
|
|
|
segments: m.segments || [],
|
|
|
|
|
|
createdAt: m.created_at,
|
|
|
|
|
|
status: m.status || 'normal',
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-12 18:14:29 +08:00
|
|
|
|
await messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(response.messages, conversationId)
|
|
|
|
|
|
);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
return messages;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 同步消息失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 订阅事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
subscribe(subscriber: MessageUseCaseSubscriber): () => void {
|
|
|
|
|
|
this.subscribers.add(subscriber);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
this.subscribers.delete(subscriber);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通知订阅者
|
|
|
|
|
|
*/
|
|
|
|
|
|
private notifySubscribers(event: MessageUseCaseEvent): void {
|
|
|
|
|
|
this.subscribers.forEach((subscriber) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
subscriber(event);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[ProcessMessageUseCase] 订阅者执行失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前用户ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
getCurrentUserId(): string | null {
|
|
|
|
|
|
return this.currentUserId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置当前用户ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
setCurrentUserId(userId: string | null): void {
|
|
|
|
|
|
this.currentUserId = userId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const processMessageUseCase = new ProcessMessageUseCase();
|
|
|
|
|
|
export default processMessageUseCase;
|