✨ 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 模板 - 新增 openapi.yaml(OpenAPI 3.1):覆盖全部 24 个路径,含参数、响应结构与鉴权方案,README 增加指引 - 删除死代码 InitDevicePoolTable;新增 rebind_test.go 单测 - 更新 README 与 config.yaml 配置示例,go.mod 新增 go-redis/v9 与 pgx/v5 依赖
This commit is contained in:
1 parent
d710d382ee
commit
1eccebedf9
15 files changed
+1406
-210
No files matched your search
@@ -20,6 +20,16 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
|
||||
// 生成缓存key
|
||||
key := generateRandomKey(16)
|
||||
|
||||
// Redis 模式:TTL 自动过期
|
||||
if redisCache != nil {
|
||||
saveAudioProxyEntry(key, &audioProxyEntry{
|
||||
OriginalURL: originalURL,
|
||||
DeviceID: deviceID,
|
||||
Headers: headers,
|
||||
})
|
||||
return fmt.Sprintf("/audio/proxy/%s", key)
|
||||
}
|
||||
|
||||
// 将headers序列化为JSON字符串
|
||||
headersJSON, _ := json.Marshal(headers)
|
||||
|
||||
@@ -28,10 +38,10 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
|
||||
cacheMutex.Lock()
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
_, err := cacheDB.Exec(`
|
||||
_, err := cacheDB.Exec(q(`
|
||||
INSERT INTO audio_cache (key, original_url, device_id, headers, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, key, originalURL, deviceID, string(headersJSON), time.Now().Unix())
|
||||
`), key, originalURL, deviceID, string(headersJSON), time.Now().Unix())
|
||||
if err != nil {
|
||||
audioLogger.Error("保存音频缓存失败: " + err.Error())
|
||||
}
|
||||
@@ -43,40 +53,50 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
|
||||
|
||||
// HandleAudioProxy 处理音频代理请求
|
||||
func HandleAudioProxy(key string) ([]byte, string, error) {
|
||||
if cacheDB == nil {
|
||||
return nil, "", fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
|
||||
// 从数据库获取音频信息
|
||||
var originalURL, deviceID, headersJSON string
|
||||
var createdAt int64
|
||||
|
||||
cacheMutex.Lock()
|
||||
err := cacheDB.QueryRow(`
|
||||
SELECT original_url, device_id, headers, created_at
|
||||
FROM audio_cache WHERE key = ?
|
||||
`, key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
|
||||
cacheMutex.Unlock()
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", fmt.Errorf("音频信息不存在,请先调用 /audio 接口")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("查询音频缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查缓存是否过期(2小时,匹配CDN链接有效期)
|
||||
if time.Now().Unix()-createdAt > 7200 { // 2小时 = 7200秒
|
||||
// 清理过期缓存
|
||||
cacheMutex.Lock()
|
||||
cacheDB.Exec("DELETE FROM audio_cache WHERE key = ?", key)
|
||||
cacheMutex.Unlock()
|
||||
return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口")
|
||||
}
|
||||
|
||||
// 解析headers
|
||||
var originalURL, deviceID string
|
||||
var headers map[string]string
|
||||
json.Unmarshal([]byte(headersJSON), &headers)
|
||||
|
||||
if redisCache != nil {
|
||||
entry, err := loadAudioProxyEntry(key)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
originalURL, deviceID, headers = entry.OriginalURL, entry.DeviceID, entry.Headers
|
||||
} else {
|
||||
if cacheDB == nil {
|
||||
return nil, "", fmt.Errorf("数据库未初始化")
|
||||
}
|
||||
|
||||
// 从数据库获取音频信息
|
||||
var headersJSON string
|
||||
var createdAt int64
|
||||
|
||||
cacheMutex.Lock()
|
||||
err := cacheDB.QueryRow(q(`
|
||||
SELECT original_url, device_id, headers, created_at
|
||||
FROM audio_cache WHERE key = ?
|
||||
`), key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
|
||||
cacheMutex.Unlock()
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", fmt.Errorf("音频信息不存在,请先调用 /audio 接口")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("查询音频缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查缓存是否过期(2小时,匹配CDN链接有效期)
|
||||
if time.Now().Unix()-createdAt > 7200 { // 2小时 = 7200秒
|
||||
// 清理过期缓存
|
||||
cacheMutex.Lock()
|
||||
cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key)
|
||||
cacheMutex.Unlock()
|
||||
return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口")
|
||||
}
|
||||
|
||||
// 解析headers
|
||||
json.Unmarshal([]byte(headersJSON), &headers)
|
||||
}
|
||||
if headers == nil {
|
||||
headers = make(map[string]string)
|
||||
}
|
||||
@@ -92,6 +112,11 @@ func HandleAudioProxy(key string) ([]byte, string, error) {
|
||||
|
||||
// CleanExpiredAudioCache 清理过期的音频缓存
|
||||
func CleanExpiredAudioCache() {
|
||||
// Redis 模式下由 TTL 自动过期,无需清理
|
||||
if redisCache != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cacheDB == nil {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +125,7 @@ func CleanExpiredAudioCache() {
|
||||
defer cacheMutex.Unlock()
|
||||
|
||||
// 删除2小时前的缓存
|
||||
result, err := cacheDB.Exec("DELETE FROM audio_cache WHERE created_at < ?", time.Now().Unix()-7200)
|
||||
result, err := cacheDB.Exec(q("DELETE FROM audio_cache WHERE created_at < ?"), time.Now().Unix()-7200)
|
||||
if err != nil {
|
||||
audioLogger.Error("清理过期音频缓存失败: " + err.Error())
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user