- 新增 /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 补充新接口文档
105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
package fqapi
|
||
|
||
import (
|
||
"database/sql"
|
||
"fanqie/internal/config"
|
||
"fmt"
|
||
"sync"
|
||
"testing"
|
||
|
||
_ "github.com/mattn/go-sqlite3"
|
||
)
|
||
|
||
// TestConcurrentCacheAccess 验证删除全局串行锁后并发读写缓存不死锁、不 panic、数据正确(配合 -race)
|
||
func TestConcurrentCacheAccess(t *testing.T) {
|
||
if err := config.LoadConfig("../../config/config.yaml"); err != nil {
|
||
t.Skipf("无法加载配置: %v", err)
|
||
}
|
||
|
||
db, err := sql.Open("sqlite3", t.TempDir()+"/test.db?_journal_mode=WAL&_busy_timeout=5000")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
oldDB, oldDriver := cacheDB, dbDriver
|
||
cacheDB, dbDriver = db, "sqlite3"
|
||
defer func() {
|
||
cacheDB, dbDriver = oldDB, oldDriver
|
||
db.Close()
|
||
}()
|
||
|
||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS cache (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT,
|
||
expi INTEGER DEFAULT 0,
|
||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||
)`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < 8; i++ {
|
||
wg.Add(1)
|
||
go func(i int) {
|
||
defer wg.Done()
|
||
for j := 0; j < 30; j++ {
|
||
key := fmt.Sprintf("k-%d-%d", i, j)
|
||
if err := UpdateCache(key, "value-"+key, 60); err != nil {
|
||
t.Errorf("UpdateCache(%s): %v", key, err)
|
||
return
|
||
}
|
||
if got := GetCache(key); got != "value-"+key {
|
||
t.Errorf("GetCache(%s) = %q, want %q", key, got, "value-"+key)
|
||
return
|
||
}
|
||
}
|
||
}(i)
|
||
}
|
||
wg.Wait()
|
||
}
|
||
|
||
// TestWithCache 验证缓存命中不回源、失败响应不缓存
|
||
func TestWithCache(t *testing.T) {
|
||
if err := config.LoadConfig("../../config/config.yaml"); err != nil {
|
||
t.Skipf("无法加载配置: %v", err)
|
||
}
|
||
db, err := sql.Open("sqlite3", t.TempDir()+"/test.db?_journal_mode=WAL&_busy_timeout=5000")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
oldDB, oldDriver := cacheDB, dbDriver
|
||
cacheDB, dbDriver = db, "sqlite3"
|
||
defer func() {
|
||
cacheDB, dbDriver = oldDB, oldDriver
|
||
db.Close()
|
||
}()
|
||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS cache (
|
||
key TEXT PRIMARY KEY, value TEXT,
|
||
expi INTEGER DEFAULT 0,
|
||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||
)`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
calls := 0
|
||
fetch := func() (string, error) { calls++; return "result", nil }
|
||
if got, _ := withCache("wc_hit", 60, fetch); got != "result" {
|
||
t.Fatalf("首次应返回回源结果, got %q", got)
|
||
}
|
||
if got, _ := withCache("wc_hit", 60, fetch); got != "result" {
|
||
t.Fatalf("命中应返回缓存结果, got %q", got)
|
||
}
|
||
if calls != 1 {
|
||
t.Fatalf("命中缓存不应回源, fetch 调用 %d 次", calls)
|
||
}
|
||
|
||
errCalls := 0
|
||
errFetch := func() (string, error) { errCalls++; return "", fmt.Errorf("boom") }
|
||
withCache("wc_miss", 60, errFetch)
|
||
withCache("wc_miss", 60, errFetch)
|
||
if errCalls != 2 {
|
||
t.Fatalf("失败响应不应被缓存, fetch 调用 %d 次", errCalls)
|
||
}
|
||
}
|