2026-03-18 12:11:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 消息同步服务
|
2026-03-31 16:13:24 +08:00
|
|
|
|
* 处理消息和会话的服务器同步逻辑
|
2026-03-31 18:22:24 +08:00
|
|
|
|
*
|
2026-03-31 19:15:13 +08:00
|
|
|
|
* 重构说明:
|
|
|
|
|
|
* - 直接使用 zustand store 替代 IMessageStateManager
|
|
|
|
|
|
* - 移除了 SubscriptionManager 调用,Zustand 的 set() 会自动触发组件重渲染
|
2026-03-18 12:11:49 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
import type { ConversationResponse, MessageResponse } from '../../../types/dto';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { messageService } from '@/services/message';
|
2026-04-04 08:01:45 +08:00
|
|
|
|
import { messageRepository, conversationRepository } from '@/database';
|
2026-04-12 18:14:29 +08:00
|
|
|
|
import { MessageMapper } from '@/data/mappers';
|
2026-03-31 16:13:24 +08:00
|
|
|
|
import {
|
|
|
|
|
|
type IConversationListPagedSource,
|
|
|
|
|
|
SqliteConversationListPagedSource,
|
|
|
|
|
|
createRemoteConversationListSource,
|
|
|
|
|
|
CONVERSATION_LIST_PAGE_SIZE,
|
2026-04-13 04:56:58 +08:00
|
|
|
|
} from '../sources';
|
2026-03-31 18:22:24 +08:00
|
|
|
|
import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types';
|
2026-03-31 19:15:13 +08:00
|
|
|
|
import { useMessageStore, normalizeConversationId, mergeMessagesById } from '../store';
|
2026-03-31 18:22:24 +08:00
|
|
|
|
import { ReadReceiptManager } from './ReadReceiptManager';
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
export class MessageSyncService implements IMessageSyncService {
|
|
|
|
|
|
private getCurrentUserId: () => string | null;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
private readReceiptManager: ReadReceiptManager;
|
|
|
|
|
|
private userCacheService: IUserCacheService;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
/** 远端会话列表(游标或页码) */
|
|
|
|
|
|
private readonly remoteConversationListSource: IConversationListPagedSource;
|
|
|
|
|
|
/** 本地会话列表缓存(SQLite) */
|
|
|
|
|
|
private readonly localConversationListSource: IConversationListPagedSource;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
/** 正在加载会话列表下一页 */
|
|
|
|
|
|
private loadingMoreConversations = false;
|
|
|
|
|
|
/** 聊天页活跃期间延迟的会话列表刷新 */
|
|
|
|
|
|
private deferredConversationRefresh = false;
|
2026-05-12 01:28:46 +08:00
|
|
|
|
/** fetchUnreadCount 去重:复用 in-flight promise */
|
|
|
|
|
|
private fetchUnreadCountPromise: Promise<void> | null = null;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
constructor(
|
|
|
|
|
|
getCurrentUserId: () => string | null,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
readReceiptManager: ReadReceiptManager,
|
|
|
|
|
|
userCacheService: IUserCacheService,
|
2026-03-31 16:13:24 +08:00
|
|
|
|
deps?: MessageManagerConversationListDeps
|
|
|
|
|
|
) {
|
|
|
|
|
|
this.getCurrentUserId = getCurrentUserId;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
this.readReceiptManager = readReceiptManager;
|
|
|
|
|
|
this.userCacheService = userCacheService;
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE;
|
|
|
|
|
|
this.remoteConversationListSource =
|
|
|
|
|
|
deps?.remoteConversationListSource ??
|
|
|
|
|
|
createRemoteConversationListSource(
|
|
|
|
|
|
deps?.remoteListKind === 'offset' ? 'offset' : 'cursor',
|
|
|
|
|
|
pageSize
|
|
|
|
|
|
);
|
|
|
|
|
|
this.localConversationListSource =
|
|
|
|
|
|
deps?.localConversationListSource ?? new SqliteConversationListPagedSource();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise<void> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
|
|
|
|
|
|
if (store.isLoading() && !forceRefresh) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.shouldDeferConversationRefresh(source, forceRefresh)) {
|
|
|
|
|
|
this.deferredConversationRefresh = true;
|
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
|
console.log('[MessageSyncService] defer fetchConversations', {
|
|
|
|
|
|
source,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
activeConversationId: store.getActiveConversation(),
|
2026-03-31 16:13:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
|
console.log('[MessageSyncService] fetchConversations', {
|
|
|
|
|
|
source,
|
|
|
|
|
|
forceRefresh,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
activeConversationId: store.getActiveConversation(),
|
2026-03-31 16:13:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 非强制刷新且内存为空:先用本地源暖机
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const currentState = store.getState();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
if (!forceRefresh && currentState.conversations.size === 0) {
|
|
|
|
|
|
const warmed = await this.hydrateConversationsFromLocalSource();
|
|
|
|
|
|
if (warmed) {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
this.persistConversationListCache();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setLoading(true);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.remoteConversationListSource.restart();
|
|
|
|
|
|
const page = await this.remoteConversationListSource.loadNext();
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 使用 zustand set 函数正确更新状态
|
|
|
|
|
|
const newConversations = new Map<string, ConversationResponse>();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
page.items.forEach(conv => {
|
|
|
|
|
|
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
newConversations.set(normalizedConv.id, normalizedConv);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
});
|
2026-03-31 18:22:24 +08:00
|
|
|
|
|
2026-05-12 01:28:46 +08:00
|
|
|
|
// 合并更新:conversations + totalUnread 在同一次 set 中完成,消除中间状态窗口
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const totalUnread = Array.from(newConversations.values()).reduce(
|
|
|
|
|
|
(sum, conv) => sum + (conv.unread_count || 0),
|
2026-05-12 01:28:46 +08:00
|
|
|
|
0,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
);
|
2026-05-12 01:28:46 +08:00
|
|
|
|
store.setConversationsWithUnread(newConversations, totalUnread, store.getUnreadCount().system);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
|
2026-03-31 19:15:13 +08:00
|
|
|
|
this.persistConversationListCache();
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} catch (error) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 获取会话列表失败:', error);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
if (store.getConversations().length === 0) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const recovered = await this.hydrateConversationsFromLocalSource();
|
|
|
|
|
|
if (recovered) {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
this.persistConversationListCache();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} finally {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setLoading(false);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 会话列表加载下一页
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadMoreConversations(): Promise<void> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
if (!this.remoteConversationListSource.hasMore) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-31 18:22:24 +08:00
|
|
|
|
if (this.loadingMoreConversations || store.isLoading()) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.loadingMoreConversations = true;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
try {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const page = await this.remoteConversationListSource.loadNext();
|
|
|
|
|
|
|
|
|
|
|
|
page.items.forEach(conv => {
|
|
|
|
|
|
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.updateConversation(normalizedConv);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
});
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.recomputeConversationTotalUnread();
|
2026-03-31 19:15:13 +08:00
|
|
|
|
this.persistConversationListCache();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[MessageSyncService] 加载更多会话失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingMoreConversations = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取单个会话详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const detail = await messageService.getConversationById(normalizedId);
|
|
|
|
|
|
if (detail) {
|
|
|
|
|
|
const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.updateConversation(normalizedDetail);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return normalizedDetail;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[MessageSyncService] 获取会话详情失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取会话消息(增量同步)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fetchMessages(conversationId: string, afterSeq?: number): Promise<void> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// 防止重复加载
|
2026-03-31 18:22:24 +08:00
|
|
|
|
if (store.isLoadingMessages(conversationId)) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setLoadingMessages(conversationId, true);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessagesAtStart = store.getMessages(conversationId);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const hasInMemoryMessages = existingMessagesAtStart.length > 0;
|
|
|
|
|
|
let baselineMaxSeq = hasInMemoryMessages
|
|
|
|
|
|
? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0)
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (!afterSeq) {
|
|
|
|
|
|
// 先从本地数据库加载
|
|
|
|
|
|
if (!hasInMemoryMessages) {
|
|
|
|
|
|
try {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const localMessages = await messageRepository.getByConversation(conversationId, 20);
|
|
|
|
|
|
const localMaxSeq = await messageRepository.getMaxSeq(conversationId);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
baselineMaxSeq = localMaxSeq;
|
|
|
|
|
|
|
|
|
|
|
|
if (localMessages.length > 0) {
|
|
|
|
|
|
const formattedMessages: MessageResponse[] = localMessages.map(m => ({
|
|
|
|
|
|
id: m.id,
|
|
|
|
|
|
conversation_id: m.conversationId,
|
|
|
|
|
|
sender_id: m.senderId,
|
|
|
|
|
|
seq: m.seq,
|
|
|
|
|
|
segments: m.segments || [],
|
|
|
|
|
|
status: m.status as any,
|
|
|
|
|
|
created_at: m.createdAt,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setMessages(conversationId, formattedMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
} else {
|
2026-03-31 19:15:13 +08:00
|
|
|
|
// 冷启动兜底:先设置空列表
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setMessages(conversationId, []);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('[MessageSyncService] 读取本地消息失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// 服务端快照 + 增量同步
|
|
|
|
|
|
try {
|
|
|
|
|
|
const snapshotResp = await messageService.getMessages(conversationId, undefined, undefined, 50);
|
|
|
|
|
|
const snapshotMessages = snapshotResp?.messages || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (snapshotMessages.length > 0) {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessages = store.getMessages(conversationId);
|
|
|
|
|
|
const mergedSnapshot = mergeMessagesById(existingMessages, snapshotMessages);
|
|
|
|
|
|
store.setMessages(conversationId, mergedSnapshot);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-04-12 18:14:29 +08:00
|
|
|
|
messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(snapshotMessages, conversationId)
|
|
|
|
|
|
).catch(error => {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 保存快照消息到本地失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 增量补齐
|
|
|
|
|
|
const snapshotMaxSeq = snapshotMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0);
|
|
|
|
|
|
if (snapshotMaxSeq > baselineMaxSeq) {
|
|
|
|
|
|
const incrementalResp = await messageService.getMessages(conversationId, baselineMaxSeq);
|
|
|
|
|
|
const newMessages = incrementalResp?.messages || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (newMessages.length > 0) {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessages = store.getMessages(conversationId);
|
|
|
|
|
|
const mergedMessages = mergeMessagesById(existingMessages, newMessages);
|
|
|
|
|
|
store.setMessages(conversationId, mergedMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-04-12 18:14:29 +08:00
|
|
|
|
messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(newMessages, conversationId)
|
|
|
|
|
|
).catch(error => {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 保存增量消息到本地失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[MessageSyncService] 快照/增量同步失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 指定了 afterSeq
|
|
|
|
|
|
const response = await messageService.getMessages(conversationId, afterSeq);
|
|
|
|
|
|
|
|
|
|
|
|
if (response?.messages && response.messages.length > 0) {
|
|
|
|
|
|
const newMessages = response.messages;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessages = store.getMessages(conversationId);
|
|
|
|
|
|
const mergedMessages = mergeMessagesById(existingMessages, newMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setMessages(conversationId, mergedMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-04-12 18:14:29 +08:00
|
|
|
|
messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(newMessages, conversationId)
|
|
|
|
|
|
).catch(error => {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
console.error('[MessageSyncService] 保存消息到本地失败:', error);
|
|
|
|
|
|
});
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} catch (error) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 获取消息失败:', error);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} finally {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 异步填充用户信息(不阻塞消息显示)
|
|
|
|
|
|
const currentMessages = store.getMessages(conversationId);
|
|
|
|
|
|
if (currentMessages.length > 0) {
|
|
|
|
|
|
this.userCacheService.enrichMessagesWithSenderInfo(
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
currentMessages,
|
|
|
|
|
|
(convId, enrichedMessages) => {
|
|
|
|
|
|
store.setMessages(convId, enrichedMessages);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
store.setLoadingMessages(conversationId, false);
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载更多历史消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
|
2026-03-18 12:11:49 +08:00
|
|
|
|
try {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// 先从本地获取
|
2026-04-04 08:01:45 +08:00
|
|
|
|
const localMessages = await messageRepository.getBeforeSeq(conversationId, beforeSeq, limit);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
if (localMessages.length >= limit) {
|
|
|
|
|
|
const formattedMessages: MessageResponse[] = [...localMessages].reverse().map(m => ({
|
|
|
|
|
|
id: m.id,
|
|
|
|
|
|
conversation_id: m.conversationId,
|
|
|
|
|
|
sender_id: m.senderId,
|
|
|
|
|
|
seq: m.seq,
|
|
|
|
|
|
segments: m.segments || [],
|
|
|
|
|
|
status: m.status as any,
|
|
|
|
|
|
created_at: m.createdAt,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessages = store.getMessages(conversationId);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setMessages(conversationId, mergedMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
return formattedMessages;
|
|
|
|
|
|
}
|
2026-03-18 12:11:49 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
// 本地数据不足,从服务端获取
|
|
|
|
|
|
const response = await messageService.getMessages(conversationId, undefined, beforeSeq, limit);
|
|
|
|
|
|
|
|
|
|
|
|
if (response?.messages && response.messages.length > 0) {
|
|
|
|
|
|
const serverMessages = response.messages;
|
|
|
|
|
|
|
2026-04-12 18:14:29 +08:00
|
|
|
|
await messageRepository.saveMessagesBatch(
|
|
|
|
|
|
MessageMapper.toCachedMessages(serverMessages, conversationId)
|
|
|
|
|
|
);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const existingMessages = store.getMessages(conversationId);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setMessages(conversationId, mergedMessages);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
return serverMessages;
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return [];
|
2026-03-18 12:11:49 +08:00
|
|
|
|
} catch (error) {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 加载更多消息失败:', error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-12 01:28:46 +08:00
|
|
|
|
* 获取未读数(去重:复用 in-flight promise)
|
2026-03-31 16:13:24 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async fetchUnreadCount(): Promise<void> {
|
2026-05-12 01:28:46 +08:00
|
|
|
|
if (this.fetchUnreadCountPromise) {
|
|
|
|
|
|
return this.fetchUnreadCountPromise;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.fetchUnreadCountPromise = this.doFetchUnreadCount();
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.fetchUnreadCountPromise;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.fetchUnreadCountPromise = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async doFetchUnreadCount(): Promise<void> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
2026-05-12 01:28:46 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const [unreadData, systemUnreadData] = await Promise.all([
|
|
|
|
|
|
messageService.getUnreadCount(),
|
|
|
|
|
|
messageService.getSystemUnreadCount(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const totalUnread = unreadData?.total_unread_count ?? 0;
|
|
|
|
|
|
const systemUnread = systemUnreadData?.unread_count ?? 0;
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
store.setUnreadCount(totalUnread, systemUnread);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
|
2026-04-25 00:39:40 +08:00
|
|
|
|
// 获取最后一条系统通知的时间
|
|
|
|
|
|
if (systemUnread > 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const latestSystemMsg = await messageService.getSystemMessagesCursor({ cursor: '', page_size: 1 });
|
|
|
|
|
|
if (latestSystemMsg?.list?.[0]?.created_at) {
|
|
|
|
|
|
store.setLastSystemMessageAt(latestSystemMsg.list[0].created_at);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 获取最后系统通知时间失败,不影响主流程
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:28:46 +08:00
|
|
|
|
// 服务端汇总未读为 0 时,仅在本地也一致时才清零(避免缓存竞态导致的抖动)
|
2026-03-31 16:13:24 +08:00
|
|
|
|
if (totalUnread === 0) {
|
2026-05-12 01:28:46 +08:00
|
|
|
|
const localSum = Array.from(store.getState().conversations.values())
|
|
|
|
|
|
.reduce((sum, conv) => sum + (conv.unread_count || 0), 0);
|
|
|
|
|
|
if (localSum > 0) {
|
|
|
|
|
|
// 本地有未读但服务端返回 0 → 可能是后端缓存竞态,保留本地状态
|
|
|
|
|
|
// 下一次 fetchConversations 会提供权威的 per-conversation 数据
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 本地也无未读,确认清零
|
|
|
|
|
|
const currentConversations = store.getState().conversations;
|
|
|
|
|
|
let anyCleared = false;
|
|
|
|
|
|
const newConversations = new Map(currentConversations);
|
|
|
|
|
|
for (const [cid, conv] of newConversations) {
|
|
|
|
|
|
if ((conv.unread_count || 0) > 0) {
|
|
|
|
|
|
newConversations.set(cid, { ...conv, unread_count: 0 });
|
|
|
|
|
|
anyCleared = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (anyCleared) {
|
|
|
|
|
|
store.setConversations(newConversations);
|
|
|
|
|
|
this.persistConversationListCache();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[MessageSyncService] 获取未读数失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否可加载更多会话
|
|
|
|
|
|
*/
|
|
|
|
|
|
canLoadMoreConversations(): boolean {
|
|
|
|
|
|
return this.remoteConversationListSource.hasMore;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重启数据源
|
|
|
|
|
|
*/
|
|
|
|
|
|
restartSources(): void {
|
|
|
|
|
|
this.remoteConversationListSource.restart();
|
|
|
|
|
|
this.localConversationListSource.restart();
|
|
|
|
|
|
this.loadingMoreConversations = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 私有工具方法 ====================
|
|
|
|
|
|
|
|
|
|
|
|
private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
if (!store.getActiveConversation()) return false;
|
2026-03-31 16:13:24 +08:00
|
|
|
|
if (!forceRefresh) return false;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// hooks-initial-refresh 是冷启动兜底刷新,不应该被延迟
|
|
|
|
|
|
return source === 'sse-reconnect' || source === 'prefetch';
|
2026-03-18 12:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const id = normalizeConversationId(conv.id);
|
|
|
|
|
|
// 应用已读保护逻辑
|
|
|
|
|
|
return this.readReceiptManager.applyConversationFromFetch({
|
2026-03-31 16:13:24 +08:00
|
|
|
|
...conv,
|
|
|
|
|
|
id,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private recomputeConversationTotalUnread(): void {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
const totalUnread = Array.from(store.getState().conversations.values()).reduce(
|
2026-03-31 16:13:24 +08:00
|
|
|
|
(sum, conv) => sum + (conv.unread_count || 0),
|
|
|
|
|
|
0
|
|
|
|
|
|
);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 使用 zustand set 函数正确更新状态
|
|
|
|
|
|
store.setUnreadCount(totalUnread, store.getUnreadCount().system);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private persistConversationListCache(): void {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
|
|
|
|
|
const currentState = store.getState();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
const list = Array.from(currentState.conversations.values());
|
|
|
|
|
|
const users = list.flatMap(conv => [
|
|
|
|
|
|
...(conv.participants || []),
|
|
|
|
|
|
...(conv.last_message?.sender ? [conv.last_message.sender] : []),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const groups = list
|
|
|
|
|
|
.map(conv => conv.group)
|
|
|
|
|
|
.filter((group): group is NonNullable<ConversationResponse['group']> => Boolean(group));
|
|
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
conversationRepository.saveWithRelated(list, users, groups).catch(error => {
|
2026-03-31 16:13:24 +08:00
|
|
|
|
console.error('[MessageSyncService] 持久化会话列表失败:', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async hydrateConversationsFromLocalSource(): Promise<boolean> {
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const store = useMessageStore.getState();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
this.localConversationListSource.restart();
|
|
|
|
|
|
const page = await this.localConversationListSource.loadNext();
|
|
|
|
|
|
if (!page.items.length) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 使用 zustand set 函数正确更新状态
|
|
|
|
|
|
const newConversations = new Map<string, ConversationResponse>();
|
2026-03-31 16:13:24 +08:00
|
|
|
|
page.items.forEach(conv => {
|
|
|
|
|
|
const normalizedConv = this.normalizeConversationFromFetch(conv);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
newConversations.set(normalizedConv.id, normalizedConv);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-12 01:28:46 +08:00
|
|
|
|
// 合并更新:conversations + totalUnread 在同一次 set 中完成
|
2026-03-31 18:22:24 +08:00
|
|
|
|
const totalUnread = Array.from(newConversations.values()).reduce(
|
|
|
|
|
|
(sum, conv) => sum + (conv.unread_count || 0),
|
2026-05-12 01:28:46 +08:00
|
|
|
|
0,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
);
|
2026-05-12 01:28:46 +08:00
|
|
|
|
store.setConversationsWithUnread(newConversations, totalUnread, store.getUnreadCount().system);
|
2026-03-31 18:22:24 +08:00
|
|
|
|
|
2026-03-31 16:13:24 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
|
|
|
|
|
|
if (incoming.length === 0) return existing;
|
|
|
|
|
|
|
|
|
|
|
|
const incomingAsc = [...incoming].sort((a, b) => a.seq - b.seq);
|
|
|
|
|
|
if (existing.length === 0) return incomingAsc;
|
|
|
|
|
|
|
|
|
|
|
|
const existingMinSeq = existing[0]?.seq ?? Number.MAX_SAFE_INTEGER;
|
|
|
|
|
|
const incomingMaxSeq = incomingAsc[incomingAsc.length - 1]?.seq ?? Number.MIN_SAFE_INTEGER;
|
|
|
|
|
|
|
|
|
|
|
|
if (incomingMaxSeq < existingMinSeq) {
|
|
|
|
|
|
return [...incomingAsc, ...existing];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
return mergeMessagesById(existing, incomingAsc);
|
2026-03-31 16:13:24 +08:00
|
|
|
|
}
|
2026-03-31 18:22:24 +08:00
|
|
|
|
}
|