refactor(database): migrate to new modular database layer and unify data access
- Remove legacy database.ts, LocalDataSource.ts, and MessageRepository.ts - Create new src/database/ module with messageRepository, userCacheRepository, conversationRepository, and groupCacheRepository - Update all consumers to import from @/database instead of services/database - Add web platform blur handling for modal components to fix focus issues - Flatten SystemMessageItem and NotificationsScreen styles for consistent design - Add draggable slider in ChatSettingsScreen and dynamic font size support - Introduce 9 new chat color themes - Add profile screens for about, terms, and privacy policy with navigation routes - Add policy links to login and registration screens - Fix post share URL format from /posts/ to /post/
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* 本地数据库数据源实现
|
||||
* 复用 database.ts 的共享连接,避免 OPFS 句柄冲突
|
||||
*/
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { ILocalDataSource, DataSourceError } from './interfaces';
|
||||
import {
|
||||
initDatabase,
|
||||
getCurrentDbUserId,
|
||||
isDbInitialized,
|
||||
getDbInstance,
|
||||
} from '../../services/database';
|
||||
|
||||
let writeQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
export interface LocalDataSourceConfig {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class LocalDataSource implements ILocalDataSource {
|
||||
private userId: string | null = null;
|
||||
private initialized = false;
|
||||
private initializingPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(config: LocalDataSourceConfig = {}) {
|
||||
this.userId = config.userId || null;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized && isDbInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.initializingPromise) {
|
||||
await this.initializingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializingPromise = this.doInitialize();
|
||||
try {
|
||||
await this.initializingPromise;
|
||||
} finally {
|
||||
this.initializingPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async doInitialize(): Promise<void> {
|
||||
try {
|
||||
// 如果 database.ts 已经初始化,复用它的连接
|
||||
if (isDbInitialized()) {
|
||||
const currentUserId = getCurrentDbUserId();
|
||||
if (this.userId && this.userId !== currentUserId) {
|
||||
await initDatabase(this.userId);
|
||||
}
|
||||
} else if (this.userId) {
|
||||
await initDatabase(this.userId);
|
||||
} else {
|
||||
const currentUserId = getCurrentDbUserId();
|
||||
if (!currentUserId) {
|
||||
console.warn('[LocalDataSource] 没有用户登录,跳过初始化');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDbInstance();
|
||||
if (db) {
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS posts_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'INITIALIZE');
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown, operation: string): never {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new DataSourceError(
|
||||
`Database ${operation} failed: ${message}`,
|
||||
'DB_ERROR',
|
||||
'LocalDataSource',
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private ensureDb(): SQLite.SQLiteDatabase {
|
||||
const db = getDbInstance();
|
||||
if (!db) {
|
||||
throw new DataSourceError(
|
||||
'Database not initialized',
|
||||
'DB_NOT_INITIALIZED',
|
||||
'LocalDataSource'
|
||||
);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
async query<T>(sql: string, params?: any[]): Promise<T[]> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.getAllAsync<T>(sql, params as any);
|
||||
}
|
||||
return await db.getAllAsync<T>(sql);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'QUERY');
|
||||
}
|
||||
}
|
||||
|
||||
async getFirst<T>(sql: string, params?: any[]): Promise<T | null> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.getFirstAsync<T>(sql, params as any);
|
||||
}
|
||||
return await db.getFirstAsync<T>(sql);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'GET_FIRST');
|
||||
}
|
||||
}
|
||||
|
||||
async execute(sql: string, params?: any[]): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
await db.execAsync(sql);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'EXECUTE');
|
||||
}
|
||||
}
|
||||
|
||||
async run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.runAsync(sql, params as any);
|
||||
}
|
||||
return await db.runAsync(sql);
|
||||
} catch (error) {
|
||||
if (this.isRecoverableError(error)) {
|
||||
this.initialized = false;
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.runAsync(sql, params as any);
|
||||
}
|
||||
return await db.runAsync(sql);
|
||||
}
|
||||
this.handleError(error, 'RUN');
|
||||
}
|
||||
}
|
||||
|
||||
async transaction(operations: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
await db.withTransactionAsync(async () => {
|
||||
await operations();
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError(error, 'TRANSACTION');
|
||||
}
|
||||
}
|
||||
|
||||
async batch(operations: (db: ILocalDataSource) => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await this.transaction(async () => {
|
||||
await operations(this);
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError(error, 'BATCH');
|
||||
}
|
||||
}
|
||||
|
||||
async enqueueWrite<T>(operation: () => Promise<T>): Promise<T> {
|
||||
await this.initialize();
|
||||
const wrappedOperation = async () => {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (this.isRecoverableError(error)) {
|
||||
console.error('[LocalDataSource] 数据库写入异常,尝试重连后重试:', error);
|
||||
this.initialized = false;
|
||||
await this.initialize();
|
||||
return operation();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const queued = writeQueue.then(wrappedOperation, wrappedOperation);
|
||||
writeQueue = queued.then(() => undefined, () => undefined);
|
||||
return queued;
|
||||
}
|
||||
|
||||
private isRecoverableError(error: unknown): boolean {
|
||||
const message = String(error);
|
||||
return (
|
||||
message.includes('NativeDatabase.prepareAsync') ||
|
||||
message.includes('NullPointerException') ||
|
||||
message.includes('database is locked') ||
|
||||
message.includes('finalizing statement') ||
|
||||
message.includes('cannot create file')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const localDataSource = new LocalDataSource();
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
export * from './interfaces';
|
||||
export * from './ApiDataSource';
|
||||
export * from './LocalDataSource';
|
||||
export { LocalDataSource, localDataSource } from '@/database';
|
||||
export * from './CacheDataSource';
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface ILocalDataSource {
|
||||
run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }>;
|
||||
transaction(operations: () => Promise<void>): Promise<void>;
|
||||
batch(operations: (db: ILocalDataSource) => Promise<void>): Promise<void>;
|
||||
enqueueWrite<T>(operation: () => Promise<T>): Promise<T>;
|
||||
initialize(userId?: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 缓存数据源接口
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
/**
|
||||
* MessageRepository - 消息仓库实现
|
||||
* 封装所有SQLite数据库操作,不依赖任何UI或状态管理
|
||||
*/
|
||||
|
||||
import {
|
||||
saveMessage as dbSaveMessage,
|
||||
saveMessagesBatch as dbSaveMessagesBatch,
|
||||
getMessagesByConversation as dbGetMessagesByConversation,
|
||||
getMaxSeq as dbGetMaxSeq,
|
||||
getMinSeq as dbGetMinSeq,
|
||||
getMessagesBeforeSeq as dbGetMessagesBeforeSeq,
|
||||
markConversationAsRead as dbMarkConversationAsRead,
|
||||
updateConversationCacheUnreadCount as dbUpdateConversationCacheUnreadCount,
|
||||
getUserCache as dbGetUserCache,
|
||||
saveUserCache as dbSaveUserCache,
|
||||
deleteConversation as dbDeleteConversation,
|
||||
updateMessageStatus as dbUpdateMessageStatus,
|
||||
CachedMessage,
|
||||
} from '../../services/database';
|
||||
import { Message, Conversation, createMessage, createConversation } from '../../core/entities/Message';
|
||||
|
||||
// 数据库消息到领域实体的转换
|
||||
const cachedMessageToMessage = (cached: CachedMessage): Message => ({
|
||||
id: cached.id,
|
||||
conversationId: cached.conversationId,
|
||||
senderId: cached.senderId,
|
||||
seq: cached.seq,
|
||||
segments: cached.segments || [],
|
||||
createdAt: cached.createdAt,
|
||||
status: cached.status as Message['status'],
|
||||
});
|
||||
|
||||
// 领域实体到数据库消息的转换
|
||||
const messageToCachedMessage = (message: Message, isRead: boolean): CachedMessage => ({
|
||||
id: message.id,
|
||||
conversationId: message.conversationId,
|
||||
senderId: message.senderId,
|
||||
content: message.segments
|
||||
.filter((s) => s.type === 'text')
|
||||
.map((s) => s.data?.text || '')
|
||||
.join(''),
|
||||
type: message.segments.find((s) => s.type === 'image') ? 'image' : 'text',
|
||||
isRead,
|
||||
createdAt: message.createdAt,
|
||||
seq: message.seq,
|
||||
status: message.status,
|
||||
segments: message.segments,
|
||||
});
|
||||
|
||||
class MessageRepository {
|
||||
// ==================== 消息操作 ====================
|
||||
|
||||
/**
|
||||
* 保存单条消息
|
||||
*/
|
||||
async saveMessage(message: Message, isRead: boolean): Promise<void> {
|
||||
try {
|
||||
const cachedMessage = messageToCachedMessage(message, isRead);
|
||||
await dbSaveMessage(cachedMessage);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 保存消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存消息
|
||||
*/
|
||||
async saveMessages(messages: Message[], isRead: boolean): Promise<void> {
|
||||
if (!messages || messages.length === 0) return;
|
||||
|
||||
try {
|
||||
const cachedMessages = messages.map((m) => messageToCachedMessage(m, isRead));
|
||||
await dbSaveMessagesBatch(cachedMessages);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 批量保存消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的消息
|
||||
*/
|
||||
async getMessagesByConversation(
|
||||
conversationId: string,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
try {
|
||||
const cachedMessages = await dbGetMessagesByConversation(conversationId, limit);
|
||||
return cachedMessages.map(cachedMessageToMessage);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 获取消息失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的最大消息序号
|
||||
*/
|
||||
async getMaxSeq(conversationId: string): Promise<number> {
|
||||
try {
|
||||
return await dbGetMaxSeq(conversationId);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 获取最大序号失败:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的最小消息序号
|
||||
*/
|
||||
async getMinSeq(conversationId: string): Promise<number> {
|
||||
try {
|
||||
return await dbGetMinSeq(conversationId);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 获取最小序号失败:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定seq之前的历史消息
|
||||
*/
|
||||
async getMessagesBeforeSeq(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
try {
|
||||
const cachedMessages = await dbGetMessagesBeforeSeq(conversationId, beforeSeq, limit);
|
||||
return cachedMessages.map(cachedMessageToMessage);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 获取历史消息失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记会话的所有消息为已读
|
||||
*/
|
||||
async markConversationAsRead(conversationId: string): Promise<void> {
|
||||
try {
|
||||
await dbMarkConversationAsRead(conversationId);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 标记会话已读失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话缓存的未读数
|
||||
*/
|
||||
async updateConversationUnreadCount(
|
||||
conversationId: string,
|
||||
count: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
await dbUpdateConversationCacheUnreadCount(conversationId, count);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 更新会话未读数失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态(如撤回)
|
||||
*/
|
||||
async updateMessageStatus(
|
||||
messageId: string,
|
||||
status: string,
|
||||
clearContent: boolean = false
|
||||
): Promise<void> {
|
||||
try {
|
||||
await dbUpdateMessageStatus(messageId, status, clearContent);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 更新消息状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话及其所有消息
|
||||
*/
|
||||
async deleteConversation(conversationId: string): Promise<void> {
|
||||
try {
|
||||
await dbDeleteConversation(conversationId);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 删除会话失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户缓存操作 ====================
|
||||
|
||||
/**
|
||||
* 获取用户缓存
|
||||
*/
|
||||
async getUserCache(userId: string): Promise<any | null> {
|
||||
try {
|
||||
return await dbGetUserCache(userId);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 获取用户缓存失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户缓存
|
||||
*/
|
||||
async saveUserCache(user: any): Promise<void> {
|
||||
try {
|
||||
await dbSaveUserCache(user);
|
||||
} catch (error) {
|
||||
console.error('[MessageRepository] 保存用户缓存失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const messageRepository = new MessageRepository();
|
||||
export default messageRepository;
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ApiDataSource, apiDataSource } from '../datasources/ApiDataSource';
|
||||
import { LocalDataSource, localDataSource } from '../datasources/LocalDataSource';
|
||||
import { localDataSource } from '@/database';
|
||||
import { PostMapper } from '../mappers/PostMapper';
|
||||
import type { Post, PostImage, PostStatus } from '../../core/entities/Post';
|
||||
import { createPost } from '../../core/entities/Post';
|
||||
@@ -79,13 +79,12 @@ interface CachedPost {
|
||||
|
||||
export class PostRepository implements IPostRepository {
|
||||
private api: ApiDataSource;
|
||||
private localDb: LocalDataSource;
|
||||
private localDb = localDataSource;
|
||||
private memoryCache: Map<string, { post: Post; timestamp: number }> = new Map();
|
||||
private readonly CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存过期时间
|
||||
private readonly CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
constructor(api: ApiDataSource, localDb: LocalDataSource) {
|
||||
constructor(api: ApiDataSource) {
|
||||
this.api = api;
|
||||
this.localDb = localDb;
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
@@ -575,5 +574,5 @@ export class PostRepository implements IPostRepository {
|
||||
|
||||
// ==================== 单例导出 ====================
|
||||
|
||||
export const postRepository = new PostRepository(apiDataSource, localDataSource);
|
||||
export const postRepository = new PostRepository(apiDataSource);
|
||||
export default postRepository;
|
||||
|
||||
Reference in New Issue
Block a user