refactor(database): migrate to new modular database layer and unify data access
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m44s
Frontend CI / ota-android (push) Successful in 12m51s
Frontend CI / build-android-apk (push) Successful in 1h1m26s

- 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:
lafay
2026-04-04 08:01:45 +08:00
parent 189b977fac
commit 82c2970a85
76 changed files with 3382 additions and 2000 deletions

View File

@@ -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();

View File

@@ -5,5 +5,5 @@
export * from './interfaces';
export * from './ApiDataSource';
export * from './LocalDataSource';
export { LocalDataSource, localDataSource } from '@/database';
export * from './CacheDataSource';

View File

@@ -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>;
}
// 缓存数据源接口

View File

@@ -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;

View File

@@ -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;