refactor(post): consolidate post sync to PostSyncService and remove ProcessPostUseCase
- Replace ProcessPostUseCase with new PostSyncService in src/services/post/ - Add postListStore for state management in src/stores/post/ - Remove deprecated ProcessPostUseCase and ProcessMessageUseCase files - Delete architecture documentation files (ARCHITECTURE_REFACTOR_PLAN.md, architecture-comparison-report.md) - Update all consumers (HomeScreen, PostDetailScreen, SearchScreen, useUserProfile, useDifferentialPosts) - Simplify postService to only contain API layer methods - Remove unused type exports from message stores BREAKING CHANGE: ProcessPostUseCase and ProcessMessageUseCase have been removed. Use postSyncService for post operations instead.
This commit is contained in:
@@ -1,689 +0,0 @@
|
||||
/**
|
||||
* ProcessMessageUseCase - 处理消息用例
|
||||
* 处理消息的业务逻辑,协调 Repository 和 SSEClient
|
||||
* 纯业务逻辑,无UI依赖
|
||||
*/
|
||||
|
||||
import { messageRepository, userCacheRepository, CachedMessage } from '@/database';
|
||||
import { wsClient, WSEventType } from '../../data/datasources/WSClient';
|
||||
import { MessageMapper } from '@/data/mappers';
|
||||
import {
|
||||
Message,
|
||||
Conversation,
|
||||
GroupNotice,
|
||||
createMessage,
|
||||
isMessageFromCurrentUser,
|
||||
shouldIncrementUnread,
|
||||
buildGroupNoticeText,
|
||||
} from '../entities/Message';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
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'],
|
||||
});
|
||||
|
||||
// 事件类型定义
|
||||
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;
|
||||
clearTimer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
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;
|
||||
this.setupWSListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁用例
|
||||
*/
|
||||
destroy(): void {
|
||||
this.unsubscribeFns.forEach((fn) => fn());
|
||||
this.unsubscribeFns = [];
|
||||
this.subscribers.clear();
|
||||
this.processedMessageIds.clear();
|
||||
this.processedMessageIdsExpiry.clear();
|
||||
this.pendingReadMap.clear();
|
||||
this.pendingUserRequests.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SSE监听器
|
||||
*/
|
||||
private setupWSListeners(): void {
|
||||
// 监听私聊消息
|
||||
const unsubChat = wsClient.on('chat', (message) => {
|
||||
this.handleNewMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊消息
|
||||
const unsubGroupMessage = wsClient.on('group_message', (message) => {
|
||||
this.handleNewMessage(message);
|
||||
});
|
||||
|
||||
// 监听私聊已读回执
|
||||
const unsubRead = wsClient.on('read', (message) => {
|
||||
this.handleReadReceipt(message);
|
||||
});
|
||||
|
||||
// 监听群聊已读回执
|
||||
const unsubGroupRead = wsClient.on('group_read', (message) => {
|
||||
this.handleGroupReadReceipt(message);
|
||||
});
|
||||
|
||||
// 监听私聊消息撤回
|
||||
const unsubRecall = wsClient.on('recall', (message) => {
|
||||
this.handleRecallMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊消息撤回
|
||||
const unsubGroupRecall = wsClient.on('group_recall', (message) => {
|
||||
this.handleGroupRecallMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊输入状态
|
||||
const unsubGroupTyping = wsClient.on('group_typing', (message) => {
|
||||
this.handleGroupTyping(message);
|
||||
});
|
||||
|
||||
// 监听群通知
|
||||
const unsubGroupNotice = wsClient.on('group_notice', (message) => {
|
||||
this.handleGroupNotice(message);
|
||||
});
|
||||
|
||||
// 监听连接状态
|
||||
const unsubConnected = wsClient.on('connected', () => {
|
||||
this.notifySubscribers({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: true },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
const unsubDisconnected = wsClient.on('disconnected', () => {
|
||||
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;
|
||||
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) => {
|
||||
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> {
|
||||
// 先检查本地缓存
|
||||
const cachedUser = await userCacheRepository.get(userId);
|
||||
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 {
|
||||
const response = await api.get<any>(`/users/${userId}`);
|
||||
if (response.code === 0 && response.data) {
|
||||
await userCacheRepository.save(response.data as any);
|
||||
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
|
||||
.updateStatus(message_id, 'recalled', true)
|
||||
.catch((error: unknown) => {
|
||||
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
|
||||
.updateStatus(message_id, 'recalled', true)
|
||||
.catch((error: unknown) => {
|
||||
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(),
|
||||
messageId: message_id,
|
||||
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(),
|
||||
});
|
||||
|
||||
// 保存到本地数据库
|
||||
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,
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 发送消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地消息
|
||||
*/
|
||||
async getLocalMessages(conversationId: string, limit: number = 20): Promise<Message[]> {
|
||||
const cached = await messageRepository.getByConversation(conversationId, limit);
|
||||
return cached.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史消息
|
||||
*/
|
||||
async getHistoryMessages(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
// 先从本地获取
|
||||
const localMessages = await messageRepository.getBeforeSeq(
|
||||
conversationId,
|
||||
beforeSeq,
|
||||
limit
|
||||
);
|
||||
|
||||
if (localMessages.length >= limit) {
|
||||
return localMessages.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
// 本地数据不足,从服务端获取
|
||||
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',
|
||||
})
|
||||
);
|
||||
|
||||
// 保存到本地
|
||||
await messageRepository.saveMessagesBatch(
|
||||
MessageMapper.toCachedMessages(serverMessages, conversationId)
|
||||
);
|
||||
|
||||
return serverMessages;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 获取历史消息失败:', error);
|
||||
}
|
||||
|
||||
return localMessages.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务端同步消息
|
||||
*/
|
||||
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',
|
||||
})
|
||||
);
|
||||
|
||||
await messageRepository.saveMessagesBatch(
|
||||
MessageMapper.toCachedMessages(response.messages, conversationId)
|
||||
);
|
||||
|
||||
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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新
|
||||
* 集成 ProcessPostUseCase 进行状态管理
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
PostIdentifier,
|
||||
@@ -20,98 +14,47 @@ import {
|
||||
PostBatcherOptions,
|
||||
createPostUpdateBatcher,
|
||||
} from '../infrastructure/diff/PostUpdateBatcher';
|
||||
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
|
||||
import type { PostUseCaseEvent, PostsState } from '../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../services/post/PostSyncService';
|
||||
import type { PostsState } from '../services/post/PostSyncService';
|
||||
import { usePostListStore } from '../stores/post/postListStore';
|
||||
import type { Post } from '../core/entities/Post';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 差异更新 Hook 配置选项
|
||||
*/
|
||||
export interface UseDifferentialPostsOptions<T extends PostIdentifier> {
|
||||
/** 差异计算配置 */
|
||||
diffConfig?: PostDiffConfig;
|
||||
/** 批量处理配置 */
|
||||
batcherOptions?: PostBatcherOptions;
|
||||
/** 是否启用差异计算,默认 true */
|
||||
enableDiff?: boolean;
|
||||
/** 是否启用批量处理,默认 true */
|
||||
enableBatching?: boolean;
|
||||
/** 最大帖子数量限制,超出时触发重置 */
|
||||
maxPostCount?: number;
|
||||
/** 变化比例阈值(0-1),超过此值触发重置 */
|
||||
changeRatioThreshold?: number;
|
||||
/** 列表键(用于区分不同的帖子列表) */
|
||||
listKey?: string;
|
||||
/** 是否自动订阅 UseCase 状态变化,默认 true */
|
||||
autoSubscribe?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异更新信息
|
||||
*/
|
||||
export interface DiffUpdatesInfo {
|
||||
/** 新增数量 */
|
||||
addedCount: number;
|
||||
/** 更新数量 */
|
||||
updatedCount: number;
|
||||
/** 删除数量 */
|
||||
deletedCount: number;
|
||||
/** 上次更新时间 */
|
||||
lastUpdateTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异更新 Hook 返回值
|
||||
*/
|
||||
export interface UseDifferentialPostsResult<T extends PostIdentifier> {
|
||||
/** 优化后的帖子列表 */
|
||||
posts: T[];
|
||||
/** 是否正在加载 */
|
||||
loading: boolean;
|
||||
/** 是否首屏加载 */
|
||||
isInitialLoading: boolean;
|
||||
/** 是否正在加载更多 */
|
||||
isLoadingMore: boolean;
|
||||
/** 是否正在刷新 */
|
||||
refreshing: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 待处理更新数量 */
|
||||
pendingUpdateCount: number;
|
||||
/** 是否正在处理更新 */
|
||||
isProcessing: boolean;
|
||||
/** 刷新方法 */
|
||||
refresh: () => Promise<void>;
|
||||
/** 加载更多方法 */
|
||||
loadMore: () => Promise<void>;
|
||||
/** 手动刷新批量处理队列 */
|
||||
flush: () => void;
|
||||
/** 重置方法 */
|
||||
reset: () => void;
|
||||
/** 强制更新(跳过批量处理) */
|
||||
forceUpdate: (posts: T[]) => void;
|
||||
/** 获取差异统计信息 */
|
||||
getDiffStats: () => {
|
||||
totalBatches: number;
|
||||
totalUpdates: number;
|
||||
averageBatchSize: number;
|
||||
};
|
||||
/** 差异更新信息 */
|
||||
getDiffStats: () => { totalBatches: number; totalUpdates: number; averageBatchSize: number };
|
||||
diffUpdates: DiffUpdatesInfo;
|
||||
}
|
||||
|
||||
// ==================== Hook 实现 ====================
|
||||
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* @param initialPosts 初始帖子列表(可选)
|
||||
* @param options 配置选项
|
||||
* @returns 优化后的帖子列表和相关方法
|
||||
*/
|
||||
export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
initialPosts: T[] = [],
|
||||
options: UseDifferentialPostsOptions<T> = {}
|
||||
@@ -126,8 +69,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
autoSubscribe = true,
|
||||
} = options;
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
const [posts, setPosts] = useState<T[]>(initialPosts);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -142,23 +83,15 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
lastUpdateTime: 0,
|
||||
});
|
||||
|
||||
// ==================== Refs ====================
|
||||
|
||||
const calculatorRef = useRef<PostDiffCalculator<T> | null>(null);
|
||||
const batcherRef = useRef<PostUpdateBatcher | null>(null);
|
||||
const previousPostsRef = useRef<T[]>(initialPosts);
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)====================
|
||||
|
||||
/**
|
||||
* 处理批量更新
|
||||
*/
|
||||
const handleBatchUpdates = useCallback((updates: PostUpdate[]) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
setPosts((currentPosts) => {
|
||||
setPosts(currentPosts => {
|
||||
let newPosts = [...currentPosts];
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
@@ -210,9 +143,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
const deleteUpdate = update as any;
|
||||
const prevLength = newPosts.length;
|
||||
newPosts = newPosts.filter(p => p.id !== deleteUpdate.postId);
|
||||
if (newPosts.length < prevLength) {
|
||||
deletedCount++;
|
||||
}
|
||||
if (newPosts.length < prevLength) deletedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -243,7 +174,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}
|
||||
|
||||
// 更新差异统计
|
||||
if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
|
||||
setDiffUpdates(prev => ({
|
||||
addedCount: prev.addedCount + addedCount,
|
||||
@@ -261,28 +191,17 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
// 初始化差异计算器
|
||||
useEffect(() => {
|
||||
if (enableDiff) {
|
||||
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
|
||||
}
|
||||
return () => {
|
||||
calculatorRef.current = null;
|
||||
};
|
||||
return () => { calculatorRef.current = null; };
|
||||
}, [enableDiff, diffConfig]);
|
||||
|
||||
// 初始化批量处理器
|
||||
useEffect(() => {
|
||||
if (!enableBatching) return;
|
||||
|
||||
batcherRef.current = createPostUpdateBatcher(batcherOptions);
|
||||
|
||||
const unsubscribe = batcherRef.current.subscribe((updates) => {
|
||||
handleBatchUpdates(updates);
|
||||
});
|
||||
|
||||
const unsubscribe = batcherRef.current.subscribe(handleBatchUpdates);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
batcherRef.current?.destroy();
|
||||
@@ -290,144 +209,46 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
};
|
||||
}, [enableBatching, batcherOptions, handleBatchUpdates]);
|
||||
|
||||
// ==================== UseCase 订阅 ====================
|
||||
const syncFromStore = useCallback((state: PostsState) => {
|
||||
setLoading(state.isLoading);
|
||||
setRefreshing(state.isRefreshing);
|
||||
setError(state.error);
|
||||
setHasMore(state.hasMore);
|
||||
|
||||
/**
|
||||
* 处理 UseCase 事件
|
||||
*/
|
||||
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
|
||||
switch (event.type) {
|
||||
case 'state_changed': {
|
||||
const { key: eventKey, state } = event.payload as { key: string; state: PostsState };
|
||||
// 全局订阅:只应用当前列表 key,避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key)
|
||||
if (eventKey !== listKey) {
|
||||
break;
|
||||
}
|
||||
if (state) {
|
||||
setLoading(state.isLoading);
|
||||
setRefreshing(state.isRefreshing);
|
||||
setError(state.error);
|
||||
setHasMore(state.hasMore);
|
||||
|
||||
// ProcessPostUseCase 的 state.posts 已是合并后的完整列表。
|
||||
// 不再走差异计算 + PostUpdateBatcher:批量应用晚于 previousPostsRef 更新时,
|
||||
// BATCH_ADD 会在过短的 currentPosts 上 splice,导致连续游标加载「请求成功但列表不增长」。
|
||||
batcherRef.current?.clearPending();
|
||||
calculatorRef.current?.reset();
|
||||
if (state.posts && state.posts.length > 0) {
|
||||
const next = state.posts as unknown as T[];
|
||||
if (next.length > maxPostCount) {
|
||||
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
|
||||
}
|
||||
setPosts(next);
|
||||
previousPostsRef.current = next;
|
||||
} else {
|
||||
setPosts([]);
|
||||
previousPostsRef.current = [];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'posts_loaded': {
|
||||
// fetchPosts 在 updatePostsState 时已发出 state_changed(含合并后 posts),此处不再应用,避免与其它 key 竞态或重复计算
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_created': {
|
||||
const { post } = event.payload;
|
||||
if (post) {
|
||||
// 新帖子添加到列表开头
|
||||
setPosts(prev => [post as T, ...prev]);
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
addedCount: prev.addedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_updated': {
|
||||
const { post } = event.payload;
|
||||
if (post) {
|
||||
setPosts(prev => prev.map(p => p.id === post.id ? { ...p, ...post } as T : p));
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
updatedCount: prev.updatedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_deleted': {
|
||||
const { postId } = event.payload;
|
||||
if (postId) {
|
||||
setPosts(prev => prev.filter(p => p.id !== postId));
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
deletedCount: prev.deletedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loading_changed': {
|
||||
const { isLoading } = event.payload;
|
||||
setLoading(isLoading);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error_occurred': {
|
||||
const payload = event.payload as { key?: string; error?: string };
|
||||
if (payload.key !== undefined && payload.key !== listKey) {
|
||||
break;
|
||||
}
|
||||
if (payload.error !== undefined) {
|
||||
setError(payload.error);
|
||||
}
|
||||
break;
|
||||
batcherRef.current?.clearPending();
|
||||
calculatorRef.current?.reset();
|
||||
if (state.posts && state.posts.length > 0) {
|
||||
const next = state.posts as unknown as T[];
|
||||
if (next.length > maxPostCount) {
|
||||
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
|
||||
}
|
||||
setPosts(next);
|
||||
previousPostsRef.current = next;
|
||||
} else {
|
||||
setPosts([]);
|
||||
previousPostsRef.current = [];
|
||||
}
|
||||
}, [listKey, maxPostCount]);
|
||||
}, [maxPostCount]);
|
||||
|
||||
// 订阅 UseCase 状态变化
|
||||
useEffect(() => {
|
||||
if (autoSubscribe) {
|
||||
unsubscribeRef.current = processPostUseCase.subscribe(handleUseCaseEvent);
|
||||
if (!autoSubscribe) return;
|
||||
|
||||
// 初始化时获取当前状态
|
||||
const currentState = processPostUseCase.getPostsState(listKey);
|
||||
if (currentState.posts.length > 0) {
|
||||
setPosts(currentState.posts as unknown as T[]);
|
||||
previousPostsRef.current = currentState.posts as unknown as T[];
|
||||
}
|
||||
setLoading(currentState.isLoading);
|
||||
setRefreshing(currentState.isRefreshing);
|
||||
setError(currentState.error);
|
||||
setHasMore(currentState.hasMore);
|
||||
const currentState = usePostListStore.getState().getPostsState(listKey);
|
||||
syncFromStore(currentState);
|
||||
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [autoSubscribe, listKey, handleUseCaseEvent]);
|
||||
const unsubscribe = usePostListStore.subscribe(state => {
|
||||
const postsState = state.postsStateMap.get(listKey);
|
||||
if (postsState) syncFromStore(postsState);
|
||||
});
|
||||
|
||||
// ==================== 操作方法 ====================
|
||||
return unsubscribe;
|
||||
}, [autoSubscribe, listKey, syncFromStore]);
|
||||
|
||||
/**
|
||||
* 刷新帖子列表
|
||||
*/
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.refreshPosts(listKey);
|
||||
await postSyncService.refreshPosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '刷新失败';
|
||||
setError(errorMsg);
|
||||
@@ -436,15 +257,12 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, [listKey]);
|
||||
|
||||
/**
|
||||
* 加载更多帖子
|
||||
*/
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !loading) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
await postSyncService.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '加载更多失败';
|
||||
setError(errorMsg);
|
||||
@@ -454,16 +272,8 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, [listKey, hasMore, loading]);
|
||||
|
||||
/**
|
||||
* 手动刷新批量处理队列
|
||||
*/
|
||||
const flush = useCallback(() => {
|
||||
batcherRef.current?.flush();
|
||||
}, []);
|
||||
const flush = useCallback(() => { batcherRef.current?.flush(); }, []);
|
||||
|
||||
/**
|
||||
* 重置方法
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
calculatorRef.current?.reset();
|
||||
batcherRef.current?.clearPending();
|
||||
@@ -473,25 +283,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
setError(null);
|
||||
setHasMore(true);
|
||||
previousPostsRef.current = [];
|
||||
setDiffUpdates({
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
lastUpdateTime: 0,
|
||||
});
|
||||
setDiffUpdates({ addedCount: 0, updatedCount: 0, deletedCount: 0, lastUpdateTime: 0 });
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 强制更新方法
|
||||
*/
|
||||
const forceUpdate = useCallback((newPosts: T[]) => {
|
||||
setPosts(newPosts);
|
||||
previousPostsRef.current = newPosts;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
const getDiffStats = useCallback(() => {
|
||||
const batcherStats = batcherRef.current?.getStats();
|
||||
return {
|
||||
@@ -501,20 +300,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ==================== 定期更新待处理数量 ====================
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableBatching || !batcherRef.current) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [enableBatching]);
|
||||
|
||||
// ==================== 返回值 ====================
|
||||
|
||||
return {
|
||||
posts,
|
||||
loading,
|
||||
|
||||
@@ -34,7 +34,7 @@ import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { SearchScreen } from './SearchScreen';
|
||||
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
@@ -375,7 +375,7 @@ export const HomeScreen: React.FC = () => {
|
||||
isLoadingMoreRef.current = true;
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
await postSyncService.loadMorePosts(listKey);
|
||||
console.log('[PostPerf] home loadMore', {
|
||||
listKey,
|
||||
costMs: Date.now() - startedAt,
|
||||
@@ -409,7 +409,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
// 先获取正确类型的帖子
|
||||
await processPostUseCase.fetchPosts(
|
||||
await postSyncService.fetchPosts(
|
||||
{
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
@@ -554,9 +554,9 @@ export const HomeScreen: React.FC = () => {
|
||||
const handleLike = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
await postSyncService.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
await postSyncService.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
@@ -567,9 +567,9 @@ export const HomeScreen: React.FC = () => {
|
||||
const handleBookmark = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
await postSyncService.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
await postSyncService.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
@@ -582,7 +582,7 @@ export const HomeScreen: React.FC = () => {
|
||||
try {
|
||||
const res = await postService.sharePost(post.id, { channel: 'copy_link' });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
processPostUseCase.applyShareCountUpdate(post.id, res.shares_count);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (shareError) {
|
||||
console.error('上报分享次数失败:', shareError);
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard, ReportDialog } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
|
||||
@@ -238,7 +238,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
// 使用 ProcessPostUseCase 获取帖子详情
|
||||
const postData = await processPostUseCase.fetchPostById(postId);
|
||||
const postData = await postSyncService.fetchPostById(postId);
|
||||
if (postData) {
|
||||
// 类型转换:将 core/entities/Post 转换为 PostDTO 以保持兼容性
|
||||
setPost(postData as unknown as Post);
|
||||
@@ -487,9 +487,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (originalPost.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
await postSyncService.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
await postSyncService.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, { context: '点赞' });
|
||||
@@ -505,9 +505,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (originalPost.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
await postSyncService.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
await postSyncService.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, { context: '收藏' });
|
||||
@@ -526,7 +526,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
? { ...prev, shares_count: res.shares_count!, sharesCount: res.shares_count! }
|
||||
: null
|
||||
);
|
||||
processPostUseCase.applyShareCountUpdate(post.id, res.shares_count);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
@@ -635,7 +635,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
onPress: async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await processPostUseCase.deletePost(post.id);
|
||||
await postSyncService.deletePost(post.id);
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
|
||||
@@ -20,6 +20,7 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
|
||||
import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { postService, authService } from '../../services';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
@@ -185,19 +186,19 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
postService.likePost(post.id);
|
||||
postSyncService.likePost(post.id);
|
||||
break;
|
||||
case 'unlike':
|
||||
postService.unlikePost(post.id);
|
||||
postSyncService.unlikePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
postService.favoritePost(post.id);
|
||||
postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'unbookmark':
|
||||
postService.unfavoritePost(post.id);
|
||||
postSyncService.unfavoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
// 搜索页暂不处理分享
|
||||
|
||||
@@ -862,11 +862,9 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
// 容器
|
||||
segmentsContainer: {
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
},
|
||||
segmentsColumn: {
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
},
|
||||
inlineChunkBase: {
|
||||
fontSize,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, authService, messageService } from '../../services';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { PostCardAction } from '../../components/business/PostCard';
|
||||
@@ -310,16 +311,23 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
|
||||
handleUserPress(post.author.id);
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
case 'like':
|
||||
case 'unlike':
|
||||
post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id);
|
||||
post.is_liked ? postSyncService.unlikePost(post.id) : postSyncService.likePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id);
|
||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
break;
|
||||
|
||||
346
src/services/post/PostSyncService.ts
Normal file
346
src/services/post/PostSyncService.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { postRepository } from '../../data/repositories/PostRepository';
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
import type {
|
||||
GetPostsParams,
|
||||
CreatePostData,
|
||||
UpdatePostData,
|
||||
PostsResult,
|
||||
SearchPostsParams,
|
||||
} from '../../data/repositories/interfaces/IPostRepository';
|
||||
import { PaginationStateManager } from '../../infrastructure/pagination/PaginationStateManager';
|
||||
import type { PaginationState } from '../../infrastructure/pagination/types';
|
||||
import { usePostListStore, type PostsState } from '../../stores/post/postListStore';
|
||||
import { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';
|
||||
|
||||
export type { PostsState } from '../../stores/post/postListStore';
|
||||
|
||||
export interface PostDetailState {
|
||||
post: Post | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
const MERGE_PERF_WARN_THRESHOLD_MS = 12;
|
||||
|
||||
class PostSyncService {
|
||||
private static instance: PostSyncService;
|
||||
private paginationManager: PaginationStateManager;
|
||||
private currentUserId: string | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.paginationManager = new PaginationStateManager();
|
||||
}
|
||||
|
||||
static getInstance(): PostSyncService {
|
||||
if (!PostSyncService.instance) {
|
||||
PostSyncService.instance = new PostSyncService();
|
||||
}
|
||||
return PostSyncService.instance;
|
||||
}
|
||||
|
||||
initialize(currentUserId: string | null = null): void {
|
||||
this.currentUserId = currentUserId;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
usePostListStore.getState().clearAllStates();
|
||||
this.paginationManager.clear();
|
||||
this.currentUserId = null;
|
||||
}
|
||||
|
||||
getPostsState(key: string = 'default'): PostsState {
|
||||
return usePostListStore.getState().getPostsState(key);
|
||||
}
|
||||
|
||||
getPostDetailState(postId: string): PostDetailState {
|
||||
return usePostListStore.getState().getPostDetailState(postId);
|
||||
}
|
||||
|
||||
applyShareCountUpdate(postId: string, sharesCount: number): void {
|
||||
usePostListStore.getState().applyShareCountUpdate(postId, sharesCount);
|
||||
}
|
||||
|
||||
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
||||
const store = usePostListStore.getState();
|
||||
store.updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const queryParams: GetPostsParams = {
|
||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||
...params,
|
||||
cursor: params?.cursor !== undefined ? params.cursor : '',
|
||||
};
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
const currentState = usePostListStore.getState().getPostsState(key);
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const mergedPosts = mergeRefreshWindow(currentState.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
|
||||
}
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: mergedPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: isCursorMode ? result.nextCursor : null,
|
||||
currentPage: isCursorMode ? undefined : 1,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
lastParams: params,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
||||
const state = usePostListStore.getState().getPostsState(key);
|
||||
usePostListStore.getState().updatePostsState(key, { isRefreshing: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.getPosts({
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
...state.lastParams,
|
||||
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '',
|
||||
});
|
||||
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const mergedPosts = mergeRefreshWindow(state.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
|
||||
}
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: mergedPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: isCursorMode ? result.nextCursor : null,
|
||||
currentPage: isCursorMode ? undefined : 1,
|
||||
isRefreshing: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
this.paginationManager.reset(key);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '刷新帖子列表失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isRefreshing: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadMorePosts(key: string = 'default'): Promise<PostsResult> {
|
||||
const state = usePostListStore.getState().getPostsState(key);
|
||||
|
||||
if (!state.hasMore) return { posts: [], hasMore: false };
|
||||
if (state.isLoading) return { posts: [], hasMore: state.hasMore };
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
|
||||
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
|
||||
let queryParams: GetPostsParams;
|
||||
|
||||
if (useCursorMode) {
|
||||
delete base.page;
|
||||
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, cursor: state.cursor! };
|
||||
} else {
|
||||
const nextPage = (state.currentPage || 1) + 1;
|
||||
delete base.cursor;
|
||||
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, page: nextPage };
|
||||
}
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const newPosts = mergePostListWithFastPath(state.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
|
||||
}
|
||||
|
||||
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: newPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: responseIsCursorMode ? result.nextCursor : null,
|
||||
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
return { posts: incomingPosts, hasMore: result.hasMore, nextCursor: result.nextCursor };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
||||
console.error('[PostSyncService] Load more error:', errorMessage);
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPostById(id: string): Promise<Post | null> {
|
||||
usePostListStore.getState().updatePostDetailState(id, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const post = await postRepository.getPostById(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post, isLoading: false });
|
||||
return post;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败';
|
||||
usePostListStore.getState().updatePostDetailState(id, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createPost(data: CreatePostData): Promise<Post> {
|
||||
const post = await postRepository.createPost(data);
|
||||
const store = usePostListStore.getState();
|
||||
const newMap = new Map(store.postsStateMap);
|
||||
for (const [key, postsState] of newMap) {
|
||||
newMap.set(key, { ...postsState, posts: [post, ...postsState.posts] });
|
||||
}
|
||||
usePostListStore.setState({ postsStateMap: newMap });
|
||||
return post;
|
||||
}
|
||||
|
||||
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
|
||||
const post = await postRepository.updatePost(id, data);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async deletePost(id: string): Promise<void> {
|
||||
await postRepository.deletePost(id);
|
||||
usePostListStore.getState().clearPostDetailState(id);
|
||||
usePostListStore.getState().removePostFromLists(id);
|
||||
}
|
||||
|
||||
async likePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.likePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async unlikePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.unlikePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async favoritePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.favoritePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async unfavoritePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.unfavoritePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise<PostsResult> {
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.searchPosts(params);
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '搜索帖子失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPostsByUser(userId: string, params?: GetPostsParams, key?: string): Promise<PostsResult> {
|
||||
const listKey = key || `user_${userId}`;
|
||||
usePostListStore.getState().updatePostsState(listKey, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.getPostsByUser(userId, params);
|
||||
usePostListStore.getState().updatePostsState(listKey, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取用户帖子失败';
|
||||
usePostListStore.getState().updatePostsState(listKey, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
clearPostsState(key: string = 'default'): void {
|
||||
usePostListStore.getState().clearPostsState(key);
|
||||
this.paginationManager.reset(key);
|
||||
}
|
||||
|
||||
clearPostDetailState(postId: string): void {
|
||||
usePostListStore.getState().clearPostDetailState(postId);
|
||||
}
|
||||
|
||||
clearAllStates(): void {
|
||||
usePostListStore.getState().clearAllStates();
|
||||
this.paginationManager.clear();
|
||||
}
|
||||
|
||||
getCurrentUserId(): string | null {
|
||||
return this.currentUserId;
|
||||
}
|
||||
|
||||
setCurrentUserId(userId: string | null): void {
|
||||
this.currentUserId = userId;
|
||||
}
|
||||
|
||||
getPaginationState(key: string): PaginationState {
|
||||
return this.paginationManager.getState(key);
|
||||
}
|
||||
|
||||
resetPagination(key: string): void {
|
||||
this.paginationManager.reset(key);
|
||||
}
|
||||
|
||||
isLoading(key: string = 'default'): boolean {
|
||||
return usePostListStore.getState().getPostsState(key).isLoading;
|
||||
}
|
||||
|
||||
hasError(key: string = 'default'): boolean {
|
||||
return usePostListStore.getState().getPostsState(key).error !== null;
|
||||
}
|
||||
|
||||
getError(key: string = 'default'): string | null {
|
||||
return usePostListStore.getState().getPostsState(key).error;
|
||||
}
|
||||
}
|
||||
|
||||
export const postSyncService = PostSyncService.getInstance();
|
||||
export { PostSyncService };
|
||||
export default postSyncService;
|
||||
66
src/services/post/postMerge.ts
Normal file
66
src/services/post/postMerge.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
|
||||
export function getEntityId(item: unknown): string | null {
|
||||
if (!item || typeof item !== 'object') return null;
|
||||
const maybeId = (item as { id?: unknown }).id;
|
||||
if (typeof maybeId === 'string' || typeof maybeId === 'number') return String(maybeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mergePostListWithFastPath(
|
||||
previousInput: Post[] | null | undefined,
|
||||
incomingInput: Post[] | null | undefined
|
||||
): Post[] {
|
||||
const previous = Array.isArray(previousInput) ? previousInput : [];
|
||||
const incoming = Array.isArray(incomingInput) ? incomingInput : [];
|
||||
if (incoming.length === 0) return previous;
|
||||
if (previous.length === 0) return incoming;
|
||||
|
||||
const lastPrevId = getEntityId(previous[previous.length - 1]);
|
||||
const firstIncomingId = getEntityId(incoming[0]);
|
||||
if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) {
|
||||
const prevIdSet = new Set(
|
||||
previous.map(item => getEntityId(item)).filter(Boolean) as string[]
|
||||
);
|
||||
if (!prevIdSet.has(firstIncomingId)) return [...previous, ...incoming];
|
||||
}
|
||||
|
||||
const merged = [...previous];
|
||||
const indexById = new Map<string, number>();
|
||||
for (let i = 0; i < merged.length; i += 1) {
|
||||
const id = getEntityId(merged[i]);
|
||||
if (id) indexById.set(id, i);
|
||||
}
|
||||
|
||||
for (const post of incoming) {
|
||||
const id = getEntityId(post);
|
||||
if (!id) { merged.push(post); continue; }
|
||||
const existingIndex = indexById.get(id);
|
||||
if (existingIndex === undefined) {
|
||||
indexById.set(id, merged.length);
|
||||
merged.push(post);
|
||||
} else {
|
||||
merged[existingIndex] = post;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeRefreshWindow(
|
||||
previousInput: Post[] | null | undefined,
|
||||
latestInput: Post[] | null | undefined
|
||||
): Post[] {
|
||||
const previous = Array.isArray(previousInput) ? previousInput : [];
|
||||
const latest = Array.isArray(latestInput) ? latestInput : [];
|
||||
if (latest.length === 0) return previous;
|
||||
if (previous.length === 0) return latest;
|
||||
|
||||
const latestIdSet = new Set(
|
||||
latest.map(item => getEntityId(item)).filter(Boolean) as string[]
|
||||
);
|
||||
const remaining = previous.filter(item => {
|
||||
const id = getEntityId(item);
|
||||
return !id || !latestIdSet.has(id);
|
||||
});
|
||||
return [...latest, ...remaining];
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
/**
|
||||
* 帖子服务
|
||||
* 处理帖子的增删改查、点赞、收藏等功能
|
||||
*
|
||||
* @deprecated 此服务已弃用,请使用 ProcessPostUseCase 代替
|
||||
* 互动操作(点赞、收藏)已委托给 processPostUseCase
|
||||
* 帖子服务 - API 层薄封装
|
||||
* 互动操作(点赞、收藏)请使用 postSyncService
|
||||
*/
|
||||
|
||||
import { api, PaginatedData } from './api';
|
||||
import { Post, CreatePostInput } from '../types';
|
||||
import { CursorPaginationRequest, CursorPaginationResponse } from '../types/dto';
|
||||
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
|
||||
import type { Post as PostEntity } from '../core/entities/Post';
|
||||
|
||||
// 帖子列表响应
|
||||
interface PostListResponse {
|
||||
@@ -129,55 +124,6 @@ class PostService {
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.likePost() 代替
|
||||
async likePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.likePost(postId);
|
||||
// 转换为旧版 Post 类型以保持向后兼容
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] likePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.unlikePost() 代替
|
||||
async unlikePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.unlikePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unlikePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.favoritePost() 代替
|
||||
async favoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.favoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] favoritePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.unfavoritePost() 代替
|
||||
async unfavoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.unfavoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unfavoritePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户帖子列表
|
||||
async getUserPosts(
|
||||
userId: string,
|
||||
|
||||
@@ -28,9 +28,6 @@ export {
|
||||
// MessageManager 新架构导出(已替换 conversationStore)
|
||||
export { messageManager, MessageManager, useMessageStore } from './message';
|
||||
export type {
|
||||
MessageEvent,
|
||||
MessageEventType,
|
||||
MessageSubscriber,
|
||||
MessageManagerConversationListDeps,
|
||||
} from './message';
|
||||
export {
|
||||
|
||||
@@ -29,9 +29,6 @@ import { useMessageStore, normalizeConversationId } from './store';
|
||||
|
||||
// 重新导出类型,保持兼容性
|
||||
export type {
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
|
||||
@@ -23,15 +23,10 @@ export type { MessageState, MessageActions, MessageStore } from './store';
|
||||
// ==================== 类型导出 ====================
|
||||
// 从 types.ts 导出所有类型
|
||||
export type {
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
HandleNewMessageOptions,
|
||||
// 服务接口类型(保留向后兼容,但服务层已不再使用)
|
||||
IMessageStateManager,
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
IReadReceiptManager,
|
||||
|
||||
@@ -1,193 +1,48 @@
|
||||
/**
|
||||
* 消息管理类型定义模块
|
||||
* 包含所有与消息管理相关的类型、接口和类型别名
|
||||
*/
|
||||
|
||||
import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto';
|
||||
import type { IConversationListPagedSource } from '../conversationListSources';
|
||||
|
||||
// ==================== 事件类型 ====================
|
||||
|
||||
/**
|
||||
* 消息事件类型枚举
|
||||
* 定义了所有可能的消息事件类型
|
||||
*/
|
||||
export type MessageEventType =
|
||||
| 'conversations_updated'
|
||||
| 'conversations_loading'
|
||||
| 'messages_updated'
|
||||
| 'unread_count_updated'
|
||||
| 'connection_changed'
|
||||
| 'message_sent'
|
||||
| 'message_read'
|
||||
| 'message_received'
|
||||
| 'message_recalled'
|
||||
| 'typing_status'
|
||||
| 'group_notice'
|
||||
| 'error';
|
||||
|
||||
/**
|
||||
* 消息事件接口
|
||||
* 所有订阅者接收的事件对象结构
|
||||
*/
|
||||
export interface MessageEvent {
|
||||
type: MessageEventType;
|
||||
payload: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息订阅者类型
|
||||
*/
|
||||
export type MessageSubscriber = (event: MessageEvent) => void;
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
/**
|
||||
* 消息管理器内部状态接口
|
||||
* 定义了所有需要管理的状态字段
|
||||
*/
|
||||
export interface MessageManagerState {
|
||||
// 会话相关
|
||||
conversations: Map<string, ConversationResponse>;
|
||||
conversationList: ConversationResponse[];
|
||||
|
||||
// 消息相关 - 按会话ID存储
|
||||
messagesMap: Map<string, MessageResponse[]>;
|
||||
|
||||
// 未读数
|
||||
totalUnreadCount: number;
|
||||
systemUnreadCount: number;
|
||||
|
||||
// 连接状态
|
||||
isWSConnected: boolean;
|
||||
|
||||
// 当前活动会话ID(用户正在查看的会话)
|
||||
currentConversationId: string | null;
|
||||
|
||||
// 加载状态
|
||||
isLoadingConversations: boolean;
|
||||
loadingMessagesSet: Set<string>; // 正在加载消息的会话ID集合
|
||||
|
||||
// 初始化状态
|
||||
loadingMessagesSet: Set<string>;
|
||||
isInitialized: boolean;
|
||||
|
||||
// 订阅者
|
||||
subscribers: Set<MessageSubscriber>;
|
||||
|
||||
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
||||
typingUsersMap: Map<string, string[]>;
|
||||
|
||||
// 当前用户的禁言状态 - 按群组ID存储
|
||||
mutedStatusMap: Map<string, boolean>;
|
||||
}
|
||||
|
||||
// ==================== 已读状态相关 ====================
|
||||
|
||||
/**
|
||||
* 已读状态记录接口
|
||||
* 用于跟踪已读操作的状态和保护机制
|
||||
*/
|
||||
export interface ReadStateRecord {
|
||||
/** 标记已读的时间戳 */
|
||||
timestamp: number;
|
||||
/** 状态版本号,用于防止旧数据覆盖新数据 */
|
||||
version: number;
|
||||
/** 已上报的已读序号(用于去重) */
|
||||
lastReadSeq: number;
|
||||
/** 清除保护的定时器ID */
|
||||
clearTimer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
// ==================== 依赖注入接口 ====================
|
||||
|
||||
/**
|
||||
* 可注入会话列表源,便于测试或替换实现
|
||||
*/
|
||||
export interface MessageManagerConversationListDeps {
|
||||
remoteConversationListSource?: IConversationListPagedSource;
|
||||
localConversationListSource?: IConversationListPagedSource;
|
||||
/**
|
||||
* 未传 remoteConversationListSource 时:默认 `cursor`(/conversations/cursor),
|
||||
* `offset` 为常规页码(/conversations?page=&page_size=)。
|
||||
*/
|
||||
remoteListKind?: 'cursor' | 'offset';
|
||||
/** 与 remoteListKind 搭配,默认与 CONVERSATION_LIST_PAGE_SIZE 一致 */
|
||||
remoteListPageSize?: number;
|
||||
}
|
||||
|
||||
// ==================== 消息处理选项 ====================
|
||||
|
||||
/**
|
||||
* 处理新消息的选项
|
||||
*/
|
||||
export interface HandleNewMessageOptions {
|
||||
/** 是否抑制未读数增加 */
|
||||
suppressUnreadIncrement?: boolean;
|
||||
/** 是否抑制震动 */
|
||||
suppressVibration?: boolean;
|
||||
/** 是否抑制会话更新 */
|
||||
suppressConversationUpdates?: boolean;
|
||||
/** 是否抑制自动标记已读 */
|
||||
suppressAutoMarkAsRead?: boolean;
|
||||
}
|
||||
|
||||
// ==================== 服务接口 ====================
|
||||
|
||||
/**
|
||||
* 状态管理器接口
|
||||
* 定义状态管理器需要实现的方法
|
||||
*/
|
||||
export interface IMessageStateManager {
|
||||
// 获取状态
|
||||
getState(): MessageManagerState;
|
||||
getConversations(): ConversationResponse[];
|
||||
getConversation(conversationId: string): ConversationResponse | null;
|
||||
getMessages(conversationId: string): MessageResponse[];
|
||||
getUnreadCount(): { total: number; system: number };
|
||||
isConnected(): boolean;
|
||||
isLoading(): boolean;
|
||||
isLoadingMessages(conversationId: string): boolean;
|
||||
getTypingUsers(groupId: string): string[];
|
||||
isMuted(groupId: string): boolean;
|
||||
getActiveConversation(): string | null;
|
||||
|
||||
// 设置状态
|
||||
setConversations(conversations: Map<string, ConversationResponse>): void;
|
||||
updateConversation(conversation: ConversationResponse): void;
|
||||
removeConversation(conversationId: string): void;
|
||||
setMessages(conversationId: string, messages: MessageResponse[]): void;
|
||||
addMessage(conversationId: string, message: MessageResponse): void;
|
||||
updateMessage(conversationId: string, messageId: string, updates: Partial<MessageResponse>): void;
|
||||
setUnreadCount(total: number, system: number): void;
|
||||
setSSEConnected(connected: boolean): void;
|
||||
setCurrentConversation(conversationId: string | null): void;
|
||||
setLoading(loading: boolean): void;
|
||||
setLoadingMessages(conversationId: string, loading: boolean): void;
|
||||
setTypingUsers(groupId: string, users: string[]): void;
|
||||
setMutedStatus(groupId: string, isMuted: boolean): void;
|
||||
setInitialized(initialized: boolean): void;
|
||||
|
||||
// 订阅机制
|
||||
subscribe(subscriber: MessageSubscriber): () => void;
|
||||
notifySubscribers(event: MessageEvent): void;
|
||||
|
||||
// 重置
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息去重服务接口
|
||||
*/
|
||||
export interface IMessageDeduplication {
|
||||
isMessageProcessed(messageId: string): boolean;
|
||||
markMessageAsProcessed(messageId: string): void;
|
||||
cleanupProcessedMessageIds(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户缓存服务接口
|
||||
*/
|
||||
export interface IUserCacheService {
|
||||
getSenderInfo(userId: string): Promise<UserDTO | null>;
|
||||
enrichMessagesWithSenderInfo(
|
||||
@@ -197,9 +52,6 @@ export interface IUserCacheService {
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已读回执管理器接口
|
||||
*/
|
||||
export interface IReadReceiptManager {
|
||||
markAsRead(conversationId: string, seq: number): Promise<void>;
|
||||
markAllAsRead(): Promise<void>;
|
||||
@@ -210,9 +62,6 @@ export interface IReadReceiptManager {
|
||||
getPendingReadMap(): Map<string, ReadStateRecord>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话操作接口
|
||||
*/
|
||||
export interface IConversationOperations {
|
||||
createConversation(userId: string): Promise<ConversationResponse | null>;
|
||||
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void;
|
||||
@@ -225,9 +74,6 @@ export interface IConversationOperations {
|
||||
decrementSystemUnreadCount(count?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送服务接口
|
||||
*/
|
||||
export interface IMessageSendService {
|
||||
sendMessage(
|
||||
conversationId: string,
|
||||
@@ -236,9 +82,6 @@ export interface IMessageSendService {
|
||||
): Promise<MessageResponse | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息同步服务接口
|
||||
*/
|
||||
export interface IMessageSyncService {
|
||||
fetchConversations(forceRefresh?: boolean, source?: string): Promise<void>;
|
||||
loadMoreConversations(): Promise<void>;
|
||||
@@ -249,9 +92,6 @@ export interface IMessageSyncService {
|
||||
canLoadMoreConversations(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 消息处理器接口
|
||||
*/
|
||||
export interface IWSMessageHandler {
|
||||
connect(): void;
|
||||
disconnect(): void;
|
||||
|
||||
@@ -50,18 +50,10 @@ export {
|
||||
|
||||
// 重导出类型
|
||||
export type {
|
||||
// 事件类型
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
|
||||
// 状态类型
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
HandleNewMessageOptions,
|
||||
|
||||
// 服务接口类型
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
IReadReceiptManager,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 帖子模块统一导出
|
||||
*/
|
||||
|
||||
// ==================== Store ====================
|
||||
// ==================== Store (PostManager) ====================
|
||||
export {
|
||||
usePostManagerStore,
|
||||
type PostManagerState,
|
||||
@@ -10,6 +10,14 @@ export {
|
||||
type PostManagerStore,
|
||||
} from './postStore';
|
||||
|
||||
// ==================== Store (ProcessPostUseCase) ====================
|
||||
export {
|
||||
usePostListStore,
|
||||
type PostsState,
|
||||
type PostDetailState,
|
||||
type PostListStore,
|
||||
} from './postListStore';
|
||||
|
||||
// ==================== Manager ====================
|
||||
export { postManager, PostManager } from './PostManager';
|
||||
|
||||
|
||||
185
src/stores/post/postListStore.ts
Normal file
185
src/stores/post/postListStore.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
|
||||
export interface PostsState {
|
||||
posts: Post[];
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
hasMore: boolean;
|
||||
error: string | null;
|
||||
cursor: string | null;
|
||||
currentPage: number;
|
||||
lastLoadTime: number | null;
|
||||
lastParams?: any;
|
||||
}
|
||||
|
||||
export interface PostDetailState {
|
||||
post: Post | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface PostListStoreState {
|
||||
postsStateMap: Map<string, PostsState>;
|
||||
postDetailStateMap: Map<string, PostDetailState>;
|
||||
}
|
||||
|
||||
export interface PostListStoreActions {
|
||||
getPostsState: (key: string) => PostsState;
|
||||
getPostDetailState: (postId: string) => PostDetailState;
|
||||
updatePostsState: (key: string, partial: Partial<PostsState>) => void;
|
||||
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => void;
|
||||
updatePostInLists: (postId: string, updatedPost: Post) => void;
|
||||
removePostFromLists: (postId: string) => void;
|
||||
applyShareCountUpdate: (postId: string, sharesCount: number) => void;
|
||||
clearPostsState: (key: string) => void;
|
||||
clearPostDetailState: (postId: string) => void;
|
||||
clearAllStates: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_POSTS_STATE: PostsState = {
|
||||
posts: [],
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasMore: true,
|
||||
error: null,
|
||||
cursor: null,
|
||||
currentPage: 1,
|
||||
lastLoadTime: null,
|
||||
};
|
||||
|
||||
const DEFAULT_POST_DETAIL_STATE: PostDetailState = {
|
||||
post: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export type PostListStore = PostListStoreState & PostListStoreActions;
|
||||
|
||||
export const usePostListStore = create<PostListStore>((set, get) => ({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
|
||||
getPostsState: (key: string) => {
|
||||
return get().postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
|
||||
},
|
||||
|
||||
getPostDetailState: (postId: string) => {
|
||||
return get().postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
|
||||
},
|
||||
|
||||
updatePostsState: (key: string, partial: Partial<PostsState>) => {
|
||||
set(state => {
|
||||
const current = state.postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
newMap.set(key, { ...current, ...partial });
|
||||
return { postsStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => {
|
||||
set(state => {
|
||||
const current = state.postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
|
||||
const newMap = new Map(state.postDetailStateMap);
|
||||
newMap.set(postId, { ...current, ...partial });
|
||||
return { postDetailStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
updatePostInLists: (postId: string, updatedPost: Post) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
let changed = false;
|
||||
for (const [key, postsState] of newMap) {
|
||||
const index = postsState.posts.findIndex(p => p.id === postId);
|
||||
if (index !== -1) {
|
||||
const newPosts = [...postsState.posts];
|
||||
newPosts[index] = updatedPost;
|
||||
newMap.set(key, { ...postsState, posts: newPosts });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { postsStateMap: newMap } : state;
|
||||
});
|
||||
},
|
||||
|
||||
removePostFromLists: (postId: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
let changed = false;
|
||||
for (const [key, postsState] of newMap) {
|
||||
const filtered = postsState.posts.filter(p => p.id !== postId);
|
||||
if (filtered.length !== postsState.posts.length) {
|
||||
newMap.set(key, { ...postsState, posts: filtered });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { postsStateMap: newMap } : state;
|
||||
});
|
||||
},
|
||||
|
||||
applyShareCountUpdate: (postId: string, sharesCount: number) => {
|
||||
set(state => {
|
||||
const newPostsMap = new Map(state.postsStateMap);
|
||||
let postsChanged = false;
|
||||
for (const [key, postsState] of newPostsMap) {
|
||||
const index = postsState.posts.findIndex(p => p.id === postId);
|
||||
if (index !== -1) {
|
||||
const newPosts = [...postsState.posts];
|
||||
const cur = newPosts[index];
|
||||
newPosts[index] = { ...cur, sharesCount, shares_count: sharesCount };
|
||||
newPostsMap.set(key, { ...postsState, posts: newPosts });
|
||||
postsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newDetailMap = new Map(state.postDetailStateMap);
|
||||
let detailChanged = false;
|
||||
const detail = newDetailMap.get(postId);
|
||||
if (detail?.post) {
|
||||
newDetailMap.set(postId, {
|
||||
...detail,
|
||||
post: { ...detail.post, sharesCount, shares_count: sharesCount },
|
||||
});
|
||||
detailChanged = true;
|
||||
}
|
||||
|
||||
if (!postsChanged && !detailChanged) return state;
|
||||
return {
|
||||
...(postsChanged ? { postsStateMap: newPostsMap } : {}),
|
||||
...(detailChanged ? { postDetailStateMap: newDetailMap } : {}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearPostsState: (key: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
newMap.delete(key);
|
||||
return { postsStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
clearPostDetailState: (postId: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postDetailStateMap);
|
||||
newMap.delete(postId);
|
||||
return { postDetailStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
clearAllStates: () => {
|
||||
set({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
});
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -1,66 +1,40 @@
|
||||
/**
|
||||
* 帖子状态 Zustand Store
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { Post } from '../../types';
|
||||
import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache';
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
export interface PostManagerState {
|
||||
// 帖子列表缓存 (key: list:type:page:pageSize)
|
||||
listsMap: Map<string, CacheEntry<Post[]>>;
|
||||
|
||||
// 帖子详情缓存
|
||||
detailsMap: Map<string, CacheEntry<Post | null>>;
|
||||
|
||||
// 加载状态
|
||||
loadingListKeys: Set<string>;
|
||||
loadingDetailIds: Set<string>;
|
||||
|
||||
// 待处理请求(去重)
|
||||
pendingRequests: Map<string, Promise<any>>;
|
||||
}
|
||||
|
||||
export interface PostManagerActions {
|
||||
// 获取状态
|
||||
getPostsList: (type: string, page: number, pageSize: number) => Post[] | null;
|
||||
getPostDetail: (postId: string) => Post | null;
|
||||
isListExpired: (type: string, page: number, pageSize: number) => boolean;
|
||||
isDetailExpired: (postId: string) => boolean;
|
||||
isLoadingList: (type: string, page: number, pageSize: number) => boolean;
|
||||
isLoadingDetail: (postId: string) => boolean;
|
||||
|
||||
// 设置状态
|
||||
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => void;
|
||||
setPostDetail: (postId: string, post: Post | null) => void;
|
||||
setLoadingList: (type: string, page: number, pageSize: number, loading: boolean) => void;
|
||||
setLoadingDetail: (postId: string, loading: boolean) => void;
|
||||
|
||||
// 缓存管理
|
||||
invalidateList: (type?: string, page?: number, pageSize?: number) => void;
|
||||
invalidateDetail: (postId: string) => void;
|
||||
invalidateAll: () => void;
|
||||
|
||||
// 请求去重
|
||||
getPendingRequest: <T>(key: string) => Promise<T> | undefined;
|
||||
setPendingRequest: <T>(key: string, request: Promise<T>) => void;
|
||||
deletePendingRequest: (key: string) => void;
|
||||
clearPendingRequests: () => void;
|
||||
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
function buildListKey(type: string, page: number, pageSize: number): string {
|
||||
return `list:${type}:${page}:${pageSize}`;
|
||||
}
|
||||
|
||||
// ==================== 初始状态 ====================
|
||||
|
||||
const initialState: PostManagerState = {
|
||||
listsMap: new Map(),
|
||||
detailsMap: new Map(),
|
||||
@@ -69,16 +43,11 @@ const initialState: PostManagerState = {
|
||||
pendingRequests: new Map(),
|
||||
};
|
||||
|
||||
// ==================== Store 创建 ====================
|
||||
|
||||
export type PostManagerStore = PostManagerState & PostManagerActions;
|
||||
|
||||
export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
// ==================== 初始状态 ====================
|
||||
...initialState,
|
||||
|
||||
// ==================== 获取状态 ====================
|
||||
|
||||
getPostsList: (type: string, page: number, pageSize: number) => {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
const entry = get().listsMap.get(key);
|
||||
@@ -116,8 +85,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
return get().loadingDetailIds.has(postId);
|
||||
},
|
||||
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
set(state => {
|
||||
@@ -160,8 +127,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
invalidateList: (type?: string, page?: number, pageSize?: number) => {
|
||||
if (type !== undefined && page !== undefined && pageSize !== undefined) {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
@@ -191,8 +156,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 请求去重 ====================
|
||||
|
||||
getPendingRequest: <T>(key: string) => {
|
||||
return get().pendingRequests.get(key) as Promise<T> | undefined;
|
||||
},
|
||||
@@ -217,8 +180,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
set({ pendingRequests: new Map() });
|
||||
},
|
||||
|
||||
// ==================== 重置 ====================
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
...initialState,
|
||||
|
||||
Reference in New Issue
Block a user