Files
frontend/src/data/datasources/LocalDataSource.ts
lafay de94447844
Some checks failed
Frontend CI / build-android-apk (push) Has been cancelled
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / ota-android (push) Has been cancelled
fix(frontend): 修复 TypeScript 类型错误,与后端 API 响应结构对齐
- 修复 UserDTO 类型定义,字段改为非 nullable 类型
- 修复 PostMapper 使用正确的 snake_case 字段名
- 修复 UserMapper 移除不存在的 is_blocked 和 updated_at 字段
- 修复 MessageMapper 使用 segments 替代已移除的字段
- 修复 MessageSyncService 正确处理 API 响应类型
- 修复 LocalDataSource SQLite 参数类型问题
- 修复导航相关类型错误
- 修复 PostDetailScreen 和 EditProfileScreen 的 null/undefined 类型不匹配
2026-03-19 10:53:09 +08:00

291 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 本地数据库数据源实现
* 封装所有 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();
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 {
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 {
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();
if (params && params.length > 0) {
return await db.runAsync(sql, params as any);
}
return await db.runAsync(sql);
} 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();