Files
fanqie-go/internal/fqapi/multi_detail.go
T
chenxin 586b714f3c ✨ feat(cache): 幂等只读接口接入缓存
- 新增 withCache 包装:命中直接返回,miss 回源后写入,仅缓存成功响应
- 详情/目录/批量详情/章节摘要缓存 24 小时;搜索/分类列表/分类详情短缓存 10 分钟
- 7 个 fqapi 处理函数原函数体不动、改名 fetchXxx,外层套缓存壳
- redisGetCache 补 3 秒读超时,与写路径一致,防远程 Redis 网络抖动拖垮请求
- 新增 TestWithCache:验证命中不回源、失败响应不缓存
2026-09-29 23:43:56 +08:00

222 lines
5.1 KiB
Go

package fqapi
import (
"encoding/json"
"fanqie/internal/common"
"fmt"
"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) {
return withCache(fmt.Sprintf("multi_detail_%v", bookID), 86400, func() (string, error) {
return fetchMultiDetail(bookID)
})
}
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,
}
resultBytes, _ := json.Marshal(response)
return string(resultBytes), nil
}