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:
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,
|
||||
|
||||
Reference in New Issue
Block a user