Files
fanqie-go/internal/fqapi/store_redis.go
T
chenxin cf16c37f32 ✨ 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 依赖
2026-09-29 20:22:52 +08:00

86 lines
2.5 KiB
Go

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
}