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) } // loadMediaProxyEntry 读取媒体代理映射(audio/video 共用;Redis 优先,回退 SQL) type mediaProxyEntry struct { OriginalURL string DeviceID string Headers map[string]string } func loadMediaProxyEntry(key string) (*mediaProxyEntry, error) { if redisCache != nil { entry, err := loadAudioProxyEntry(key) if err != nil { return nil, err } return &mediaProxyEntry{OriginalURL: entry.OriginalURL, DeviceID: entry.DeviceID, Headers: entry.Headers}, nil } if cacheDB == nil { return nil, fmt.Errorf("数据库未初始化") } var originalURL, deviceID, 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 或 /video 接口") } if err != nil { return nil, fmt.Errorf("查询媒体缓存失败: %w", err) } // 过期清理(2小时,匹配CDN链接有效期) if time.Now().Unix()-createdAt > 7200 { cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key) return nil, fmt.Errorf("媒体信息已过期,请重新调用接口") } var headers map[string]string json.Unmarshal([]byte(headersJSON), &headers) if headers == nil { headers = make(map[string]string) } return &mediaProxyEntry{OriginalURL: originalURL, DeviceID: deviceID, Headers: headers}, nil } // HandleAudioProxy 处理音频代理请求(流式返回,调用方负责 Close) func HandleAudioProxy(key string) (io.ReadCloser, string, error) { entry, err := loadMediaProxyEntry(key) if err != nil { return nil, "", err } // 下载音频内容(流式),使用关联的设备信息 body, contentType, err := downloadMediaWithRetry(entry.OriginalURL, entry.DeviceID, entry.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)) } }