✨ feat(api): 新增榜单书架历史与搜索续传并重构正文缓存链路

- 新增 /rank 免签名榜单接口(多域名容错)、/bookshelf 与 /read-history(sessionid 登录态,书架支持分组过滤)
- 搜索重写:翻页透传上游 passback/search_id/next_offset/corrected_query 并按 book_id 去重,15~20 位纯数字关键词直查详情(ID 直搜)
- 删除 /login/check 登录校验端点(login.go、路由与文档同步移除,sessionid 说明改为直接供书架/历史使用)
- 正文链路重构:singleflight 合并同章节并发回源防击穿,缓存与解析解耦统一在出口处理,decodeContentBody 按 crypt_status 分支并兜底 500003 明文
- 缓存层:SQL 持久层 TTL 与业务 TTL 解耦(不可变内容过期后由 SQL 回填),动态短缓存只走 Redis,withCache 合并并发 miss
- 密钥池:KeyTs 随正文请求上传降低密钥不匹配重试,密钥超 24 小时异步刷新并防刷新风暴,last_used_at 批量落库
- 签名补 X-Helios(与 X-Ladon 同值,对齐书源实现);设备注册版本号统一走 ClientVersionCode/Name
- 分类 landing 透传新版标签 selected_items;multi-detail 批量详情供书架/历史复用
- README 与 openapi.yaml 补充新接口文档
This commit is contained in:
chenxin committed 2026-10-12 14:49:14 +08:00
1 parent 8ca372050b
commit 84bd3238ab
28 files changed
+1180 -270

No files matched your search

+66 -15
View File
@@ -20,9 +20,14 @@ type DeviceKeyItem struct {
DeviceHeader string `json:"device_header"`
Key string `json:"key"`
KeyVer int `json:"key_ver"`
KeyTs int64 `json:"key_ts"` // 密钥注册时间戳(key_register_ts)
IsValid bool `json:"is_valid"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt time.Time `json:"last_used_at"`
// 仅内存字段(不落库)
KeyFetchedAt time.Time `json:"-"` // 密钥获取时间,超 24h 异步刷新
KeyRefreshAt time.Time `json:"-"` // 上次刷新尝试时间,防刷新风暴
}
// DeviceKeyPool 设备密钥池
@@ -77,6 +82,10 @@ func (p *DeviceKeyPool) Initialize() error {
}
poolLogger.Info(fmt.Sprintf("设备密钥池初始化完成,当前有效设备数: %d", p.getValidCount()))
// last_used_at 批量落库(GetNext 不再同步写库)
p.StartDevicePoolFlusher()
return nil
}
@@ -110,11 +119,22 @@ func (p *DeviceKeyPool) GetNext() (*DeviceKeyItem, error) {
item := validItems[p.currentIndex%len(validItems)]
p.currentIndex++
// 更新最后使用时间
// 只更新内存中的最后使用时间,由后台 flusher 定期批量落库
// (此前每次取设备同步 UPDATE,SQLite 单写锁下是吞吐瓶颈)
item.LastUsedAt = time.Now()
p.updateDeviceInDB(item)
// poolLogger.Debug(fmt.Sprintf("使用设备 ID=%d", item.ID))
// 密钥超 24h 未刷新:异步换新(本次请求仍用旧密钥,下次生效)
if time.Since(item.KeyFetchedAt) > 20*time.Hour && time.Since(item.KeyRefreshAt) > 10*time.Minute {
item.KeyRefreshAt = time.Now()
header := item.DeviceHeader
go func() {
if ki, err := GetKey(header); err == nil {
UpdateKeyInPool(header, ki)
} else {
poolLogger.Warning(fmt.Sprintf("刷新设备密钥失败: %v", err))
}
}()
}
// 异步检查是否需要补充
go p.checkAndRefill()
@@ -316,9 +336,11 @@ func (p *DeviceKeyPool) addNewDevice() error {
DeviceHeader: deviceHeader,
Key: keyInfo.Key,
KeyVer: keyInfo.Ver,
KeyTs: keyInfo.Ts,
IsValid: true,
CreatedAt: time.Now(),
LastUsedAt: time.Now(),
KeyFetchedAt: time.Now(),
}
// 保存到数据库
@@ -344,8 +366,8 @@ func (p *DeviceKeyPool) loadFromDB() error {
}
rows, err := cacheDB.Query(`
SELECT id, device_header, key, key_ver, is_valid, created_at, last_used_at
FROM device_pool
SELECT id, device_header, key, key_ver, COALESCE(key_ts, 0), is_valid, created_at, last_used_at
FROM device_pool
ORDER BY id
`)
if err != nil {
@@ -363,6 +385,7 @@ func (p *DeviceKeyPool) loadFromDB() error {
&item.DeviceHeader,
&item.Key,
&item.KeyVer,
&item.KeyTs,
&item.IsValid,
&createdAt,
&lastUsedAt,
@@ -374,6 +397,7 @@ func (p *DeviceKeyPool) loadFromDB() error {
item.CreatedAt = time.Unix(createdAt, 0)
item.LastUsedAt = time.Unix(lastUsedAt, 0)
item.KeyFetchedAt = time.Now() // 旧设备的密钥年龄未知,从加载起算
items = append(items, &item)
}
@@ -401,10 +425,10 @@ func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
// 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, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO device_pool (device_header, key, key_ver, key_ts, is_valid, created_at, last_used_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
`), item.DeviceHeader, item.Key, item.KeyVer, item.KeyTs, boolToInt(item.IsValid),
item.CreatedAt.Unix(), item.LastUsedAt.Unix(), item.CreatedAt.Unix()).Scan(&item.ID)
return err
@@ -418,14 +442,39 @@ func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error {
_, err := cacheDB.Exec(q(`
UPDATE device_pool
SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ?, updated_at = ?
SET device_header = ?, key = ?, key_ver = ?, key_ts = ?, is_valid = ?, last_used_at = ?, updated_at = ?
WHERE id = ?
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
`), item.DeviceHeader, item.Key, item.KeyVer, item.KeyTs, boolToInt(item.IsValid),
item.LastUsedAt.Unix(), item.LastUsedAt.Unix(), item.ID)
return err
}
// StartDevicePoolFlusher 启动 last_used_at 批量落库任务(GetNext 已改为只写内存)
func (p *DeviceKeyPool) StartDevicePoolFlusher() {
go func() {
for range time.Tick(time.Minute) {
FlushDevicePool()
}
}()
poolLogger.Info("设备池 last_used_at 批量落库已启动(每分钟)")
}
// FlushDevicePool 将内存中的设备使用时间批量刷入数据库
// ponytail: 进程退出时最多丢 1 分钟使用记录(仅统计用途),无需精确停表
func FlushDevicePool() {
p := GetDeviceKeyPool()
if !p.enabled || cacheDB == nil {
return
}
p.mu.RLock()
items := append([]*DeviceKeyItem(nil), p.items...)
p.mu.RUnlock()
for _, it := range items {
p.updateDeviceInDB(it)
}
}
// removeInvalidFromDB 从数据库移除无效设备
func (p *DeviceKeyPool) removeInvalidFromDB() error {
if cacheDB == nil {
@@ -483,10 +532,10 @@ func GetKeyInfoFromPool(deviceHeader string) *KeyInfo {
return nil
}
// UpdateKeyInPool 更新池中设备的密钥
func UpdateKeyInPool(deviceHeader string, key string, ver int) error {
// UpdateKeyInPool 更新池中设备的密钥(含注册时间戳)
func UpdateKeyInPool(deviceHeader string, ki *KeyInfo) error {
pool := GetDeviceKeyPool()
if !pool.IsEnabled() {
if !pool.IsEnabled() || ki == nil {
return nil
}
@@ -495,8 +544,10 @@ func UpdateKeyInPool(deviceHeader string, key string, ver int) error {
for _, item := range pool.items {
if item.DeviceHeader == deviceHeader {
item.Key = key
item.KeyVer = ver
item.Key = ki.Key
item.KeyVer = ki.Ver
item.KeyTs = ki.Ts
item.KeyFetchedAt = time.Now()
return pool.updateDeviceInDB(item)
}
}