mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-22 17:13:05 +08:00
196 lines
5.0 KiB
TypeScript
196 lines
5.0 KiB
TypeScript
/**
|
||
* 会话级通用 JSON 缓存模块
|
||
*
|
||
* 每个 session 对应一个 {sessionId}.cache.json,内部按 key 分区存储。
|
||
* 与数据库 schema 完全解耦,读取失败自动重建,无需版本管理。
|
||
*
|
||
* 文件路径: {cacheDir}/{sessionId}.cache.json
|
||
*/
|
||
|
||
import * as fs from 'fs'
|
||
import * as path from 'path'
|
||
import type Database from 'better-sqlite3'
|
||
|
||
// ==================== 通用缓存基础设施 ====================
|
||
|
||
interface CacheEntry<T = unknown> {
|
||
data: T
|
||
ts: number
|
||
}
|
||
|
||
type CacheFile = Record<string, CacheEntry>
|
||
|
||
export function getCachePath(sessionId: string, cacheDir: string): string {
|
||
return path.join(cacheDir, `${sessionId}.cache.json`)
|
||
}
|
||
|
||
function readCacheFile(cachePath: string): CacheFile | null {
|
||
try {
|
||
if (!fs.existsSync(cachePath)) return null
|
||
return JSON.parse(fs.readFileSync(cachePath, 'utf-8')) as CacheFile
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function writeCacheFile(cachePath: string, content: CacheFile): void {
|
||
try {
|
||
const dir = path.dirname(cachePath)
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true })
|
||
}
|
||
fs.writeFileSync(cachePath, JSON.stringify(content), 'utf-8')
|
||
} catch {
|
||
// 写入失败不影响主流程
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取缓存分区
|
||
*/
|
||
export function getCache<T>(sessionId: string, key: string, cacheDir: string): T | null {
|
||
const cachePath = getCachePath(sessionId, cacheDir)
|
||
const file = readCacheFile(cachePath)
|
||
if (!file || !file[key]) return null
|
||
return file[key].data as T
|
||
}
|
||
|
||
/**
|
||
* 写入缓存分区
|
||
*/
|
||
export function setCache<T>(sessionId: string, key: string, data: T, cacheDir: string): void {
|
||
const cachePath = getCachePath(sessionId, cacheDir)
|
||
const file = readCacheFile(cachePath) ?? {}
|
||
file[key] = { data, ts: Math.floor(Date.now() / 1000) }
|
||
writeCacheFile(cachePath, file)
|
||
}
|
||
|
||
/**
|
||
* 使缓存失效(不传 key 则清空整个文件)
|
||
*/
|
||
export function invalidateCache(sessionId: string, cacheDir: string, key?: string): void {
|
||
const cachePath = getCachePath(sessionId, cacheDir)
|
||
try {
|
||
if (!key) {
|
||
if (fs.existsSync(cachePath)) fs.unlinkSync(cachePath)
|
||
} else {
|
||
const file = readCacheFile(cachePath)
|
||
if (file && file[key]) {
|
||
delete file[key]
|
||
writeCacheFile(cachePath, file)
|
||
}
|
||
}
|
||
} catch {
|
||
// 忽略失败
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除 session 的所有缓存
|
||
*/
|
||
export function deleteSessionCache(sessionId: string, cacheDir: string): void {
|
||
const cachePath = getCachePath(sessionId, cacheDir)
|
||
try {
|
||
if (fs.existsSync(cachePath)) fs.unlinkSync(cachePath)
|
||
} catch {
|
||
// 忽略删除失败
|
||
}
|
||
}
|
||
|
||
// ==================== Overview 缓存(聚合统计) ====================
|
||
|
||
export const CACHE_KEY_OVERVIEW = 'overview'
|
||
|
||
export interface OverviewCache {
|
||
totalMessages: number
|
||
totalMembers: number
|
||
firstMessageTs: number | null
|
||
lastMessageTs: number | null
|
||
}
|
||
|
||
/**
|
||
* 从数据库计算概览统计并写入缓存
|
||
*/
|
||
export function computeAndSetOverviewCache(
|
||
db: Database.Database,
|
||
sessionId: string,
|
||
cacheDir: string
|
||
): OverviewCache {
|
||
const msgStats = db.prepare('SELECT MIN(ts) as first_ts, MAX(ts) as last_ts FROM message').get() as {
|
||
first_ts: number | null
|
||
last_ts: number | null
|
||
}
|
||
|
||
const totalMessages = (
|
||
db
|
||
.prepare(
|
||
`SELECT COUNT(*) as count FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
WHERE COALESCE(m.account_name, '') != '系统消息'`
|
||
)
|
||
.get() as { count: number }
|
||
).count
|
||
|
||
const totalMembers = (
|
||
db.prepare(`SELECT COUNT(*) as count FROM member WHERE COALESCE(account_name, '') != '系统消息'`).get() as {
|
||
count: number
|
||
}
|
||
).count
|
||
|
||
const data: OverviewCache = {
|
||
totalMessages,
|
||
totalMembers,
|
||
firstMessageTs: msgStats.first_ts,
|
||
lastMessageTs: msgStats.last_ts,
|
||
}
|
||
|
||
setCache(sessionId, CACHE_KEY_OVERVIEW, data, cacheDir)
|
||
|
||
// 同步生成成员缓存
|
||
computeAndSetMembersCache(db, sessionId, cacheDir)
|
||
|
||
return data
|
||
}
|
||
|
||
// ==================== Members 缓存(成员维度) ====================
|
||
|
||
export const CACHE_KEY_MEMBERS = 'members'
|
||
|
||
export interface MemberStat {
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface MembersCache {
|
||
/** member.id -> { name, count } */
|
||
members: Record<number, MemberStat>
|
||
}
|
||
|
||
/**
|
||
* 从数据库计算成员统计并写入缓存
|
||
*/
|
||
export function computeAndSetMembersCache(
|
||
db: Database.Database,
|
||
sessionId: string,
|
||
cacheDir: string
|
||
): MembersCache {
|
||
const rows = db
|
||
.prepare(
|
||
`SELECT msg.sender_id, COUNT(*) as count,
|
||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as name
|
||
FROM message msg
|
||
JOIN member m ON msg.sender_id = m.id
|
||
GROUP BY msg.sender_id`
|
||
)
|
||
.all() as Array<{ sender_id: number; count: number; name: string }>
|
||
|
||
const members: Record<number, MemberStat> = {}
|
||
for (const row of rows) {
|
||
members[row.sender_id] = { name: row.name, count: row.count }
|
||
}
|
||
|
||
const data: MembersCache = { members }
|
||
setCache(sessionId, CACHE_KEY_MEMBERS, data, cacheDir)
|
||
return data
|
||
}
|