Files
frontend/src/core/usecases/ProcessPostUseCase.ts

1007 lines
27 KiB
TypeScript
Raw Normal View History

/**
* 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<PostUseCaseSubscriber> = new Set();
// 状态管理
private postsState: Map<string, PostsState> = new Map();
private postDetailState: Map<string, PostDetailState> = 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<PostsState>): 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<string, number>();
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<PostDetailState>): void {
const currentState = this.getPostDetailState(postId);
this.postDetailState.set(postId, {
...currentState,
...partialState,
});
}
// ==================== 帖子列表操作 ====================
/**
*
* @param params
* @param key
*/
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
// 更新加载状态
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<PostsResult> {
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<PostsResult> {
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<Post | null> {
// 更新加载状态
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<Post> {
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<Post> {
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<void> {
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<Post> {
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<Post> {
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<Post> {
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<Post> {
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<PostsResult> {
// 更新加载状态
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<PostsResult> {
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;