package fqapi import ( "database/sql" "encoding/json" "fanqie/internal/common" "fanqie/internal/config" "fmt" "sync" "time" _ "github.com/mattn/go-sqlite3" ) var ( cacheDB *sql.DB cacheMutex sync.Mutex cacheLogger = common.NewLogger("cache_manager", "INFO", "") contentRequestCount int // content接口访问计数 countMutex sync.Mutex cacheCleanupTicker *time.Ticker // 缓存清理定时器 ) // CacheData 缓存数据结构 type CacheData struct { Data string `json:"data"` Expi float64 `json:"expi"` } // InitDB 初始化数据库 func InitDB() error { if cacheDB != nil { return nil // 已初始化 } // 使用配置的路径 dbPath := "./cache.db" cfg := config.GetConfig() if cfg != nil && cfg.Cache.SQLitePath != "" { dbPath = cfg.Cache.SQLitePath } var err error cacheDB, err = sql.Open("sqlite3", dbPath) if err != nil { cacheLogger.Error("打开数据库失败: " + err.Error()) return err } // 创建缓存表(添加created_at字段用于清理) _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS cache ( key TEXT PRIMARY KEY, value TEXT, created_at INTEGER DEFAULT (strftime('%s', 'now')) )`) if err != nil { cacheLogger.Error("创建缓存表失败: " + err.Error()) return err } // 创建statistics表(保存请求统计,始终使用) _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS statistics ( id INTEGER PRIMARY KEY CHECK (id = 1), total_requests INTEGER DEFAULT 0, search_requests INTEGER DEFAULT 0, detail_requests INTEGER DEFAULT 0, catalog_requests INTEGER DEFAULT 0, content_requests INTEGER DEFAULT 0, manga_requests INTEGER DEFAULT 0, audio_requests INTEGER DEFAULT 0, video_requests INTEGER DEFAULT 0, category_requests INTEGER DEFAULT 0, updated_at INTEGER DEFAULT (strftime('%s', 'now')) )`) if err != nil { cacheLogger.Error("创建statistics表失败: " + err.Error()) return err } // 初始化statistics表(如果不存在记录) _, err = cacheDB.Exec(`INSERT OR IGNORE INTO statistics (id) VALUES (1)`) if err != nil { cacheLogger.Error("初始化statistics表失败: " + err.Error()) } // 创建索引以提高清理效率 _, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_created_at ON cache(created_at)`) if err != nil { cacheLogger.Error("创建索引失败: " + err.Error()) } // 创建device_pool表(设备密钥池) _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS device_pool ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_header TEXT NOT NULL, key TEXT NOT NULL, key_ver INTEGER NOT NULL, is_valid INTEGER DEFAULT 1, created_at INTEGER NOT NULL, last_used_at INTEGER NOT NULL )`) if err != nil { cacheLogger.Error("创建device_pool表失败: " + err.Error()) return err } // 创建device_pool索引 _, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`) if err != nil { cacheLogger.Warning("创建device_pool索引失败: " + err.Error()) } // 创建audio_cache表(音频代理缓存) _, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS audio_cache ( key TEXT PRIMARY KEY, original_url TEXT NOT NULL, device_id TEXT NOT NULL, headers TEXT, created_at INTEGER DEFAULT (strftime('%s', 'now')) )`) if err != nil { cacheLogger.Error("创建audio_cache表失败: " + err.Error()) return err } return nil } // isCacheEnabled 检查正文内容缓存是否启用 func isCacheEnabled() bool { cfg := config.GetConfig() if cfg == nil { return false } return cfg.Cache.Enabled } // GetCache 获取正文内容缓存 func GetCache(key string) string { if !isCacheEnabled() { return "" } if cacheDB == nil { return "" } cacheMutex.Lock() defer cacheMutex.Unlock() var value string err := cacheDB.QueryRow("SELECT value FROM cache WHERE key = ?", key).Scan(&value) if err != nil { if err != sql.ErrNoRows { cacheLogger.Error("查询缓存失败: " + err.Error()) } return "" } var cacheData CacheData err = json.Unmarshal([]byte(value), &cacheData) if err != nil { cacheLogger.Error("解析缓存数据失败: " + err.Error()) return "" } cacheLogger.Debug("缓存命中: " + key) // 检查是否过期 if time.Now().Unix() < int64(cacheData.Expi) { cacheLogger.Debug("缓存有效!") return cacheData.Data } cacheLogger.Debug("缓存已过期!") return "" } // UpdateCache 更新缓存 // UpdateCache 更新或插入正文内容缓存 func UpdateCache(key string, value string, expi int) error { if !isCacheEnabled() { return nil } if cacheDB == nil { return nil } cacheMutex.Lock() defer cacheMutex.Unlock() cacheData := CacheData{ Data: value, Expi: float64(time.Now().Unix() + int64(expi)), } jsonData, err := json.Marshal(cacheData) if err != nil { return err } // 检查是否存在 var exists bool err = cacheDB.QueryRow("SELECT 1 FROM cache WHERE key = ?", key).Scan(&exists) if err == sql.ErrNoRows { // 不存在,插入 _, err = cacheDB.Exec("INSERT INTO cache (key, value) VALUES (?, ?)", key, string(jsonData)) } else { // 存在,更新 _, err = cacheDB.Exec("UPDATE cache SET value = ? WHERE key = ?", string(jsonData), key) } if err != nil { cacheLogger.Error("更新缓存失败: " + err.Error()) return err } cacheLogger.Debug("缓存已更新: " + key) return nil } // CloseCache 关闭缓存数据库 func CloseCache() { if cacheDB != nil { cacheDB.Close() } } // CleanupAllCache 立即清理所有正文缓存 func CleanupAllCache() error { if cacheDB == nil { return fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() // 删除所有缓存数据 _, err := cacheDB.Exec("DELETE FROM cache") if err != nil { return err } // 智能压缩:仅在空闲空间超过阈值时执行 var freelistCount int64 err = cacheDB.QueryRow("PRAGMA freelist_count").Scan(&freelistCount) if err == nil && freelistCount > 0 { var pageSize int64 err = cacheDB.QueryRow("PRAGMA page_size").Scan(&pageSize) if err == nil { freeBytes := freelistCount * pageSize // 阈值:空闲空间超过 5MB 时才执行压缩 const vacuumThreshold = 5 * 1024 * 1024 // 5MB if freeBytes >= vacuumThreshold { cacheLogger.Info(fmt.Sprintf("空闲空间 %.2f MB,开始执行压缩...", float64(freeBytes)/(1024*1024))) _, err = cacheDB.Exec("VACUUM") if err != nil { cacheLogger.Warning("数据库压缩失败: " + err.Error()) } else { cacheLogger.Info(fmt.Sprintf("数据库压缩完成,已释放 %.2f MB 磁盘空间", float64(freeBytes)/(1024*1024))) } } else { cacheLogger.Debug(fmt.Sprintf("空闲空间仅 %.2f MB,跳过压缩", float64(freeBytes)/(1024*1024))) } } } return nil } // CleanupExpiredCache 清理过期的正文缓存 func CleanupExpiredCache() error { cfg := config.GetConfig() if cfg == nil { return fmt.Errorf("配置未加载") } // 检查是否启用清理 if !cfg.Cache.CleanupEnabled { return nil } // 检查清理时间配置 if cfg.Cache.CleanupSeconds <= 0 { return nil } if cacheDB == nil { return fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() // 计算过期时间点(当前时间 - cleanup_seconds) expiredTime := time.Now().Unix() - int64(cfg.Cache.CleanupSeconds) // 删除过期的缓存(created_at < expiredTime) result, err := cacheDB.Exec("DELETE FROM cache WHERE created_at < ?", expiredTime) if err != nil { cacheLogger.Error("清理过期缓存失败: " + err.Error()) return err } // 获取删除的行数 rowsAffected, err := result.RowsAffected() if err != nil { cacheLogger.Warning("获取删除行数失败: " + err.Error()) } else if rowsAffected > 0 { cacheLogger.Info(fmt.Sprintf("已清理 %d 条过期缓存(创建时间早于 %d 秒前)", rowsAffected, cfg.Cache.CleanupSeconds)) } return nil } // StartCacheCleanup 启动缓存自动清理任务 func StartCacheCleanup() { cfg := config.GetConfig() if cfg == nil || !cfg.Cache.CleanupEnabled || cfg.Cache.CleanupSeconds <= 0 { cacheLogger.Info("缓存自动清理未启用或配置无效,跳过启动") return } // 每小时执行一次清理(可以根据需要调整间隔) cleanupInterval := 1 * time.Hour cacheCleanupTicker = time.NewTicker(cleanupInterval) cacheLogger.Info(fmt.Sprintf("缓存自动清理已启动,清理间隔: %v,过期时间: %d 秒", cleanupInterval, cfg.Cache.CleanupSeconds)) // 立即执行一次清理 go func() { if err := CleanupExpiredCache(); err != nil { cacheLogger.Error("初始清理失败: " + err.Error()) } }() // 定时执行清理 go func() { for range cacheCleanupTicker.C { if err := CleanupExpiredCache(); err != nil { cacheLogger.Error("定时清理失败: " + err.Error()) } } }() } // StopCacheCleanup 停止缓存自动清理任务 func StopCacheCleanup() { if cacheCleanupTicker != nil { cacheCleanupTicker.Stop() cacheLogger.Info("缓存自动清理已停止") // 最后执行一次清理 if err := CleanupExpiredCache(); err != nil { cacheLogger.Error("最后清理失败: " + err.Error()) } } } // IncrementContentRequestCount 增加content接口访问计数 func IncrementContentRequestCount() int { countMutex.Lock() defer countMutex.Unlock() contentRequestCount++ return contentRequestCount } // ResetContentRequestCount 重置访问计数 func ResetContentRequestCount() { countMutex.Lock() defer countMutex.Unlock() contentRequestCount = 0 } // GetContentRequestCount 获取当前访问计数 func GetContentRequestCount() int { countMutex.Lock() defer countMutex.Unlock() return contentRequestCount } // VacuumDatabase 压缩数据库,释放磁盘空间 func VacuumDatabase() error { if cacheDB == nil { return fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() cacheLogger.Info("开始执行数据库压缩...") _, err := cacheDB.Exec("VACUUM") if err != nil { cacheLogger.Error("数据库压缩失败: " + err.Error()) return err } cacheLogger.Info("数据库压缩完成,磁盘空间已释放") return nil } // GetDatabaseStats 获取数据库统计信息 func GetDatabaseStats() (map[string]interface{}, error) { if cacheDB == nil { return nil, fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() stats := make(map[string]interface{}) // 获取cache表记录数 var cacheCount int err := cacheDB.QueryRow("SELECT COUNT(*) FROM cache").Scan(&cacheCount) if err != nil { cacheLogger.Warning("获取cache表记录数失败: " + err.Error()) cacheCount = 0 } stats["cache_count"] = cacheCount // 获取数据库页面统计 var pageCount, pageSize, freelistCount int64 err = cacheDB.QueryRow("PRAGMA page_count").Scan(&pageCount) if err == nil { stats["page_count"] = pageCount } err = cacheDB.QueryRow("PRAGMA page_size").Scan(&pageSize) if err == nil { stats["page_size"] = pageSize } err = cacheDB.QueryRow("PRAGMA freelist_count").Scan(&freelistCount) if err == nil { stats["freelist_count"] = freelistCount stats["total_size_bytes"] = pageCount * pageSize stats["free_size_bytes"] = freelistCount * pageSize stats["used_size_bytes"] = (pageCount - freelistCount) * pageSize } return stats, nil } // SaveStatistics 保存请求统计到数据库 func SaveStatistics(stats map[string]int64) error { if cacheDB == nil { return fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() _, err := cacheDB.Exec(` UPDATE statistics SET total_requests = ?, search_requests = ?, detail_requests = ?, catalog_requests = ?, content_requests = ?, manga_requests = ?, audio_requests = ?, video_requests = ?, category_requests = ?, updated_at = strftime('%s', 'now') WHERE id = 1 `, stats["total"], stats["search"], stats["detail"], stats["catalog"], stats["content"], stats["manga"], stats["audio"], stats["video"], stats["category"]) if err != nil { cacheLogger.Error("保存统计数据失败: " + err.Error()) return err } return nil } // LoadStatistics 从数据库加载请求统计 func LoadStatistics() (map[string]int64, error) { if cacheDB == nil { return nil, fmt.Errorf("数据库未初始化") } cacheMutex.Lock() defer cacheMutex.Unlock() var total, search, detail, catalog, content, manga, audio, video, category int64 err := cacheDB.QueryRow(` SELECT total_requests, search_requests, detail_requests, catalog_requests, content_requests, manga_requests, audio_requests, video_requests, category_requests FROM statistics WHERE id = 1 `).Scan(&total, &search, &detail, &catalog, &content, &manga, &audio, &video, &category) if err == sql.ErrNoRows { // 没有数据,返回空统计 return map[string]int64{ "total": 0, "search": 0, "detail": 0, "catalog": 0, "content": 0, "manga": 0, "audio": 0, "video": 0, "category": 0, }, nil } if err != nil { cacheLogger.Error("加载统计数据失败: " + err.Error()) return nil, err } stats := map[string]int64{ "total": total, "search": search, "detail": detail, "catalog": catalog, "content": content, "manga": manga, "audio": audio, "video": video, "category": category, } return stats, nil }