Files
frontend/src/data/repositories/PostRepository.ts

567 lines
16 KiB
TypeScript
Raw Normal View History

/**
* 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;
community_id?: string;
tags?: string[];
created_at: string;
updated_at: string;
content_edited_at?: string;
author?: {
id: number;
username: string;
nickname?: string;
avatar?: string;
};
}
/**
* 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<string, { post: Post; timestamp: number }> = 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,
} : 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,
communityId: model.communityId,
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,
community_id: model.communityId,
};
}
/**
*
*/
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<Post | null> {
try {
await this.localDb.initialize();
const result = await this.localDb.getFirst<CachedPost>(
'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<void> {
try {
await this.localDb.initialize();
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<void> {
try {
await this.localDb.initialize();
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<PostsResult> {
try {
const queryParams: Record<string, any> = {};
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?.communityId) queryParams.community_id = params.communityId;
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<PostsListApiResponse>('/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<Post | null> {
try {
// 1. 先查内存缓存
const memoryCached = this.getFromMemoryCache(id);
if (memoryCached) {
return memoryCached;
}
// 2. 查本地数据库缓存
const localCached = await this.getFromLocalCache(id);
if (localCached) {
this.saveToMemoryCache(localCached);
return localCached;
}
// 3. 从API获取
const response = await this.api.get<PostApiResponse>(`/posts/${id}`);
const post = this.mapToPost(response);
// 更新缓存
this.saveToMemoryCache(post);
await this.saveToLocalCache(post);
return post;
} catch (error) {
// 如果API请求失败尝试返回本地缓存
const localCached = await this.getFromLocalCache(id);
if (localCached) {
console.warn('[PostRepository] API获取失败使用本地缓存:', error);
return localCached;
}
this.handleError(error, '获取帖子详情');
}
}
/**
*
*/
async getPostsByUser(userId: string, params?: GetPostsParams): Promise<PostsResult> {
try {
const queryParams: Record<string, any> = { 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<PostsListApiResponse>(`/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<PostsResult> {
try {
const queryParams: Record<string, any> = {
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.communityId) queryParams.community_id = params.communityId;
const response = await this.api.get<PostsListApiResponse>('/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<Post> {
try {
const requestData = PostMapper.createPostRequest(
data.title,
data.content,
data.images?.map(img => img.url),
data.communityId
);
if (data.tags && data.tags.length > 0) {
requestData.tags = data.tags;
}
if (data.status) {
requestData.status = data.status;
}
const response = await this.api.post<PostApiResponse>('/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<Post> {
try {
const requestData: Record<string, any> = {};
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<PostApiResponse>(`/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<void> {
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<Post> {
try {
const response = await this.api.post<PostApiResponse>(`/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<Post> {
try {
const response = await this.api.delete<PostApiResponse>(`/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<Post> {
try {
const response = await this.api.post<PostApiResponse>(`/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<Post> {
try {
const response = await this.api.delete<PostApiResponse>(`/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<void> {
this.memoryCache.clear();
try {
await this.localDb.initialize();
await this.localDb.run('DELETE FROM posts_cache');
} catch (error) {
console.error('[PostRepository] 清除所有缓存失败:', error);
}
}
/**
* 使
*/
async invalidateCache(id: string): Promise<void> {
this.memoryCache.delete(id);
await this.clearLocalCache(id);
}
}
// ==================== 单例导出 ====================
export const postRepository = new PostRepository(apiDataSource, localDataSource);
export default postRepository;