✨ feat(cache): 新增 Redis 缓存与 PostgreSQL 后端支持
- 新增 cache.redis 配置(独立 host/port/password/db 字段),启用后正文缓存与音频代理缓存走 Redis,键前缀 fq:cache:/fq:audio:,TTL 自动过期 - 新增 cache.backend: sqlite|postgres 切换,PG 连接用 net/url 构造 DSN,密码特殊字符自动转义;SQLite 顺手启用 WAL 与 busy_timeout - SQL 方言收敛:q() 将 ? 占位符转换为 $n、INSERT OR IGNORE 与查询再插入统一为 ON CONFLICT、LastInsertId 改为 RETURNING id、建表 DDL 双版本分派 - PG 下 VACUUM 走 autovacuum 说明、状态页大小改查 pg_database_size - 修复 PG 下设备保存失败:is_valid 列为 INTEGER,写入点补 boolToInt 转换 - 新增 .env 支持:敏感配置(密码/主机/访问密钥)由环境变量覆盖 config.yaml,.env 进 gitignore,仓库附 .env.example 模板 - 删除死代码 InitDevicePoolTable;新增 rebind_test.go 单测 - 更新 README 与 config.yaml 配置示例,go.mod 新增 go-redis/v9 与 pgx/v5 依赖
This commit is contained in:
1 parent
d710d382ee
commit
cf16c37f32
14 files changed
+671
-210
No files matched your search
@@ -0,0 +1,85 @@
|
||||
package fqapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// audioProxyEntry 音频代理缓存条目(Redis 版)
|
||||
type audioProxyEntry struct {
|
||||
OriginalURL string `json:"original_url"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
const (
|
||||
redisCacheKeyPrefix = "fq:cache:"
|
||||
redisAudioKeyPrefix = "fq:audio:"
|
||||
// audioCacheTTL 音频代理缓存有效期(匹配 CDN 链接有效期)
|
||||
audioCacheTTL = 2 * time.Hour
|
||||
)
|
||||
|
||||
// redisGetCache 从 Redis 获取正文缓存
|
||||
func redisGetCache(key string) string {
|
||||
val, err := redisCache.Get(context.Background(), redisCacheKeyPrefix+key).Result()
|
||||
if err != nil {
|
||||
return "" // miss 或错误都视为未命中
|
||||
}
|
||||
|
||||
var cacheData CacheData
|
||||
if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
|
||||
cacheLogger.Error("解析Redis缓存数据失败: " + err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
cacheLogger.Debug("Redis缓存命中: " + key)
|
||||
if time.Now().Unix() < int64(cacheData.Expi) {
|
||||
return cacheData.Data
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// redisUpdateCache 写入 Redis 正文缓存(TTL 与过期时间一致)
|
||||
func redisUpdateCache(key string, value string, expi int) error {
|
||||
cacheData := CacheData{
|
||||
Data: value,
|
||||
Expi: float64(time.Now().Unix() + int64(expi)),
|
||||
}
|
||||
jsonData, err := json.Marshal(cacheData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return redisCache.Set(ctx, redisCacheKeyPrefix+key, jsonData, time.Duration(expi)*time.Second).Err()
|
||||
}
|
||||
|
||||
// saveAudioProxyEntry 将音频代理信息存入 Redis
|
||||
func saveAudioProxyEntry(key string, entry *audioProxyEntry) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
jsonData, _ := json.Marshal(entry)
|
||||
if err := redisCache.Set(ctx, redisAudioKeyPrefix+key, jsonData, audioCacheTTL).Err(); err != nil {
|
||||
audioLogger.Error("保存音频缓存到Redis失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// loadAudioProxyEntry 从 Redis 读取音频代理信息,未命中或过期返回错误
|
||||
func loadAudioProxyEntry(key string) (*audioProxyEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
val, err := redisCache.Get(ctx, redisAudioKeyPrefix+key).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("音频信息不存在或已过期,请重新调用 /audio 接口")
|
||||
}
|
||||
|
||||
var entry audioProxyEntry
|
||||
if err := json.Unmarshal([]byte(val), &entry); err != nil {
|
||||
return nil, fmt.Errorf("解析音频缓存失败: %w", err)
|
||||
}
|
||||
return &entry, nil
|
||||
}
|
||||
Reference in new issue
Block a user