/** * ProcessPostUseCase - 处理帖子用例 * 处理帖子的业务逻辑,协调 Repository 和状态管理 * 纯业务逻辑,无UI依赖 */ import { postRepository } from '../../data/repositories/PostRepository'; import type { Post } from '../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'; // ==================== 事件类型定义 ==================== /** * 帖子用例事件类型 */ export type PostUseCaseEventType = | 'posts_loaded' | 'post_created' | 'post_updated' | 'post_deleted' | 'post_liked' | 'post_unliked' | 'post_favorited' | 'post_unfavorited' | 'loading_changed' | 'error_occurred' | 'state_changed'; /** * 帖子用例事件 */ export interface PostUseCaseEvent { type: PostUseCaseEventType; payload: any; timestamp: number; } /** * 事件订阅者类型 */ export type PostUseCaseSubscriber = (event: PostUseCaseEvent) => void; // ==================== 状态类型定义 ==================== /** * 帖子列表状态 */ export interface PostsState { /** 帖子列表 */ posts: Post[]; /** 是否正在加载 */ isLoading: boolean; /** 是否正在刷新 */ isRefreshing: boolean; /** 是否有更多数据 */ hasMore: boolean; /** 错误信息 */ error: string | null; /** 当前游标(cursor 模式) */ cursor: string | null; /** 当前页码(page 模式) */ currentPage: number; /** 最后加载时间 */ lastLoadTime: number | null; /** 上次请求的参数(用于加载更多和刷新) */ lastParams?: GetPostsParams; } /** * 帖子详情状态 */ export interface PostDetailState { /** 帖子详情 */ post: Post | null; /** 是否正在加载 */ isLoading: boolean; /** 错误信息 */ error: string | null; } // ==================== 默认配置 ==================== const DEFAULT_PAGE_SIZE = 20; // ==================== ProcessPostUseCase 类 ==================== class ProcessPostUseCase { // 单例实例 private static instance: ProcessPostUseCase; // 订阅者管理 private subscribers: Set = new Set(); // 状态管理 private postsState: Map = new Map(); private postDetailState: Map = new Map(); // 分页状态管理器 private paginationManager: PaginationStateManager; // 当前用户ID private currentUserId: string | null = null; private readonly mergePerfWarnThresholdMs = 12; // 私有构造函数 private constructor() { this.paginationManager = new PaginationStateManager(); } /** * 获取单例实例 */ static getInstance(): ProcessPostUseCase { if (!ProcessPostUseCase.instance) { ProcessPostUseCase.instance = new ProcessPostUseCase(); } return ProcessPostUseCase.instance; } // ==================== 初始化和销毁 ==================== /** * 初始化用例 * @param currentUserId 当前用户ID */ initialize(currentUserId: string | null = null): void { this.currentUserId = currentUserId; } /** * 销毁用例 */ destroy(): void { this.subscribers.clear(); this.postsState.clear(); this.postDetailState.clear(); this.paginationManager.clear(); this.currentUserId = null; } // ==================== 订阅机制 ==================== /** * 订阅事件 * @param subscriber 订阅者函数 * @returns 取消订阅函数 */ subscribe(subscriber: PostUseCaseSubscriber): () => void { this.subscribers.add(subscriber); return () => { this.subscribers.delete(subscriber); }; } /** * 通知所有订阅者 * @param event 事件对象 */ private notifySubscribers(event: PostUseCaseEvent): void { this.subscribers.forEach((subscriber) => { try { subscriber(event); } catch (error) { console.error('[ProcessPostUseCase] 订阅者执行失败:', error); } }); } /** * 通知状态变化 * @param key 列表键 */ private notifyStateChanged(key: string): void { const state = this.getPostsState(key); this.notifySubscribers({ type: 'state_changed', payload: { key, state }, timestamp: Date.now(), }); } // ==================== 状态管理 ==================== /** * 获取帖子列表状态 * @param key 列表键(默认为 'default') */ getPostsState(key: string = 'default'): PostsState { let state = this.postsState.get(key); if (!state) { state = { posts: [], isLoading: false, isRefreshing: false, hasMore: true, error: null, cursor: null, currentPage: 1, lastLoadTime: null, }; this.postsState.set(key, state); } return state; } /** * 获取帖子详情状态 * @param postId 帖子ID */ getPostDetailState(postId: string): PostDetailState { let state = this.postDetailState.get(postId); if (!state) { state = { post: null, isLoading: false, error: null, }; this.postDetailState.set(postId, state); } return state; } /** * 更新帖子列表状态 * @param key 列表键 * @param partialState 部分状态 */ private updatePostsState(key: string, partialState: Partial): void { const currentState = this.getPostsState(key); this.postsState.set(key, { ...currentState, ...partialState, }); this.notifyStateChanged(key); } private 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; } private 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 = this.getEntityId(previous[previous.length - 1]); const firstIncomingId = this.getEntityId(incoming[0]); if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) { const prevIdSet = new Set(previous.map((item) => this.getEntityId(item)).filter(Boolean) as string[]); if (!prevIdSet.has(firstIncomingId)) { return [...previous, ...incoming]; } } const merged = [...previous]; const indexById = new Map(); for (let i = 0; i < merged.length; i += 1) { const id = this.getEntityId(merged[i]); if (id) { indexById.set(id, i); } } for (const post of incoming) { const id = this.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; } private 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) => this.getEntityId(item)).filter(Boolean) as string[] ); const remaining = previous.filter((item) => { const id = this.getEntityId(item); return !id || !latestIdSet.has(id); }); return [...latest, ...remaining]; } /** * 更新帖子详情状态 * @param postId 帖子ID * @param partialState 部分状态 */ private updatePostDetailState(postId: string, partialState: Partial): void { const currentState = this.getPostDetailState(postId); this.postDetailState.set(postId, { ...currentState, ...partialState, }); } /** * 分享计数上报成功后,同步各列表与详情中的分享数 */ applyShareCountUpdate(postId: string, sharesCount: number): void { this.postsState.forEach((state, key) => { const index = state.posts.findIndex((p) => p.id === postId); if (index !== -1) { const newPosts = [...state.posts]; const cur = newPosts[index]; newPosts[index] = { ...cur, sharesCount, shares_count: sharesCount, }; this.updatePostsState(key, { posts: newPosts }); } }); const detail = this.getPostDetailState(postId); if (detail.post) { this.updatePostDetailState(postId, { post: { ...detail.post, sharesCount, shares_count: sharesCount, }, }); } } // ==================== 帖子列表操作 ==================== /** * 获取帖子列表 * @param params 查询参数 * @param key 列表键(用于区分不同的列表) */ async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise { // 更新加载状态 this.updatePostsState(key, { isLoading: true, error: null }); try { // 默认传递空字符串 cursor 来启动 cursor 模式 // 注意:cursor 要放在 ...params 后面,这样只有在 params 没有传 cursor 时才使用默认值 const queryParams: GetPostsParams = { pageSize: params?.pageSize || DEFAULT_PAGE_SIZE, ...params, cursor: params?.cursor !== undefined ? params.cursor : '', // 默认使用空字符串启动 cursor 模式 }; const result = await postRepository.getPosts(queryParams); // 判断是否为 cursor 模式(有 next_cursor 返回) const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; // 更新状态(保存请求参数以便加载更多时使用) const mergeStart = Date.now(); const currentState = this.getPostsState(key); const incomingPosts = Array.isArray(result.posts) ? result.posts : []; const mergedPosts = this.mergeRefreshWindow(currentState.posts, incomingPosts); const mergeCost = Date.now() - mergeStart; if (mergeCost >= this.mergePerfWarnThresholdMs) { console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length }); } this.updatePostsState(key, { posts: mergedPosts, hasMore: result.hasMore, cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor,否则设为 null 表示 page 模式 currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始 isLoading: false, lastLoadTime: Date.now(), lastParams: params, // 保存请求参数 }); // 通知订阅者 this.notifySubscribers({ type: 'posts_loaded', payload: { key, posts: incomingPosts, hasMore: result.hasMore }, timestamp: Date.now(), }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败'; this.updatePostsState(key, { isLoading: false, error: errorMessage, }); this.notifySubscribers({ type: 'error_occurred', payload: { key, error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 刷新帖子列表 * @param key 列表键 */ async refreshPosts(key: string = 'default'): Promise { const state = this.getPostsState(key); // 更新刷新状态 this.updatePostsState(key, { isRefreshing: true, error: null }); try { // 使用保存的请求参数(如 post_type)进行刷新 // 默认传递空字符串 cursor 来启动 cursor 模式 const result = await postRepository.getPosts({ pageSize: DEFAULT_PAGE_SIZE, ...state.lastParams, cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '', // 默认使用空字符串启动 cursor 模式 }); // 判断是否为 cursor 模式(有 next_cursor 返回) const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; // 更新状态 const mergeStart = Date.now(); const incomingPosts = Array.isArray(result.posts) ? result.posts : []; const mergedPosts = this.mergeRefreshWindow(state.posts, incomingPosts); const mergeCost = Date.now() - mergeStart; if (mergeCost >= this.mergePerfWarnThresholdMs) { console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length }); } this.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 : '刷新帖子列表失败'; this.updatePostsState(key, { isRefreshing: false, error: errorMessage, }); throw error; } } /** * 加载更多帖子 * @param key 列表键 */ async loadMorePosts(key: string = 'default'): Promise { const state = this.getPostsState(key); // 检查是否还有更多数据 if (!state.hasMore) { return { posts: [], hasMore: false, }; } // 检查是否正在加载 if (state.isLoading) { return { posts: [], hasMore: state.hasMore, }; } // 更新加载状态 this.updatePostsState(key, { isLoading: true, error: null }); try { // 判断使用哪种分页模式:cursor 不为 null 表示使用 cursor 模式 const useCursorMode = state.cursor !== null && state.cursor !== undefined; // 必须先展开 lastParams,再由本次分页字段覆盖。 // lastParams 来自首次 fetch(通常含 page:1),若写在展开之前会把 nextPage/cursor 覆盖掉,导致永远请求第 1 页、无法加载第 3 页及以后。 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 mergeStart = Date.now(); const incomingPosts = Array.isArray(result.posts) ? result.posts : []; const newPosts = this.mergePostListWithFastPath(state.posts, incomingPosts); const mergeCost = Date.now() - mergeStart; if (mergeCost >= this.mergePerfWarnThresholdMs) { console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length }); } // 判断响应是否为 cursor 模式(有 next_cursor 返回) const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; // 更新状态 this.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('[ProcessPostUseCase] Load more error:', errorMessage); this.updatePostsState(key, { isLoading: false, error: errorMessage, }); throw error; } } // ==================== 帖子详情操作 ==================== /** * 获取单个帖子详情 * @param id 帖子ID */ async fetchPostById(id: string): Promise { // 更新加载状态 this.updatePostDetailState(id, { isLoading: true, error: null }); try { const post = await postRepository.getPostById(id); // 更新状态 this.updatePostDetailState(id, { post, isLoading: false, }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败'; this.updatePostDetailState(id, { isLoading: false, error: errorMessage, }); throw error; } } // ==================== 帖子CRUD操作 ==================== /** * 创建帖子 * @param data 创建数据 */ async createPost(data: CreatePostData): Promise { try { const post = await postRepository.createPost(data); // 通知订阅者 this.notifySubscribers({ type: 'post_created', payload: { post }, timestamp: Date.now(), }); // 更新所有列表(新帖子应该出现在列表顶部) this.postsState.forEach((state, key) => { this.updatePostsState(key, { posts: [post, ...state.posts], }); }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '创建帖子失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'create', error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 更新帖子 * @param id 帖子ID * @param data 更新数据 */ async updatePost(id: string, data: UpdatePostData): Promise { try { const post = await postRepository.updatePost(id, data); // 更新详情状态 this.updatePostDetailState(id, { post }); // 更新列表中的帖子 this.updatePostInLists(id, post); // 通知订阅者 this.notifySubscribers({ type: 'post_updated', payload: { postId: id, post }, timestamp: Date.now(), }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '更新帖子失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'update', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 删除帖子 * @param id 帖子ID */ async deletePost(id: string): Promise { try { await postRepository.deletePost(id); // 从详情状态中移除 this.postDetailState.delete(id); // 从所有列表中移除 this.postsState.forEach((state, key) => { const filteredPosts = state.posts.filter((post) => post.id !== id); this.updatePostsState(key, { posts: filteredPosts, }); }); // 通知订阅者 this.notifySubscribers({ type: 'post_deleted', payload: { postId: id }, timestamp: Date.now(), }); } catch (error) { const errorMessage = error instanceof Error ? error.message : '删除帖子失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'delete', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } // ==================== 互动操作 ==================== /** * 点赞帖子 * @param id 帖子ID */ async likePost(id: string): Promise { try { const post = await postRepository.likePost(id); // 更新详情状态 this.updatePostDetailState(id, { post }); // 更新列表中的帖子 this.updatePostInLists(id, post); // 通知订阅者 this.notifySubscribers({ type: 'post_liked', payload: { postId: id, post }, timestamp: Date.now(), }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '点赞失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'like', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 取消点赞 * @param id 帖子ID */ async unlikePost(id: string): Promise { try { const post = await postRepository.unlikePost(id); // 更新详情状态 this.updatePostDetailState(id, { post }); // 更新列表中的帖子 this.updatePostInLists(id, post); // 通知订阅者 this.notifySubscribers({ type: 'post_unliked', payload: { postId: id, post }, timestamp: Date.now(), }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '取消点赞失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'unlike', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 收藏帖子 * @param id 帖子ID */ async favoritePost(id: string): Promise { try { const post = await postRepository.favoritePost(id); // 更新详情状态 this.updatePostDetailState(id, { post }); // 更新列表中的帖子 this.updatePostInLists(id, post); // 通知订阅者 this.notifySubscribers({ type: 'post_favorited', payload: { postId: id, post }, timestamp: Date.now(), }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '收藏失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'favorite', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } /** * 取消收藏 * @param id 帖子ID */ async unfavoritePost(id: string): Promise { try { const post = await postRepository.unfavoritePost(id); // 更新详情状态 this.updatePostDetailState(id, { post }); // 更新列表中的帖子 this.updatePostInLists(id, post); // 通知订阅者 this.notifySubscribers({ type: 'post_unfavorited', payload: { postId: id, post }, timestamp: Date.now(), }); return post; } catch (error) { const errorMessage = error instanceof Error ? error.message : '取消收藏失败'; this.notifySubscribers({ type: 'error_occurred', payload: { operation: 'unfavorite', postId: id, error: errorMessage }, timestamp: Date.now(), }); throw error; } } // ==================== 搜索操作 ==================== /** * 搜索帖子 * @param params 搜索参数 * @param key 列表键 */ async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise { // 更新加载状态 this.updatePostsState(key, { isLoading: true, error: null }); try { const result = await postRepository.searchPosts(params); // 更新状态 this.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 : '搜索帖子失败'; this.updatePostsState(key, { isLoading: false, error: errorMessage, }); throw error; } } // ==================== 用户帖子操作 ==================== /** * 获取用户发布的帖子 * @param userId 用户ID * @param params 查询参数 * @param key 列表键 */ async fetchPostsByUser( userId: string, params?: GetPostsParams, key?: string ): Promise { const listKey = key || `user_${userId}`; // 更新加载状态 this.updatePostsState(listKey, { isLoading: true, error: null }); try { const result = await postRepository.getPostsByUser(userId, params); // 更新状态 this.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 : '获取用户帖子失败'; this.updatePostsState(listKey, { isLoading: false, error: errorMessage, }); throw error; } } // ==================== 辅助方法 ==================== /** * 更新所有列表中的帖子 * @param postId 帖子ID * @param updatedPost 更新后的帖子 */ private updatePostInLists(postId: string, updatedPost: Post): void { this.postsState.forEach((state, key) => { const index = state.posts.findIndex((post) => post.id === postId); if (index !== -1) { const newPosts = [...state.posts]; newPosts[index] = updatedPost; this.updatePostsState(key, { posts: newPosts }); } }); } /** * 清除指定列表的状态 * @param key 列表键 */ clearPostsState(key: string = 'default'): void { this.postsState.delete(key); this.paginationManager.reset(key); } /** * 清除帖子详情状态 * @param postId 帖子ID */ clearPostDetailState(postId: string): void { this.postDetailState.delete(postId); } /** * 清除所有状态 */ clearAllStates(): void { this.postsState.clear(); this.postDetailState.clear(); this.paginationManager.clear(); } /** * 获取当前用户ID */ getCurrentUserId(): string | null { return this.currentUserId; } /** * 设置当前用户ID */ setCurrentUserId(userId: string | null): void { this.currentUserId = userId; } // ==================== 分页状态管理集成 ==================== /** * 获取分页状态 * @param key 列表键 */ getPaginationState(key: string): PaginationState { return this.paginationManager.getState(key); } /** * 重置分页状态 * @param key 列表键 */ resetPagination(key: string): void { this.paginationManager.reset(key); } /** * 检查是否正在加载 * @param key 列表键 */ isLoading(key: string = 'default'): boolean { return this.getPostsState(key).isLoading; } /** * 检查是否有错误 * @param key 列表键 */ hasError(key: string = 'default'): boolean { return this.getPostsState(key).error !== null; } /** * 获取错误信息 * @param key 列表键 */ getError(key: string = 'default'): string | null { return this.getPostsState(key).error; } } // ==================== 导出 ==================== // 导出单例实例 export const processPostUseCase = ProcessPostUseCase.getInstance(); export default processPostUseCase;