Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
936 lines
29 KiB
TypeScript
936 lines
29 KiB
TypeScript
/**
|
||
* 本地数据库服务
|
||
* 使用 SQLite 存储聊天记录
|
||
* 每个用户使用独立的数据库文件,避免数据串用
|
||
*/
|
||
|
||
import * as SQLite from 'expo-sqlite';
|
||
import type {
|
||
UserDTO,
|
||
GroupResponse,
|
||
GroupMemberResponse,
|
||
ConversationResponse,
|
||
ConversationDetailResponse,
|
||
} from '../types/dto';
|
||
|
||
// 数据库实例
|
||
let db: SQLite.SQLiteDatabase | null = null;
|
||
let writeQueue: Promise<void> = Promise.resolve();
|
||
|
||
// 当前数据库对应的用户ID
|
||
let currentDbUserId: string | null = null;
|
||
|
||
// 数据库版本,用于迁移
|
||
const DB_VERSION = 2;
|
||
|
||
/**
|
||
* 初始化用户数据库
|
||
* @param userId 用户ID,用于创建用户专属数据库文件
|
||
*/
|
||
export const initDatabase = async (userId?: string): Promise<void> => {
|
||
try {
|
||
// 如果指定了用户ID,使用用户专属数据库
|
||
// 否则使用默认数据库(兼容旧版本)
|
||
const dbName = userId ? `carrot_bbs_${userId}.db` : 'carrot_bbs.db';
|
||
|
||
// 如果存在旧的数据库连接且用户ID变化,需要关闭旧连接
|
||
if (db && currentDbUserId !== userId) {
|
||
console.log(`[Database] 切换用户数据库: ${currentDbUserId} -> ${userId}`);
|
||
await closeDatabase();
|
||
}
|
||
|
||
// 如果已经打开了正确的数据库,直接返回
|
||
if (db && currentDbUserId === userId) {
|
||
console.log(`[Database] 数据库已初始化: ${dbName}`);
|
||
return;
|
||
}
|
||
|
||
db = await SQLite.openDatabaseAsync(dbName);
|
||
currentDbUserId = userId || null;
|
||
console.log(`[Database] 打开数据库: ${dbName}`);
|
||
|
||
// 创建消息表(包含新字段 seq, status, segments)
|
||
await db.execAsync(`
|
||
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
|
||
);
|
||
`);
|
||
|
||
// 创建会话表
|
||
await db.execAsync(`
|
||
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
|
||
);
|
||
`);
|
||
|
||
// 会话缓存表(完整 JSON,兼容私聊/群聊不同结构)
|
||
await db.execAsync(`
|
||
CREATE TABLE IF NOT EXISTS conversation_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
);
|
||
`);
|
||
|
||
// 会话列表缓存表(仅用于 ChatList,避免被详情缓存污染)
|
||
await db.execAsync(`
|
||
CREATE TABLE IF NOT EXISTS conversation_list_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
);
|
||
`);
|
||
|
||
// 用户缓存表
|
||
await db.execAsync(`
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
);
|
||
`);
|
||
|
||
// 当前登录用户缓存(仅保存 me)
|
||
await db.execAsync(`
|
||
CREATE TABLE IF NOT EXISTS current_user_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
);
|
||
`);
|
||
|
||
// 群组缓存表
|
||
await db.execAsync(`
|
||
CREATE TABLE IF NOT EXISTS groups (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
);
|
||
`);
|
||
|
||
// 群成员缓存表
|
||
await db.execAsync(`
|
||
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)
|
||
);
|
||
`);
|
||
|
||
// 创建索引
|
||
await db.execAsync(`
|
||
CREATE INDEX IF NOT EXISTS idx_messages_conversationId ON messages(conversationId);
|
||
`);
|
||
await db.execAsync(`
|
||
CREATE INDEX IF NOT EXISTS idx_group_members_groupId ON group_members(groupId);
|
||
`);
|
||
|
||
// 迁移:检查并添加新列(兼容旧版本数据库)
|
||
await migrateDatabase();
|
||
|
||
// 迁移后再创建依赖新列的索引
|
||
await db.execAsync(`
|
||
CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq);
|
||
`);
|
||
await db.execAsync(`
|
||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_seq ON messages(conversationId, seq);
|
||
`);
|
||
|
||
console.log('数据库初始化成功');
|
||
} catch (error) {
|
||
console.error('数据库初始化失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 关闭数据库连接
|
||
*/
|
||
export const closeDatabase = async (): Promise<void> => {
|
||
if (db) {
|
||
try {
|
||
await db.closeAsync();
|
||
console.log('[Database] 数据库连接已关闭');
|
||
} catch (error) {
|
||
console.error('[Database] 关闭数据库失败:', error);
|
||
}
|
||
db = null;
|
||
currentDbUserId = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取当前数据库用户ID
|
||
*/
|
||
export const getCurrentDbUserId = (): string | null => {
|
||
return currentDbUserId;
|
||
};
|
||
|
||
// 数据库迁移
|
||
const migrateDatabase = async (): Promise<void> => {
|
||
const database = await getDb();
|
||
|
||
try {
|
||
// 检查 messages 表是否有 seq 列
|
||
const tableInfo = await database.getAllAsync<any>(
|
||
`PRAGMA table_info(messages)`
|
||
);
|
||
|
||
const columns = tableInfo.map(col => col.name);
|
||
|
||
// 添加 seq 列
|
||
if (!columns.includes('seq')) {
|
||
await database.execAsync(`ALTER TABLE messages ADD COLUMN seq INTEGER DEFAULT 0`);
|
||
console.log('数据库迁移:添加 seq 列');
|
||
}
|
||
|
||
// 添加 status 列
|
||
if (!columns.includes('status')) {
|
||
await database.execAsync(`ALTER TABLE messages ADD COLUMN status TEXT DEFAULT 'normal'`);
|
||
console.log('数据库迁移:添加 status 列');
|
||
}
|
||
|
||
// 添加 segments 列(用于存储消息的 segments JSON)
|
||
if (!columns.includes('segments')) {
|
||
await database.execAsync(`ALTER TABLE messages ADD COLUMN segments TEXT`);
|
||
console.log('数据库迁移:添加 segments 列');
|
||
}
|
||
|
||
// 检查 conversations 表是否有 lastSeq 列
|
||
const convTableInfo = await database.getAllAsync<any>(
|
||
`PRAGMA table_info(conversations)`
|
||
);
|
||
const convColumns = convTableInfo.map(col => col.name);
|
||
|
||
if (!convColumns.includes('lastSeq')) {
|
||
await database.execAsync(`ALTER TABLE conversations ADD COLUMN lastSeq INTEGER DEFAULT 0`);
|
||
console.log('数据库迁移:添加 lastSeq 列');
|
||
}
|
||
} catch (error) {
|
||
console.error('数据库迁移失败:', error);
|
||
}
|
||
};
|
||
|
||
// 获取数据库实例
|
||
const getDb = async (): Promise<SQLite.SQLiteDatabase> => {
|
||
if (!db) {
|
||
throw new Error('数据库未初始化,请先调用 initDatabase(userId)');
|
||
}
|
||
return db;
|
||
};
|
||
|
||
const isRecoverableDbError = (error: unknown): boolean => {
|
||
const message = String(error);
|
||
return (
|
||
message.includes('NativeDatabase.prepareAsync') ||
|
||
message.includes('NullPointerException')
|
||
);
|
||
};
|
||
|
||
const withDbRead = async <T>(operation: (database: SQLite.SQLiteDatabase) => Promise<T>): Promise<T> => {
|
||
let database = await getDb();
|
||
try {
|
||
return await operation(database);
|
||
} catch (error) {
|
||
if (!isRecoverableDbError(error)) {
|
||
throw error;
|
||
}
|
||
console.error('数据库读取异常,尝试重连后重试:', error);
|
||
db = null;
|
||
database = await getDb();
|
||
return operation(database);
|
||
}
|
||
};
|
||
|
||
const enqueueWrite = async <T>(operation: () => Promise<T>): Promise<T> => {
|
||
const wrappedOperation = async () => {
|
||
try {
|
||
return await operation();
|
||
} catch (error) {
|
||
if (!isRecoverableDbError(error)) {
|
||
throw error;
|
||
}
|
||
console.error('数据库写入异常,尝试重连后重试:', error);
|
||
db = null;
|
||
return operation();
|
||
}
|
||
};
|
||
const queued = writeQueue.then(wrappedOperation, wrappedOperation);
|
||
writeQueue = queued.then(() => undefined, () => undefined);
|
||
return queued;
|
||
};
|
||
|
||
// ==================== 消息类型定义 ====================
|
||
|
||
export interface CachedMessage {
|
||
id: string;
|
||
conversationId: string;
|
||
senderId: string;
|
||
content?: string;
|
||
type?: string;
|
||
isRead: boolean;
|
||
createdAt: string;
|
||
seq: number;
|
||
status: string;
|
||
segments?: any;
|
||
}
|
||
|
||
// ==================== 消息操作 ====================
|
||
|
||
// 保存单条消息
|
||
export const saveMessage = async (message: CachedMessage): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO messages (id, conversationId, senderId, content, type, isRead, createdAt, seq, status, segments)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
message.id,
|
||
message.conversationId,
|
||
message.senderId,
|
||
message.content || '',
|
||
message.type || 'text',
|
||
message.isRead ? 1 : 0,
|
||
message.createdAt,
|
||
message.seq || 0,
|
||
message.status || 'normal',
|
||
message.segments ? JSON.stringify(message.segments) : null,
|
||
]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 批量保存消息
|
||
export const saveMessagesBatch = async (messages: CachedMessage[]): Promise<void> => {
|
||
if (!messages || messages.length === 0) return;
|
||
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
for (const message of messages) {
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO messages (id, conversationId, senderId, content, type, isRead, createdAt, seq, status, segments)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
message.id,
|
||
message.conversationId,
|
||
message.senderId,
|
||
message.content || '',
|
||
message.type || 'text',
|
||
message.isRead ? 1 : 0,
|
||
message.createdAt,
|
||
message.seq || 0,
|
||
message.status || 'normal',
|
||
message.segments ? JSON.stringify(message.segments) : null,
|
||
]
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 获取会话的消息(默认只加载最近的20条,避免卡顿)
|
||
export const getMessagesByConversation = async (
|
||
conversationId: string,
|
||
limit: number = 20
|
||
): Promise<CachedMessage[]> => {
|
||
return withDbRead(async (database) => {
|
||
const result = await database.getAllAsync<any>(
|
||
`SELECT * FROM messages
|
||
WHERE conversationId = ?
|
||
ORDER BY seq DESC
|
||
LIMIT ?`,
|
||
[conversationId, limit]
|
||
);
|
||
|
||
// 反转以保持时间正序(最新的在底部)
|
||
return result.reverse().map(msg => ({
|
||
...msg,
|
||
isRead: msg.isRead === 1,
|
||
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
|
||
}));
|
||
});
|
||
};
|
||
|
||
// 获取会话的最大消息序号
|
||
export const getMaxSeq = async (conversationId: string): Promise<number> => {
|
||
return withDbRead(async (database) => {
|
||
const result = await database.getFirstAsync<{ maxSeq: number }>(
|
||
`SELECT MAX(seq) as maxSeq FROM messages WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
|
||
return result?.maxSeq || 0;
|
||
});
|
||
};
|
||
|
||
// 获取会话的最小消息序号
|
||
export const getMinSeq = async (conversationId: string): Promise<number> => {
|
||
return withDbRead(async (database) => {
|
||
const result = await database.getFirstAsync<{ minSeq: number }>(
|
||
`SELECT MIN(seq) as minSeq FROM messages WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
|
||
return result?.minSeq || 0;
|
||
});
|
||
};
|
||
|
||
// 获取指定 seq 之前的消息(用于加载历史)
|
||
export const getMessagesBeforeSeq = async (conversationId: string, beforeSeq: number, limit: number = 20): Promise<CachedMessage[]> => {
|
||
return withDbRead(async (database) => {
|
||
const result = await database.getAllAsync<any>(
|
||
`SELECT * FROM messages
|
||
WHERE conversationId = ? AND seq < ?
|
||
ORDER BY seq DESC
|
||
LIMIT ?`,
|
||
[conversationId, beforeSeq, limit]
|
||
);
|
||
|
||
return result.map(msg => ({
|
||
...msg,
|
||
isRead: msg.isRead === 1,
|
||
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
|
||
})).reverse(); // 反转以保持时间正序
|
||
});
|
||
};
|
||
|
||
// 获取本地消息数量(用于判断是否还有更多历史)
|
||
export const getLocalMessageCountBeforeSeq = async (conversationId: string, beforeSeq: number): Promise<number> => {
|
||
const database = await getDb();
|
||
|
||
const result = await database.getFirstAsync<{ count: number }>(
|
||
`SELECT COUNT(*) as count FROM messages WHERE conversationId = ? AND seq < ?`,
|
||
[conversationId, beforeSeq]
|
||
);
|
||
|
||
return result?.count || 0;
|
||
};
|
||
|
||
// 获取消息数量
|
||
export const getMessageCount = async (conversationId: string): Promise<number> => {
|
||
const database = await getDb();
|
||
|
||
const result = await database.getFirstAsync<{ count: number }>(
|
||
`SELECT COUNT(*) as count FROM messages WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
|
||
return result?.count || 0;
|
||
};
|
||
|
||
// 标记消息为已读
|
||
export const markMessageAsRead = async (messageId: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`UPDATE messages SET isRead = 1 WHERE id = ?`,
|
||
[messageId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 标记会话的所有消息为已读
|
||
export const markConversationAsRead = async (conversationId: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`UPDATE messages SET isRead = 1 WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 删除单条消息
|
||
export const deleteMessage = async (messageId: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`DELETE FROM messages WHERE id = ?`,
|
||
[messageId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 更新消息状态(如撤回)
|
||
export const updateMessageStatus = async (messageId: string, status: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`UPDATE messages SET status = ? WHERE id = ?`,
|
||
[status, messageId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 清空会话的所有消息
|
||
export const clearConversationMessages = async (conversationId: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`DELETE FROM messages WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// ==================== 会话操作 ====================
|
||
|
||
// 保存或更新会话
|
||
export const saveConversation = async (conversation: {
|
||
id: string;
|
||
participantId: string;
|
||
lastMessageId?: string;
|
||
lastSeq?: number;
|
||
unreadCount?: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO conversations (id, participantId, lastMessageId, lastSeq, unreadCount, createdAt, updatedAt)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||
[
|
||
conversation.id,
|
||
conversation.participantId,
|
||
conversation.lastMessageId || null,
|
||
conversation.lastSeq || 0,
|
||
conversation.unreadCount || 0,
|
||
conversation.createdAt,
|
||
conversation.updatedAt,
|
||
]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 获取所有会话
|
||
export const getAllConversations = async (): Promise<any[]> => {
|
||
const database = await getDb();
|
||
|
||
const result = await database.getAllAsync<any>(
|
||
`SELECT * FROM conversations ORDER BY updatedAt DESC`
|
||
);
|
||
|
||
return result;
|
||
};
|
||
|
||
// 更新会话未读数
|
||
export const updateConversationUnreadCount = async (conversationId: string, count: number): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`UPDATE conversations SET unreadCount = ? WHERE id = ?`,
|
||
[count, conversationId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 更新会话最后消息
|
||
export const updateConversationLastMessage = async (
|
||
conversationId: string,
|
||
lastMessageId: string,
|
||
lastSeq: number,
|
||
updatedAt: string
|
||
): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`UPDATE conversations SET lastMessageId = ?, lastSeq = ?, updatedAt = ? WHERE id = ?`,
|
||
[lastMessageId, lastSeq, updatedAt, conversationId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// 删除会话(包含所有消息)
|
||
export const deleteConversation = async (conversationId: string): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`DELETE FROM messages WHERE conversationId = ?`,
|
||
[conversationId]
|
||
);
|
||
await database.runAsync(
|
||
`DELETE FROM conversations WHERE id = ?`,
|
||
[conversationId]
|
||
);
|
||
});
|
||
};
|
||
|
||
// ==================== 搜索 ====================
|
||
|
||
// 搜索消息
|
||
export const searchMessages = async (keyword: string): Promise<CachedMessage[]> => {
|
||
const database = await getDb();
|
||
|
||
const result = await database.getAllAsync<any>(
|
||
`SELECT * FROM messages
|
||
WHERE content LIKE ?
|
||
ORDER BY createdAt DESC
|
||
LIMIT 50`,
|
||
[`%${keyword}%`]
|
||
);
|
||
|
||
return result.map(msg => ({
|
||
...msg,
|
||
isRead: msg.isRead === 1,
|
||
segments: msg.segments ? JSON.parse(msg.segments) : undefined,
|
||
}));
|
||
};
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
// 获取数据库统计信息
|
||
export const getDatabaseStats = async (): Promise<{
|
||
totalMessages: number;
|
||
totalConversations: number;
|
||
}> => {
|
||
const database = await getDb();
|
||
|
||
const msgCount = await database.getFirstAsync<{ count: number }>(
|
||
`SELECT COUNT(*) as count FROM messages`
|
||
);
|
||
|
||
const convCount = await database.getFirstAsync<{ count: number }>(
|
||
`SELECT COUNT(*) as count FROM conversations`
|
||
);
|
||
|
||
return {
|
||
totalMessages: msgCount?.count || 0,
|
||
totalConversations: convCount?.count || 0,
|
||
};
|
||
};
|
||
|
||
// 清空所有数据(用于调试或用户退出登录)
|
||
export const clearAllData = async (): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.execAsync(`DELETE FROM messages`);
|
||
await database.execAsync(`DELETE FROM conversations`);
|
||
await database.execAsync(`DELETE FROM conversation_cache`);
|
||
await database.execAsync(`DELETE FROM conversation_list_cache`);
|
||
await database.execAsync(`DELETE FROM users`);
|
||
await database.execAsync(`DELETE FROM current_user_cache`);
|
||
await database.execAsync(`DELETE FROM groups`);
|
||
await database.execAsync(`DELETE FROM group_members`);
|
||
});
|
||
};
|
||
|
||
// ==================== 通用 JSON 缓存辅助 ====================
|
||
|
||
const safeParseJson = <T>(value: string): T | null => {
|
||
try {
|
||
return JSON.parse(value) as T;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// ==================== 用户缓存 ====================
|
||
|
||
export const saveUserCache = async (user: UserDTO): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO users (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[String(user.id), JSON.stringify(user), new Date().toISOString()]
|
||
);
|
||
});
|
||
};
|
||
|
||
export const saveUsersCache = async (users: UserDTO[]): Promise<void> => {
|
||
if (!users || users.length === 0) return;
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
for (const user of users) {
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO users (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[String(user.id), JSON.stringify(user), new Date().toISOString()]
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
export const getUserCache = async (userId: string): Promise<UserDTO | null> => {
|
||
return withDbRead(async (database) => {
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM users WHERE id = ?`,
|
||
[String(userId)]
|
||
);
|
||
if (!row?.data) return null;
|
||
return safeParseJson<UserDTO>(row.data);
|
||
});
|
||
};
|
||
|
||
export const getLatestUserCache = async (): Promise<UserDTO | null> => {
|
||
const database = await getDb();
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM users ORDER BY updatedAt DESC LIMIT 1`
|
||
);
|
||
if (!row?.data) return null;
|
||
return safeParseJson<UserDTO>(row.data);
|
||
};
|
||
|
||
export const saveCurrentUserCache = async (user: UserDTO): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO current_user_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
['me', JSON.stringify(user), new Date().toISOString()]
|
||
);
|
||
});
|
||
};
|
||
|
||
export const getCurrentUserCache = async (): Promise<UserDTO | null> => {
|
||
return withDbRead(async (database) => {
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM current_user_cache WHERE id = 'me'`
|
||
);
|
||
if (!row?.data) return null;
|
||
return safeParseJson<UserDTO>(row.data);
|
||
});
|
||
};
|
||
|
||
export const clearCurrentUserCache = async (): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(`DELETE FROM current_user_cache WHERE id = 'me'`);
|
||
});
|
||
};
|
||
|
||
// ==================== 群组缓存 ====================
|
||
|
||
export const saveGroupCache = async (group: GroupResponse): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO groups (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[String(group.id), JSON.stringify(group), new Date().toISOString()]
|
||
);
|
||
});
|
||
};
|
||
|
||
export const saveGroupsCache = async (groups: GroupResponse[]): Promise<void> => {
|
||
if (!groups || groups.length === 0) return;
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
for (const group of groups) {
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO groups (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[String(group.id), JSON.stringify(group), new Date().toISOString()]
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
export const getGroupCache = async (groupId: string): Promise<GroupResponse | null> => {
|
||
return withDbRead(async (database) => {
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM groups WHERE id = ?`,
|
||
[String(groupId)]
|
||
);
|
||
if (!row?.data) return null;
|
||
return safeParseJson<GroupResponse>(row.data);
|
||
});
|
||
};
|
||
|
||
export const getAllGroupsCache = async (): Promise<GroupResponse[]> => {
|
||
return withDbRead(async (database) => {
|
||
const rows = await database.getAllAsync<{ data: string }>(
|
||
`SELECT data FROM groups ORDER BY updatedAt DESC`
|
||
);
|
||
return rows
|
||
.map(row => safeParseJson<GroupResponse>(row.data))
|
||
.filter((item): item is GroupResponse => Boolean(item));
|
||
});
|
||
};
|
||
|
||
// ==================== 群成员缓存 ====================
|
||
|
||
export const saveGroupMembersCache = async (
|
||
groupId: string,
|
||
members: GroupMemberResponse[]
|
||
): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
const now = new Date().toISOString();
|
||
await database.runAsync(`DELETE FROM group_members WHERE groupId = ?`, [String(groupId)]);
|
||
for (const member of members) {
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO group_members (groupId, userId, data, updatedAt) VALUES (?, ?, ?, ?)`,
|
||
[String(groupId), String(member.user_id), JSON.stringify(member), now]
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
export const getGroupMembersCache = async (groupId: string): Promise<GroupMemberResponse[]> => {
|
||
return withDbRead(async (database) => {
|
||
const rows = await database.getAllAsync<{ data: string }>(
|
||
`SELECT data FROM group_members WHERE groupId = ? ORDER BY updatedAt DESC`,
|
||
[String(groupId)]
|
||
);
|
||
return rows
|
||
.map(row => safeParseJson<GroupMemberResponse>(row.data))
|
||
.filter((item): item is GroupMemberResponse => Boolean(item));
|
||
});
|
||
};
|
||
|
||
// ==================== 会话缓存 ====================
|
||
|
||
type ConversationCacheData = ConversationResponse | ConversationDetailResponse;
|
||
|
||
export const saveConversationCache = async (conversation: ConversationCacheData): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
const cacheUpdatedAt = ('updated_at' in conversation && conversation.updated_at)
|
||
? String(conversation.updated_at)
|
||
: new Date().toISOString();
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO conversation_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[String(conversation.id), JSON.stringify(conversation), cacheUpdatedAt]
|
||
);
|
||
});
|
||
};
|
||
|
||
export const saveConversationsCache = async (conversations: ConversationResponse[]): Promise<void> => {
|
||
if (!conversations || conversations.length === 0) return;
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
for (const conversation of conversations) {
|
||
await database.runAsync(
|
||
`INSERT OR REPLACE INTO conversation_list_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||
[
|
||
String(conversation.id),
|
||
JSON.stringify(conversation),
|
||
conversation.updated_at || new Date().toISOString(),
|
||
]
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
export const getConversationCache = async (conversationId: string): Promise<ConversationCacheData | null> => {
|
||
return withDbRead(async (database) => {
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_cache WHERE id = ?`,
|
||
[String(conversationId)]
|
||
);
|
||
if (!row?.data) return null;
|
||
return safeParseJson<ConversationCacheData>(row.data);
|
||
});
|
||
};
|
||
|
||
export const getConversationListCache = async (): Promise<ConversationResponse[]> => {
|
||
return withDbRead(async (database) => {
|
||
let rows = await database.getAllAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_list_cache ORDER BY updatedAt DESC`
|
||
);
|
||
// 兼容旧版本:首次升级时尝试从旧表读取一次
|
||
if (rows.length === 0) {
|
||
rows = await database.getAllAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_cache ORDER BY updatedAt DESC`
|
||
);
|
||
}
|
||
const list = rows
|
||
.map(row => safeParseJson<ConversationResponse>(row.data))
|
||
.filter((item): item is ConversationResponse => Boolean(item))
|
||
.filter(item => typeof item.id !== 'undefined' && typeof item.type !== 'undefined');
|
||
return list.sort((a, b) => {
|
||
const aPinned = a.is_pinned ? 1 : 0;
|
||
const bPinned = b.is_pinned ? 1 : 0;
|
||
if (aPinned !== bPinned) {
|
||
return bPinned - aPinned;
|
||
}
|
||
const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime();
|
||
const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime();
|
||
return bTime - aTime;
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 局部更新 conversation_list_cache 中的某条会话(如 last_message、last_message_at、unread_count 等)
|
||
* 用于:发送消息后立即更新缓存中的最后一条消息,避免 SWR 返回旧数据
|
||
*/
|
||
export const updateConversationListCacheEntry = async (
|
||
conversationId: string,
|
||
updates: Partial<ConversationResponse>
|
||
): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
const row = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_list_cache WHERE id = ?`,
|
||
[String(conversationId)]
|
||
);
|
||
if (!row?.data) return;
|
||
const conv = safeParseJson<ConversationResponse>(row.data);
|
||
if (!conv) return;
|
||
const updated = { ...conv, ...updates };
|
||
const updatedAt = updates.last_message_at || updated.last_message_at || new Date().toISOString();
|
||
await database.runAsync(
|
||
`UPDATE conversation_list_cache SET data = ?, updatedAt = ? WHERE id = ?`,
|
||
[JSON.stringify(updated), updatedAt, String(conversationId)]
|
||
);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 更新会话 SWR 缓存中的未读数(标记已读后立即同步清零,避免下次从缓存加载时仍显示旧红点)
|
||
*/
|
||
export const updateConversationCacheUnreadCount = async (
|
||
conversationId: string,
|
||
count: number
|
||
): Promise<void> => {
|
||
await enqueueWrite(async () => {
|
||
const database = await getDb();
|
||
|
||
// 更新 conversation_list_cache
|
||
const listRow = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_list_cache WHERE id = ?`,
|
||
[String(conversationId)]
|
||
);
|
||
if (listRow?.data) {
|
||
const conv = safeParseJson<ConversationResponse>(listRow.data);
|
||
if (conv) {
|
||
conv.unread_count = count;
|
||
await database.runAsync(
|
||
`UPDATE conversation_list_cache SET data = ? WHERE id = ?`,
|
||
[JSON.stringify(conv), String(conversationId)]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 更新 conversation_cache
|
||
const cacheRow = await database.getFirstAsync<{ data: string }>(
|
||
`SELECT data FROM conversation_cache WHERE id = ?`,
|
||
[String(conversationId)]
|
||
);
|
||
if (cacheRow?.data) {
|
||
const conv = safeParseJson<Record<string, unknown>>(cacheRow.data);
|
||
if (conv) {
|
||
conv.unread_count = count;
|
||
await database.runAsync(
|
||
`UPDATE conversation_cache SET data = ? WHERE id = ?`,
|
||
[JSON.stringify(conv), String(conversationId)]
|
||
);
|
||
}
|
||
}
|
||
});
|
||
};
|