- Add full emoji picker with virtualized FlatList to CreatePostScreen and PostDetailScreen - Add autoExpand prop to PostMentionInput for XHS-style content area - Improve mention item styling with @ icon and hint text - Fix HomeScreen tab switching race condition with requestAnimationFrame - Fix PostRepository pagination to prefer cursor over page parameter - Fix user store imports to use explicit UserManager path - Refactor useCurrentUser hook to use sessionStore directly
576 lines
16 KiB
TypeScript
576 lines
16 KiB
TypeScript
/**
|
||
* PostRepository - 帖子仓库实现
|
||
* 实现IPostRepository接口,封装帖子相关的数据访问逻辑
|
||
*/
|
||
|
||
import { ApiDataSource, apiDataSource } from '../datasources/ApiDataSource';
|
||
import { localDataSource } from '@/database';
|
||
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: string;
|
||
user_id: string;
|
||
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;
|
||
channel?: { id: string; name: string } | null;
|
||
author?: {
|
||
id: string;
|
||
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<string, { post: Post; timestamp: number }> = new Map();
|
||
private readonly CACHE_TTL = 5 * 60 * 1000;
|
||
|
||
constructor(api: ApiDataSource) {
|
||
this.api = api;
|
||
}
|
||
|
||
// ==================== 私有辅助方法 ====================
|
||
|
||
/**
|
||
* 将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,
|
||
channel: model.channel,
|
||
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<Post | null> {
|
||
try {
|
||
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.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<void> {
|
||
try {
|
||
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<PostsResult> {
|
||
try {
|
||
const queryParams: Record<string, any> = {};
|
||
|
||
if (params?.pageSize) queryParams.page_size = params.pageSize;
|
||
// cursor 与 page 互斥:cursor 存在(含空字符串)时走游标分页,否则走页码分页
|
||
if (params?.cursor !== undefined && params?.cursor !== null) {
|
||
queryParams.cursor = params.cursor;
|
||
} else if (params?.page !== undefined) {
|
||
queryParams.page = params.page;
|
||
}
|
||
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<PostsListApiResponse>('/posts', queryParams);
|
||
|
||
const posts = (response.list || []).map(p => this.mapToPost(p));
|
||
|
||
// 缓存帖子
|
||
for (const post of posts) {
|
||
this.saveToMemoryCache(post);
|
||
// 不等待本地缓存写入完成,避免阻塞 UI
|
||
this.saveToLocalCache(post).catch(err => {
|
||
console.warn('[PostRepository] 缓存帖子失败:', post.id, err);
|
||
});
|
||
}
|
||
|
||
// 计算 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 {
|
||
// 详情页优先取最新服务端数据,避免旧缓存导致关注态等关系字段过期
|
||
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 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<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));
|
||
|
||
// 缓存帖子
|
||
for (const post of posts) {
|
||
this.saveToMemoryCache(post);
|
||
this.saveToLocalCache(post).catch(err => {
|
||
console.warn('[PostRepository] 缓存帖子失败:', post.id, err);
|
||
});
|
||
}
|
||
|
||
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.channelId) queryParams.channel_id = params.channelId;
|
||
|
||
const response = await this.api.get<PostsListApiResponse>('/posts/search', queryParams);
|
||
|
||
const posts = (response.list || []).map(p => this.mapToPost(p));
|
||
|
||
// 缓存帖子
|
||
for (const post of posts) {
|
||
this.saveToMemoryCache(post);
|
||
this.saveToLocalCache(post).catch(err => {
|
||
console.warn('[PostRepository] 缓存帖子失败:', post.id, err);
|
||
});
|
||
}
|
||
|
||
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.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<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.enqueueWrite(async () => {
|
||
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);
|
||
export default postRepository;
|