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

576 lines
16 KiB
TypeScript
Raw Normal View History

/**
* 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 {
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
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?: {
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
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;