Files
fanqie-go/internal/fqapi/cache_manager.go
T
chenxin 6981148727 🔧 chore: 初始化番茄小说 API 项目结构
新增项目基础框架,包含核心 API 实现、算法模块、HTTP 处理器等完整功能模块
2026-08-28 15:22:22 +08:00

528 lines
13 KiB
Go
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.
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
}