/** * PostRepository - 帖子仓库实现 * 实现IPostRepository接口,封装帖子相关的数据访问逻辑 */ import { ApiDataSource, apiDataSource } from '../datasources/ApiDataSource'; import { LocalDataSource, localDataSource } from '../datasources/LocalDataSource'; import { PostMapper } from '../mappers/PostMapper'; import type { Post, PostImage, PostStatus } from '../../core/entities/Post'; import { createPost } from '../../core/entities/Post'; import type { IPostRepository, GetPostsParams, CreatePostData, UpdatePostData, PostsResult, SearchPostsParams, } from './interfaces/IPostRepository'; // ==================== 类型定义 ==================== /** * API帖子响应类型 */ interface PostApiResponse { id: number; user_id: number; title: string; content: string; images?: Array<{ url: string; width?: number; height?: number; thumbnail_url?: string }>; likes_count: number; comments_count: number; shares_count: number; views_count: number; favorites_count: number; is_liked: boolean; is_favorited: boolean; is_pinned: boolean; status: string; channel_id?: string; tags?: string[]; created_at: string; updated_at: string; content_edited_at?: string; author?: { id: number; username: string; nickname?: string; avatar?: string; is_following?: boolean; is_following_me?: boolean; }; } /** * API帖子列表响应类型 */ interface PostsListApiResponse { list: PostApiResponse[]; has_more?: boolean; next_cursor?: string; total?: number; page?: number; page_size?: number; total_pages?: number; } /** * 缓存的帖子数据 */ interface CachedPost { id: string; data: string; updatedAt: string; } // ==================== PostRepository 实现 ==================== export class PostRepository implements IPostRepository { private api: ApiDataSource; private localDb: LocalDataSource; private memoryCache: Map = new Map(); private readonly CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存过期时间 constructor(api: ApiDataSource, localDb: LocalDataSource) { this.api = api; this.localDb = localDb; } // ==================== 私有辅助方法 ==================== /** * 将API响应转换为领域实体 * 同时设置 camelCase 和 snake_case 字段以保持向后兼容 */ private mapToPost(response: PostApiResponse): Post { const model = PostMapper.fromApiResponse(response as any); return { // camelCase 字段(新标准) id: model.id, authorId: model.authorId, author: model.author ? { id: model.author.id, username: model.author.username, nickname: model.author.nickname, avatar: model.author.avatar, // 关注关系直接透传 API 字段,供详情页关注按钮状态使用 is_following: response.author?.is_following, is_following_me: response.author?.is_following_me, } : undefined, title: model.title, content: model.content, images: (model.images || []).map(url => ({ url, width: undefined, height: undefined, })) as PostImage[], likesCount: model.likeCount, commentsCount: model.commentCount, sharesCount: model.shareCount, viewsCount: model.viewCount, favoritesCount: model.favoriteCount, isLiked: model.isLiked, isFavorited: model.isFavorited, isPinned: model.isTop, status: model.status, channelId: model.channelId, tags: model.tags || [], createdAt: model.createdAt.toISOString(), updatedAt: model.updatedAt.toISOString(), contentEditedAt: response.content_edited_at, // snake_case 字段(向后兼容) likes_count: model.likeCount, comments_count: model.commentCount, favorites_count: model.favoriteCount, shares_count: model.shareCount, views_count: model.viewCount, is_liked: model.isLiked, is_favorited: model.isFavorited, is_pinned: model.isTop, user_id: model.authorId, created_at: model.createdAt.toISOString(), updated_at: model.updatedAt.toISOString(), content_edited_at: response.content_edited_at, channel_id: model.channelId, }; } /** * 从内存缓存获取帖子 */ private getFromMemoryCache(id: string): Post | null { const cached = this.memoryCache.get(id); if (!cached) return null; // 检查缓存是否过期 if (Date.now() - cached.timestamp > this.CACHE_TTL) { this.memoryCache.delete(id); return null; } return cached.post; } /** * 保存帖子到内存缓存 */ private saveToMemoryCache(post: Post): void { this.memoryCache.set(post.id, { post, timestamp: Date.now(), }); } /** * 从本地数据库获取缓存的帖子 */ private async getFromLocalCache(id: string): Promise { try { await this.localDb.initialize(); const result = await this.localDb.getFirst( 'SELECT * FROM posts_cache WHERE id = ?', [id] ); if (!result) return null; const post = JSON.parse(result.data) as Post; return post; } catch (error) { console.error('[PostRepository] 获取本地缓存失败:', error); return null; } } /** * 保存帖子到本地缓存 */ private async saveToLocalCache(post: Post): Promise { try { await this.localDb.initialize(); await this.localDb.enqueueWrite(async () => { await this.localDb.run( `INSERT OR REPLACE INTO posts_cache (id, data, updatedAt) VALUES (?, ?, ?)`, [post.id, JSON.stringify(post), new Date().toISOString()] ); }); } catch (error) { console.error('[PostRepository] 保存本地缓存失败:', error); } } /** * 清除帖子的本地缓存 */ private async clearLocalCache(id: string): Promise { try { await this.localDb.initialize(); await this.localDb.enqueueWrite(async () => { await this.localDb.run('DELETE FROM posts_cache WHERE id = ?', [id]); }); } catch (error) { console.error('[PostRepository] 清除本地缓存失败:', error); } } /** * 处理错误 */ private handleError(error: unknown, operation: string): never { const message = error instanceof Error ? error.message : 'Unknown error'; console.error(`[PostRepository] ${operation} 失败:`, error); throw new Error(`帖子${operation}失败: ${message}`); } // ==================== 帖子查询 ==================== /** * 获取帖子列表 */ async getPosts(params?: GetPostsParams): Promise { try { const queryParams: Record = {}; if (params?.page !== undefined) queryParams.page = params.page; if (params?.pageSize) queryParams.page_size = params.pageSize; // 支持 cursor 参数:空字符串也传递,用于启动 cursor 模式 if (params?.cursor !== undefined && params?.cursor !== null) { queryParams.cursor = params.cursor; } if (params?.post_type) queryParams.tab = params.post_type; if (params?.channelId) queryParams.channel_id = params.channelId; if (params?.authorId) queryParams.author_id = params.authorId; if (params?.tags && params.tags.length > 0) queryParams.tags = params.tags.join(','); if (params?.status) queryParams.status = params.status; if (params?.sortBy) queryParams.sort_by = params.sortBy; if (params?.sortOrder) queryParams.sort_order = params.sortOrder; if (params?.pinnedOnly) queryParams.pinned_only = true; const response = await this.api.get('/posts', queryParams); const posts = (response.list || []).map(p => this.mapToPost(p)); // 缓存帖子 posts.forEach(post => { this.saveToMemoryCache(post); this.saveToLocalCache(post); }); // 计算 hasMore:优先使用 has_more,否则通过 page/total_pages 计算 let hasMore = response.has_more; if (hasMore === undefined && response.page !== undefined && response.total_pages !== undefined) { hasMore = response.page < response.total_pages; } hasMore = hasMore ?? false; return { posts, hasMore, nextCursor: response.next_cursor, total: response.total, }; } catch (error) { this.handleError(error, '获取帖子列表'); } } /** * 获取单个帖子详情 */ async getPostById(id: string): Promise { try { // 详情页优先取最新服务端数据,避免旧缓存导致关注态等关系字段过期 const response = await this.api.get(`/posts/${id}`); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { // API失败时再回退缓存,保证离线/弱网可用 const memoryCached = this.getFromMemoryCache(id); if (memoryCached) { console.warn('[PostRepository] API获取失败,使用内存缓存:', error); return memoryCached; } const localCached = await this.getFromLocalCache(id); if (localCached) { console.warn('[PostRepository] API获取失败,使用本地缓存:', error); this.saveToMemoryCache(localCached); return localCached; } this.handleError(error, '获取帖子详情'); } } /** * 获取用户发布的帖子列表 */ async getPostsByUser(userId: string, params?: GetPostsParams): Promise { try { const queryParams: Record = { author_id: userId }; if (params?.page) queryParams.page = params.page; if (params?.pageSize) queryParams.page_size = params.pageSize; if (params?.cursor) queryParams.cursor = params.cursor; if (params?.status) queryParams.status = params.status; if (params?.sortBy) queryParams.sort_by = params.sortBy; if (params?.sortOrder) queryParams.sort_order = params.sortOrder; const response = await this.api.get(`/users/${userId}/posts`, queryParams); const posts = (response.list || []).map(p => this.mapToPost(p)); // 缓存帖子 posts.forEach(post => { this.saveToMemoryCache(post); this.saveToLocalCache(post); }); return { posts, hasMore: response.has_more ?? false, nextCursor: response.next_cursor, total: response.total, }; } catch (error) { this.handleError(error, '获取用户帖子列表'); } } /** * 搜索帖子 */ async searchPosts(params: SearchPostsParams): Promise { try { const queryParams: Record = { keyword: params.keyword, }; if (params.page) queryParams.page = params.page; if (params.pageSize) queryParams.page_size = params.pageSize; if (params.scope) queryParams.scope = params.scope; if (params.channelId) queryParams.channel_id = params.channelId; const response = await this.api.get('/posts/search', queryParams); const posts = (response.list || []).map(p => this.mapToPost(p)); // 缓存帖子 posts.forEach(post => { this.saveToMemoryCache(post); this.saveToLocalCache(post); }); return { posts, hasMore: response.has_more ?? false, nextCursor: response.next_cursor, total: response.total, }; } catch (error) { this.handleError(error, '搜索帖子'); } } // ==================== 帖子操作 ==================== /** * 创建帖子 */ async createPost(data: CreatePostData): Promise { try { const requestData = PostMapper.createPostRequest( data.title, data.content, data.images?.map(img => img.url), data.channelId ); if (data.tags && data.tags.length > 0) { requestData.tags = data.tags; } if (data.status) { requestData.status = data.status; } const response = await this.api.post('/posts', requestData); const post = this.mapToPost(response); // 缓存新创建的帖子 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '创建帖子'); } } /** * 更新帖子 */ async updatePost(id: string, data: UpdatePostData): Promise { try { const requestData: Record = {}; if (data.title !== undefined) requestData.title = data.title; if (data.content !== undefined) requestData.content = data.content; if (data.images !== undefined) requestData.images = data.images.map(img => img.url); if (data.tags !== undefined) requestData.tags = data.tags; if (data.status !== undefined) requestData.status = data.status; if (data.isPinned !== undefined) requestData.is_pinned = data.isPinned; const response = await this.api.put(`/posts/${id}`, requestData); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '更新帖子'); } } /** * 删除帖子 */ async deletePost(id: string): Promise { try { await this.api.delete(`/posts/${id}`); // 清除缓存 this.memoryCache.delete(id); await this.clearLocalCache(id); } catch (error) { this.handleError(error, '删除帖子'); } } // ==================== 互动操作 ==================== /** * 点赞帖子 */ async likePost(id: string): Promise { try { const response = await this.api.post(`/posts/${id}/like`); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '点赞帖子'); } } /** * 取消点赞 */ async unlikePost(id: string): Promise { try { const response = await this.api.delete(`/posts/${id}/like`); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '取消点赞'); } } /** * 收藏帖子 */ async favoritePost(id: string): Promise { try { const response = await this.api.post(`/posts/${id}/favorite`); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '收藏帖子'); } } /** * 取消收藏 */ async unfavoritePost(id: string): Promise { try { const response = await this.api.delete(`/posts/${id}/favorite`); const post = this.mapToPost(response); // 更新缓存 this.saveToMemoryCache(post); await this.saveToLocalCache(post); return post; } catch (error) { this.handleError(error, '取消收藏'); } } // ==================== 缓存管理 ==================== /** * 清除所有缓存 */ async clearAllCache(): Promise { this.memoryCache.clear(); try { await this.localDb.initialize(); await this.localDb.enqueueWrite(async () => { await this.localDb.run('DELETE FROM posts_cache'); }); } catch (error) { console.error('[PostRepository] 清除所有缓存失败:', error); } } /** * 使指定帖子的缓存失效 */ async invalidateCache(id: string): Promise { this.memoryCache.delete(id); await this.clearLocalCache(id); } } // ==================== 单例导出 ==================== export const postRepository = new PostRepository(apiDataSource, localDataSource); export default postRepository;