Files
fanqie-go/internal/fqapi/device_key_pool.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

554 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fqapi
import (
"fanqie/internal/common"
"fanqie/internal/config"
"fmt"
"sync"
"time"
)
var (
poolLogger = common.NewLogger("device_key_pool", "INFO", "")
globalPool *DeviceKeyPool
poolOnce sync.Once
)
// DeviceKeyItem 设备密钥项
type DeviceKeyItem struct {
ID int `json:"id"`
DeviceHeader string `json:"device_header"`
Key string `json:"key"`
KeyVer int `json:"key_ver"`
IsValid bool `json:"is_valid"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt time.Time `json:"last_used_at"`
}
// DeviceKeyPool 设备密钥池
type DeviceKeyPool struct {
mu sync.RWMutex
currentIndex int
items []*DeviceKeyItem
enabled bool
}
// GetDeviceKeyPool 获取全局设备密钥池实例
func GetDeviceKeyPool() *DeviceKeyPool {
poolOnce.Do(func() {
globalPool = &DeviceKeyPool{
items: make([]*DeviceKeyItem, 0),
enabled: false,
}
})
return globalPool
}
// Initialize 初始化设备密钥池
func (p *DeviceKeyPool) Initialize() error {
cfg := config.GetConfig()
if cfg == nil || !cfg.DeviceKeyPool.Enabled {
poolLogger.Info("设备密钥池未启用")
p.enabled = false
return nil
}
p.enabled = true
poolLogger.Info("开始初始化设备密钥池...")
// 从数据库加载已有的设备
if err := p.loadFromDB(); err != nil {
poolLogger.Warning("从数据库加载设备失败: " + err.Error())
}
// 检查并补充设备到初始化数量
validCount := p.getValidCount()
initialSize := cfg.DeviceKeyPool.InitialSize
if validCount < initialSize {
needCount := initialSize - validCount
poolLogger.Info(fmt.Sprintf("当前有效设备数: %d, 需要补充: %d", validCount, needCount))
for i := 0; i < needCount; i++ {
if err := p.addNewDevice(); err != nil {
poolLogger.Error(fmt.Sprintf("添加设备失败: %v", err))
}
}
}
poolLogger.Info(fmt.Sprintf("设备密钥池初始化完成,当前有效设备数: %d", p.getValidCount()))
return nil
}
// IsEnabled 检查设备密钥池是否启用
func (p *DeviceKeyPool) IsEnabled() bool {
return p.enabled
}
// GetNext 获取下一个可用的设备密钥(轮询方式)
func (p *DeviceKeyPool) GetNext() (*DeviceKeyItem, error) {
if !p.enabled {
return nil, fmt.Errorf("设备密钥池未启用")
}
p.mu.Lock()
defer p.mu.Unlock()
// 过滤有效设备
validItems := make([]*DeviceKeyItem, 0)
for _, item := range p.items {
if item.IsValid {
validItems = append(validItems, item)
}
}
if len(validItems) == 0 {
return nil, fmt.Errorf("没有可用的设备")
}
// 轮询获取下一个设备
item := validItems[p.currentIndex%len(validItems)]
p.currentIndex++
// 更新最后使用时间
item.LastUsedAt = time.Now()
p.updateDeviceInDB(item)
// poolLogger.Debug(fmt.Sprintf("使用设备 ID=%d", item.ID))
// 异步检查是否需要补充
go p.checkAndRefill()
return item, nil
}
// MarkInvalid 标记设备为无效
func (p *DeviceKeyPool) MarkInvalid(deviceHeader string) error {
if !p.enabled {
return nil
}
p.mu.Lock()
defer p.mu.Unlock()
for _, item := range p.items {
if item.DeviceHeader == deviceHeader {
item.IsValid = false
// poolLogger.Warning(fmt.Sprintf("设备 ID=%d 已标记为无效", item.ID))
// 更新数据库
if err := p.updateDeviceInDB(item); err != nil {
return err
}
// 异步补充新设备
go p.checkAndRefill()
return nil
}
}
return fmt.Errorf("未找到设备")
}
// RemoveInvalid 移除所有无效设备并自动补充
func (p *DeviceKeyPool) RemoveInvalid() error {
if !p.enabled {
return nil
}
p.mu.Lock()
// 从数据库中删除无效设备
if err := p.removeInvalidFromDB(); err != nil {
p.mu.Unlock()
return err
}
// 从内存中移除无效设备
validItems := make([]*DeviceKeyItem, 0)
for _, item := range p.items {
if item.IsValid {
validItems = append(validItems, item)
}
}
removedCount := len(p.items) - len(validItems)
p.items = validItems
if removedCount > 0 {
poolLogger.Info(fmt.Sprintf("已移除 %d 个无效设备", removedCount))
// 立即检查并补充设备(使用已持有锁的版本)
p.checkAndRefillLocked()
}
p.mu.Unlock()
return nil
}
// GetStats 获取池统计信息
func (p *DeviceKeyPool) GetStats() map[string]interface{} {
p.mu.RLock()
defer p.mu.RUnlock()
validCount := 0
invalidCount := 0
for _, item := range p.items {
if item.IsValid {
validCount++
} else {
invalidCount++
}
}
return map[string]interface{}{
"enabled": p.enabled,
"total": len(p.items),
"valid": validCount,
"invalid": invalidCount,
"current_index": p.currentIndex,
}
}
// checkAndRefill 检查并补充设备(私有方法,会自动加锁)
func (p *DeviceKeyPool) checkAndRefill() {
if !p.enabled {
return
}
cfg := config.GetConfig()
if cfg == nil {
return
}
p.mu.Lock()
validCount := p.getValidCount()
p.mu.Unlock()
minSize := cfg.DeviceKeyPool.MinSize
maxSize := cfg.DeviceKeyPool.MaxSize
if validCount < minSize {
needCount := minSize - validCount
// 限制不超过最大值
if validCount+needCount > maxSize {
needCount = maxSize - validCount
}
if needCount > 0 {
poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
for i := 0; i < needCount; i++ {
if err := p.addNewDevice(); err != nil {
poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
}
}
}
}
}
// checkAndRefillLocked 检查并补充设备(已持有锁的内部版本)
func (p *DeviceKeyPool) checkAndRefillLocked() {
if !p.enabled {
return
}
cfg := config.GetConfig()
if cfg == nil {
return
}
validCount := p.getValidCount()
minSize := cfg.DeviceKeyPool.MinSize
maxSize := cfg.DeviceKeyPool.MaxSize
if validCount < minSize {
needCount := minSize - validCount
// 限制不超过最大值
if validCount+needCount > maxSize {
needCount = maxSize - validCount
}
if needCount > 0 {
poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
// 临时释放锁,因为addNewDevice需要加锁
p.mu.Unlock()
for i := 0; i < needCount; i++ {
if err := p.addNewDevice(); err != nil {
poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
}
}
p.mu.Lock()
}
}
}
// getValidCount 获取有效设备数量(调用前需已加锁或在RLock内)
func (p *DeviceKeyPool) getValidCount() int {
count := 0
for _, item := range p.items {
if item.IsValid {
count++
}
}
return count
}
// addNewDevice 添加新设备(会自动加锁)
func (p *DeviceKeyPool) addNewDevice() error {
// 注册新设备
deviceHeader, err := Register()
if err != nil {
return fmt.Errorf("注册设备失败: %w", err)
}
// 获取密钥
keyInfo, err := GetKey(deviceHeader)
if err != nil {
return fmt.Errorf("获取密钥失败: %w", err)
}
// 创建设备项
item := &DeviceKeyItem{
DeviceHeader: deviceHeader,
Key: keyInfo.Key,
KeyVer: keyInfo.Ver,
IsValid: true,
CreatedAt: time.Now(),
LastUsedAt: time.Now(),
}
// 保存到数据库
if err := p.saveDeviceToDB(item); err != nil {
return fmt.Errorf("保存设备到数据库失败: %w", err)
}
// 添加到内存
p.mu.Lock()
p.items = append(p.items, item)
p.mu.Unlock()
// poolLogger.Info(fmt.Sprintf("已添加新设备 ID=%d", item.ID))
return nil
}
// ===== 数据库操作方法 =====
// loadFromDB 从数据库加载设备
func (p *DeviceKeyPool) loadFromDB() error {
if cacheDB == nil {
return fmt.Errorf("数据库未初始化")
}
rows, err := cacheDB.Query(`
SELECT id, device_header, key, key_ver, is_valid, created_at, last_used_at
FROM device_pool
ORDER BY id
`)
if err != nil {
return err
}
defer rows.Close()
items := make([]*DeviceKeyItem, 0)
for rows.Next() {
var item DeviceKeyItem
var createdAt, lastUsedAt int64
err := rows.Scan(
&item.ID,
&item.DeviceHeader,
&item.Key,
&item.KeyVer,
&item.IsValid,
&createdAt,
&lastUsedAt,
)
if err != nil {
poolLogger.Error("扫描行失败: " + err.Error())
continue
}
item.CreatedAt = time.Unix(createdAt, 0)
item.LastUsedAt = time.Unix(lastUsedAt, 0)
items = append(items, &item)
}
p.mu.Lock()
p.items = items
p.mu.Unlock()
poolLogger.Info(fmt.Sprintf("从数据库加载了 %d 个设备", len(items)))
return nil
}
// boolToInt 布尔转整数存储(is_valid 列为 INTEGER,PostgreSQL 严格要求类型匹配)
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// saveDeviceToDB 保存设备到数据库
func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
if cacheDB == nil {
return fmt.Errorf("数据库未初始化")
}
cacheMutex.Lock()
defer cacheMutex.Unlock()
// RETURNING id 同时兼容 SQLite(3.35+) 与 PostgreSQL,替代 LastInsertId()
err := cacheDB.QueryRow(q(`
INSERT INTO device_pool (device_header, key, key_ver, is_valid, created_at, last_used_at)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING id
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
item.CreatedAt.Unix(), item.LastUsedAt.Unix()).Scan(&item.ID)
return err
}
// updateDeviceInDB 更新设备信息
func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error {
if cacheDB == nil {
return fmt.Errorf("数据库未初始化")
}
cacheMutex.Lock()
defer cacheMutex.Unlock()
_, err := cacheDB.Exec(q(`
UPDATE device_pool
SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ?
WHERE id = ?
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
item.LastUsedAt.Unix(), item.ID)
return err
}
// removeInvalidFromDB 从数据库移除无效设备
func (p *DeviceKeyPool) removeInvalidFromDB() error {
if cacheDB == nil {
return fmt.Errorf("数据库未初始化")
}
cacheMutex.Lock()
defer cacheMutex.Unlock()
result, err := cacheDB.Exec(`DELETE FROM device_pool WHERE is_valid = 0`)
if err != nil {
return err
}
affected, _ := result.RowsAffected()
if affected > 0 {
poolLogger.Info(fmt.Sprintf("从数据库删除了 %d 个无效设备", affected))
}
return nil
}
// GetDeviceHeaderFromPool 从池中获取设备header(兼容旧接口)
func GetDeviceHeaderFromPool() string {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return ""
}
item, err := pool.GetNext()
if err != nil {
poolLogger.Error("从池中获取设备失败: " + err.Error())
return ""
}
return item.DeviceHeader
}
// GetKeyInfoFromPool 从池中获取密钥信息(兼容旧接口)
func GetKeyInfoFromPool(deviceHeader string) *KeyInfo {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return nil
}
pool.mu.RLock()
defer pool.mu.RUnlock()
for _, item := range pool.items {
if item.DeviceHeader == deviceHeader && item.IsValid {
return &KeyInfo{
Key: item.Key,
Ver: item.KeyVer,
}
}
}
return nil
}
// UpdateKeyInPool 更新池中设备的密钥
func UpdateKeyInPool(deviceHeader string, key string, ver int) error {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return nil
}
pool.mu.Lock()
defer pool.mu.Unlock()
for _, item := range pool.items {
if item.DeviceHeader == deviceHeader {
item.Key = key
item.KeyVer = ver
return pool.updateDeviceInDB(item)
}
}
return fmt.Errorf("未找到设备")
}
// GetAnyValidDeviceHeader 获取任意一个有效设备的header(用于辅助功能)
func GetAnyValidDeviceHeader() string {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return ""
}
pool.mu.RLock()
defer pool.mu.RUnlock()
for _, item := range pool.items {
if item.IsValid {
return item.DeviceHeader
}
}
return ""
}
// GetValidDevice 获取一个有效的设备
func GetValidDevice() (*DeviceKeyItem, error) {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return nil, fmt.Errorf("设备池未启用")
}
return pool.GetNext()
}
// GetCurrentDevice 获取当前使用的设备(和GetValidDevice一样,返回下一个可用设备)
func GetCurrentDevice() *DeviceKeyItem {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
return nil
}
item, _ := pool.GetNext()
return item
}