- 新增 /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 补充新接口文档
250 lines
5.8 KiB
Go
250 lines
5.8 KiB
Go
package fqapi
|
|
|
|
import (
|
|
"fanqie/internal/common"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
var multiDetailLogger = common.NewLogger("multi_detail", "INFO", "")
|
|
|
|
// singleDetail 获取单个书籍详情
|
|
func singleDetail(bookID string) (map[string]interface{}, error) {
|
|
reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/detail/v/", "book_id="+bookID)
|
|
|
|
resp, err := GorgonRequest(reqURL, map[string]interface{}{
|
|
"method": "GET",
|
|
"headers": map[string]string{
|
|
"User-Agent": "com.dragon.read",
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return map[string]interface{}{}, err
|
|
}
|
|
|
|
respJSON, err := resp.JSON()
|
|
if err != nil {
|
|
return map[string]interface{}{}, err
|
|
}
|
|
|
|
if code, ok := respJSON["code"].(float64); ok && code == 0 {
|
|
if data, ok := respJSON["data"].(map[string]interface{}); ok {
|
|
delete(data, "content")
|
|
return data, nil
|
|
}
|
|
}
|
|
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
// multiDetail 批量获取书籍详情
|
|
func multiDetail(bookIDs string) ([]map[string]interface{}, error) {
|
|
reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/multi-detail/v/", "book_id="+bookIDs)
|
|
|
|
resp, err := GorgonRequest(reqURL, map[string]interface{}{
|
|
"method": "GET",
|
|
"headers": map[string]string{
|
|
"User-Agent": "com.dragon.read",
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
// 返回空列表
|
|
count := len(strings.Split(bookIDs, ","))
|
|
result := make([]map[string]interface{}, count)
|
|
for i := range result {
|
|
result[i] = map[string]interface{}{}
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
respJSON, err := resp.JSON()
|
|
if err != nil {
|
|
count := len(strings.Split(bookIDs, ","))
|
|
result := make([]map[string]interface{}, count)
|
|
for i := range result {
|
|
result[i] = map[string]interface{}{}
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
if code, ok := respJSON["code"].(float64); ok && code == 0 {
|
|
if data, ok := respJSON["data"].([]interface{}); ok && len(data) > 0 {
|
|
result := make([]map[string]interface{}, len(data))
|
|
for i, item := range data {
|
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
|
result[i] = itemMap
|
|
} else {
|
|
result[i] = map[string]interface{}{}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// 返回空列表
|
|
count := len(strings.Split(bookIDs, ","))
|
|
result := make([]map[string]interface{}, count)
|
|
for i := range result {
|
|
result[i] = map[string]interface{}{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// chunkArray 分块数组
|
|
func chunkArray(arr []string, size int) [][]string {
|
|
chunks := make([][]string, 0)
|
|
for i := 0; i < len(arr); i += size {
|
|
end := i + size
|
|
if end > len(arr) {
|
|
end = len(arr)
|
|
}
|
|
chunks = append(chunks, arr[i:end])
|
|
}
|
|
return chunks
|
|
}
|
|
|
|
// HandleGetMultiDetail 处理批量获取书籍详情,详情数据基本不变,缓存 24 小时
|
|
func HandleGetMultiDetail(bookID interface{}) (string, error) {
|
|
// 缓存 key 归一化:ID 排序去重,相同集合不同顺序共享缓存
|
|
ids := normalizeIDList(bookID)
|
|
if ids == "" {
|
|
return fetchMultiDetail(bookID)
|
|
}
|
|
return withCache("multi_detail_"+ids, 86400, func() (string, error) {
|
|
return fetchMultiDetail(bookID)
|
|
})
|
|
}
|
|
|
|
// normalizeIDList 将 book_id 参数归一化为排序去重后的逗号串
|
|
func normalizeIDList(bookID interface{}) string {
|
|
var list []string
|
|
switch v := bookID.(type) {
|
|
case string:
|
|
list = strings.Split(v, ",")
|
|
case []string:
|
|
list = v
|
|
default:
|
|
return ""
|
|
}
|
|
seen := make(map[string]bool, len(list))
|
|
out := make([]string, 0, len(list))
|
|
for _, id := range list {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
out = append(out, id)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, ",")
|
|
}
|
|
|
|
func fetchMultiDetail(bookID interface{}) (string, error) {
|
|
if bookID == nil {
|
|
return BuildErrorResponse("需要参数 book_id"), nil
|
|
}
|
|
|
|
var bookIDList []string
|
|
|
|
switch v := bookID.(type) {
|
|
case string:
|
|
if v == "" {
|
|
return BuildErrorResponse("需要参数 book_id"), nil
|
|
}
|
|
bookIDList = strings.Split(v, ",")
|
|
case []string:
|
|
bookIDList = v
|
|
default:
|
|
return BuildErrorResponse("book_id 参数格式错误"), nil
|
|
}
|
|
|
|
// 分块处理,每批100个
|
|
chunks := chunkArray(bookIDList, 100)
|
|
|
|
// 并发请求
|
|
type result struct {
|
|
data []map[string]interface{}
|
|
err error
|
|
}
|
|
|
|
results := make([]result, len(chunks))
|
|
var wg sync.WaitGroup
|
|
|
|
for i, chunk := range chunks {
|
|
wg.Add(1)
|
|
go func(idx int, ch []string) {
|
|
defer wg.Done()
|
|
data, err := multiDetail(strings.Join(ch, ","))
|
|
results[idx] = result{data: data, err: err}
|
|
}(i, chunk)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// 合并结果
|
|
res := make([]map[string]interface{}, 0)
|
|
for _, r := range results {
|
|
if r.err == nil {
|
|
res = append(res, r.data...)
|
|
} else {
|
|
multiDetailLogger.Error(fmt.Sprintf("批量获取详情失败: %v", r.err))
|
|
}
|
|
}
|
|
|
|
// 处理下架书籍
|
|
offShelfBookIDs := make([]string, 0)
|
|
offShelfIndexes := make([]int, 0)
|
|
|
|
for i, item := range res {
|
|
if status, ok := item["tomato_book_status"].(string); ok && status == "3" {
|
|
if bid, ok := item["book_id"].(string); ok {
|
|
offShelfBookIDs = append(offShelfBookIDs, bid)
|
|
offShelfIndexes = append(offShelfIndexes, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 单独获取下架书籍信息
|
|
if len(offShelfBookIDs) > 0 {
|
|
type offshelfResult struct {
|
|
data map[string]interface{}
|
|
idx int
|
|
}
|
|
|
|
offshelfResults := make([]offshelfResult, len(offShelfBookIDs))
|
|
var wg2 sync.WaitGroup
|
|
|
|
for i, bid := range offShelfBookIDs {
|
|
wg2.Add(1)
|
|
go func(idx int, bookID string, originalIdx int) {
|
|
defer wg2.Done()
|
|
data, _ := singleDetail(bookID)
|
|
offshelfResults[idx] = offshelfResult{data: data, idx: originalIdx}
|
|
}(i, bid, offShelfIndexes[i])
|
|
}
|
|
|
|
wg2.Wait()
|
|
|
|
// 更新结果
|
|
for _, r := range offshelfResults {
|
|
if r.idx < len(res) {
|
|
res[r.idx] = r.data
|
|
}
|
|
}
|
|
}
|
|
|
|
// 序列化结果
|
|
response := map[string]interface{}{
|
|
"code": 0,
|
|
"message": "SUCCESS",
|
|
"data": res,
|
|
}
|
|
|
|
return marshalNoEscape(UnwrapJSONStrings(response)), nil
|
|
}
|