refactor: 架构重构 - 解耦过度耦合模块
主要改动: 1. 创建乐观更新工具函数 (optimisticUpdate.ts) - 消除 userStore.ts 中的重复代码 2. 拆分 useResponsive.ts (485行 -> 12个专注模块) - useBreakpoint: 断点检测 - useOrientation: 方向检测 - usePlatform: 平台检测 - useScreenSize: 屏幕尺寸 - useResponsiveValue: 响应式值 - useResponsiveStyle: 响应式样式 - useMediaQuery: 媒体查询 - useColumnCount: 列数计算 - useResponsiveSpacing: 响应式间距 3. 整理数据层 (Repository 层) - ApiDataSource: API数据源 - LocalDataSource: 本地数据源 - CacheDataSource: 缓存数据源 - MessageRepository: 消息仓库 4. 重构 messageManager.ts (2194行 -> 4个模块) - MessageStateManager: 状态管理 - WebSocketMessageHandler: WebSocket处理 - MessageSyncService: 消息同步 - ReadReceiptManager: 已读管理 5. 导航解耦 (MainNavigator.tsx: 1118行 -> 100行) - 创建 NavigationService 解耦层 - 拆分多个 Navigator 组件 架构改进: - 单一职责原则: 每个模块职责明确 - 依赖倒置: 通过接口解耦 - 代码复用: 工具函数可被多处使用 - 可测试性: 各模块可独立测试
This commit is contained in:
281
src/data/datasources/LocalDataSource.ts
Normal file
281
src/data/datasources/LocalDataSource.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 本地数据库数据源实现
|
||||
* 封装所有 SQLite 操作,提供统一的错误处理和事务支持
|
||||
*/
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { ILocalDataSource, DataSourceError } from './interfaces';
|
||||
|
||||
// 数据库实例管理
|
||||
let dbInstance: SQLite.SQLiteDatabase | null = null;
|
||||
let currentDbName: string | null = null;
|
||||
let writeQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
export interface LocalDataSourceConfig {
|
||||
dbName?: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class LocalDataSource implements ILocalDataSource {
|
||||
private db: SQLite.SQLiteDatabase | null = null;
|
||||
private dbName: string;
|
||||
private initialized = false;
|
||||
|
||||
constructor(config: LocalDataSourceConfig = {}) {
|
||||
// 如果提供了userId,使用用户专属数据库
|
||||
this.dbName = config.dbName || (config.userId ? `carrot_bbs_${config.userId}.db` : 'carrot_bbs.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized && this.db) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用全局实例管理
|
||||
if (dbInstance && currentDbName === this.dbName) {
|
||||
this.db = dbInstance;
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭旧连接
|
||||
if (dbInstance) {
|
||||
try {
|
||||
await dbInstance.closeAsync();
|
||||
} catch (e) {
|
||||
console.warn('关闭旧数据库连接失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新连接
|
||||
this.db = await SQLite.openDatabaseAsync(this.dbName);
|
||||
dbInstance = this.db;
|
||||
currentDbName = this.dbName;
|
||||
this.initialized = true;
|
||||
|
||||
// 初始化数据库表结构
|
||||
await this.createTables();
|
||||
} catch (error) {
|
||||
this.handleError(error, 'INITIALIZE');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据库表结构
|
||||
*/
|
||||
private async createTables(): Promise<void> {
|
||||
if (!this.db) return;
|
||||
|
||||
const tables = [
|
||||
// 消息表
|
||||
`CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
conversationId TEXT NOT NULL,
|
||||
senderId TEXT NOT NULL,
|
||||
content TEXT,
|
||||
type TEXT DEFAULT 'text',
|
||||
isRead INTEGER DEFAULT 0,
|
||||
createdAt TEXT NOT NULL,
|
||||
seq INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'normal',
|
||||
segments TEXT
|
||||
)`,
|
||||
// 会话表
|
||||
`CREATE TABLE IF NOT EXISTS conversations (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
participantId TEXT NOT NULL,
|
||||
lastMessageId TEXT,
|
||||
lastSeq INTEGER DEFAULT 0,
|
||||
unreadCount INTEGER DEFAULT 0,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 会话缓存表
|
||||
`CREATE TABLE IF NOT EXISTS conversation_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 会话列表缓存表
|
||||
`CREATE TABLE IF NOT EXISTS conversation_list_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 用户缓存表
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 当前登录用户缓存
|
||||
`CREATE TABLE IF NOT EXISTS current_user_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 群组缓存表
|
||||
`CREATE TABLE IF NOT EXISTS groups (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
// 群成员缓存表
|
||||
`CREATE TABLE IF NOT EXISTS group_members (
|
||||
groupId TEXT NOT NULL,
|
||||
userId TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
PRIMARY KEY (groupId, userId)
|
||||
)`,
|
||||
];
|
||||
|
||||
for (const sql of tables) {
|
||||
await this.db.execAsync(sql);
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
const indexes = [
|
||||
`CREATE INDEX IF NOT EXISTS idx_messages_conversationId ON messages(conversationId)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_messages_conversation_seq ON messages(conversationId, seq)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_group_members_groupId ON group_members(groupId)`,
|
||||
];
|
||||
|
||||
for (const sql of indexes) {
|
||||
await this.db.execAsync(sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
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 {
|
||||
if (!this.db) {
|
||||
throw new DataSourceError(
|
||||
'Database not initialized',
|
||||
'DB_NOT_INITIALIZED',
|
||||
'LocalDataSource'
|
||||
);
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
|
||||
// ==================== ILocalDataSource 实现 ====================
|
||||
|
||||
async query<T>(sql: string, params?: any[]): Promise<T[]> {
|
||||
try {
|
||||
const db = this.ensureDb();
|
||||
return await db.getAllAsync<T>(sql, params);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'QUERY');
|
||||
}
|
||||
}
|
||||
|
||||
async getFirst<T>(sql: string, params?: any[]): Promise<T | null> {
|
||||
try {
|
||||
const db = this.ensureDb();
|
||||
return await db.getFirstAsync<T>(sql, params);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'GET_FIRST');
|
||||
}
|
||||
}
|
||||
|
||||
async execute(sql: string, params?: any[]): Promise<void> {
|
||||
try {
|
||||
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 {
|
||||
const db = this.ensureDb();
|
||||
return await db.runAsync(sql, params);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'RUN');
|
||||
}
|
||||
}
|
||||
|
||||
async transaction(operations: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
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> {
|
||||
const wrappedOperation = async () => {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
// 尝试重连后重试
|
||||
if (this.isRecoverableError(error)) {
|
||||
console.error('数据库写入异常,尝试重连后重试:', error);
|
||||
this.db = null;
|
||||
dbInstance = null;
|
||||
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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const localDataSource = new LocalDataSource();
|
||||
Reference in New Issue
Block a user