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