- request_log 新增 user_agent 列(含旧表 ALTER 迁移)及 ip/status 索引,记录解码后的明文 query(含 key,不脱敏) - 请求日志批量写入改为多值 INSERT 单次往返,替代事务逐条 Exec - 新增按本地时区分天聚合查询,状态页展示今日请求量与近 7 天请求趋势(峰值日高亮) - 新增 TZ 时区支持:.env 配置 TZ 后日志时间戳与状态页全部时间统一生效,二进制内嵌 tzdata 保证容器可用 - 五张表统一补齐 created_at/updated_at 字段,启动时自动补列并按业务时间回填旧行 - device_pool 的 device_header 增加唯一约束;audio_cache 写入改为 UPSERT 修复同 key 重复报错 - 状态页"最近请求"新增 UA 列与限流状态显示;config.yaml 暴露 rate_limit_qps 配置并默认启用请求日志 - deploy.sh 同名版本默认拒绝覆盖,新增 --force/-f 参数显式允许覆盖
133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
package fqapi
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// AudioCacheItem 音频缓存项
|
|
type AudioCacheItem struct {
|
|
OriginalURL string `json:"original_url"` // 原始CDN URL
|
|
DeviceID string `json:"device_id"` // 关联的设备ID
|
|
Headers map[string]string `json:"headers"` // 请求头信息
|
|
CachedAt time.Time `json:"cached_at"` // 缓存时间
|
|
}
|
|
|
|
// GenerateAudioProxyURL 生成音频代理URL
|
|
func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]string) string {
|
|
// 生成缓存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)
|
|
|
|
// 存储到数据库
|
|
if cacheDB != nil {
|
|
|
|
_, err := cacheDB.Exec(q(`
|
|
INSERT INTO audio_cache (key, original_url, device_id, headers, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET original_url = excluded.original_url,
|
|
device_id = excluded.device_id, headers = excluded.headers, updated_at = excluded.updated_at
|
|
`), key, originalURL, deviceID, string(headersJSON), time.Now().Unix(), time.Now().Unix())
|
|
if err != nil {
|
|
audioLogger.Error("保存音频缓存失败: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// 返回代理URL
|
|
return fmt.Sprintf("/audio/proxy/%s", key)
|
|
}
|
|
|
|
// HandleAudioProxy 处理音频代理请求(流式返回,调用方负责 Close)
|
|
func HandleAudioProxy(key string) (io.ReadCloser, string, error) {
|
|
var originalURL, deviceID string
|
|
var headers map[string]string
|
|
|
|
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
|
|
|
|
err := cacheDB.QueryRow(q(`
|
|
SELECT original_url, device_id, headers, created_at
|
|
FROM audio_cache WHERE key = ?
|
|
`), key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
|
|
|
|
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秒
|
|
// 清理过期缓存
|
|
cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key)
|
|
return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口")
|
|
}
|
|
|
|
// 解析headers
|
|
json.Unmarshal([]byte(headersJSON), &headers)
|
|
}
|
|
if headers == nil {
|
|
headers = make(map[string]string)
|
|
}
|
|
|
|
// 下载音频内容(流式),使用关联的设备信息
|
|
body, contentType, err := downloadMediaWithRetry(originalURL, deviceID, headers, 3)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("下载音频失败: %w", err)
|
|
}
|
|
|
|
return body, contentType, nil
|
|
}
|
|
|
|
// CleanExpiredAudioCache 清理过期的音频缓存
|
|
func CleanExpiredAudioCache() {
|
|
// Redis 模式下由 TTL 自动过期,无需清理
|
|
if redisCache != nil {
|
|
return
|
|
}
|
|
|
|
if cacheDB == nil {
|
|
return
|
|
}
|
|
|
|
// 删除2小时前的缓存
|
|
result, err := cacheDB.Exec(q("DELETE FROM audio_cache WHERE created_at < ?"), time.Now().Unix()-7200)
|
|
if err != nil {
|
|
audioLogger.Error("清理过期音频缓存失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
if rowsAffected > 0 {
|
|
audioLogger.Info(fmt.Sprintf("已清理 %d 条过期音频缓存", rowsAffected))
|
|
}
|
|
}
|