✨ 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:
1 parent
8ca372050b
commit
84bd3238ab
28 files changed
+1180
-270
No files matched your search
@@ -2,12 +2,84 @@ package fqapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"compress/zlib"
|
||||
"encoding/json"
|
||||
"fanqie/internal/config"
|
||||
"sort"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 客户端版本统一(与 2026-10 可用的书源实现一致;旧值 58932/5.8.9.32 混用易触发风控)
|
||||
const (
|
||||
ClientVersionCode = "72732"
|
||||
ClientVersionName = "7.2.7.32"
|
||||
)
|
||||
|
||||
// PermanentNovelImgHost 番茄小说图片的永久 CDN 域名(原始链接带临时参数会过期失效)
|
||||
const PermanentNovelImgHost = "https://p6-novel.byteimg.com"
|
||||
|
||||
// novelPicMd5Re 匹配图片路径中的 32 位十六进制资源 ID
|
||||
var novelPicMd5Re = regexp.MustCompile(`^/novel-pic/([a-f0-9]{32})`)
|
||||
|
||||
// NormalizeNovelImageURL 将临时图片链接永久化:
|
||||
// - /novel-pic/{md5} → p6-novel.byteimg.com/origin/novel-pic/{md5}(正文插图原图)
|
||||
// - 含 ~tplv 的模板图(封面缩放)→ p6-novel.byteimg.com 保留原路径
|
||||
func NormalizeNovelImageURL(raw string) string {
|
||||
u := strings.TrimSpace(strings.ReplaceAll(raw, "&", "&"))
|
||||
if u == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(u, "//") {
|
||||
u = "https:" + u
|
||||
}
|
||||
idx := strings.Index(u, "/novel-pic/")
|
||||
if idx < 0 {
|
||||
return u
|
||||
}
|
||||
path := u[idx:]
|
||||
if m := novelPicMd5Re.FindStringSubmatch(path); m != nil && !strings.Contains(path, "~tplv") {
|
||||
return PermanentNovelImgHost + "/origin/novel-pic/" + m[1]
|
||||
}
|
||||
return PermanentNovelImgHost + path
|
||||
}
|
||||
|
||||
// gunzipMaybe 自适应解压上游正文(gzip 与 zlib 帧都可能出现),非压缩数据原样返回
|
||||
func gunzipMaybe(data []byte) []byte {
|
||||
if r, err := gzip.NewReader(bytes.NewReader(data)); err == nil {
|
||||
if out, err2 := io.ReadAll(r); err2 == nil {
|
||||
r.Close()
|
||||
return out
|
||||
}
|
||||
}
|
||||
if r, err := zlib.NewReader(bytes.NewReader(data)); err == nil {
|
||||
if out, err2 := io.ReadAll(r); err2 == nil {
|
||||
r.Close()
|
||||
return out
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// buildSortedQuery 将若干 "k=v" 段合并为 form 编码的查询串(按键排序)。
|
||||
// 设备机型等值含空格时必须编码,否则实际请求的 URL 与签名输入不一致,
|
||||
// registerkey 等接口会注册失败(书源 2026-10-10 修复的同类问题)
|
||||
func buildSortedQuery(parts ...string) string {
|
||||
vals := url.Values{}
|
||||
for _, part := range parts {
|
||||
for _, seg := range strings.Split(part, "&") {
|
||||
if seg == "" {
|
||||
continue
|
||||
}
|
||||
k, v, _ := strings.Cut(seg, "=")
|
||||
vals.Add(k, v)
|
||||
}
|
||||
}
|
||||
return vals.Encode()
|
||||
}
|
||||
|
||||
// getContentConfig 获取内容配置(带默认值)
|
||||
func getContentConfig() config.ContentConfig {
|
||||
if config.GetAppConfig() != nil {
|
||||
@@ -188,7 +260,7 @@ func getDeviceHeader() string {
|
||||
|
||||
// removeVersionFromHeader 移除header中的version_name参数
|
||||
func removeVersionFromHeader(header string) string {
|
||||
return strings.Replace(header, "&version_name=5.8.9.32", "", -1)
|
||||
return strings.Replace(header, "&version_name="+ClientVersionName, "", -1)
|
||||
}
|
||||
|
||||
// ============ API参数构建工具 ============
|
||||
@@ -203,7 +275,7 @@ func BuildURL(fullURL string, customParams ...string) string {
|
||||
"channel=0",
|
||||
"device_platform=android",
|
||||
"os_version=0",
|
||||
"version_code=58932",
|
||||
"version_code=" + ClientVersionCode,
|
||||
}
|
||||
|
||||
// 添加设备header
|
||||
@@ -215,13 +287,8 @@ func BuildURL(fullURL string, customParams ...string) string {
|
||||
// 添加自定义参数
|
||||
allParams = append(allParams, customParams...)
|
||||
|
||||
// 合并、拆分、排序
|
||||
joined := strings.Join(allParams, "&")
|
||||
paramList := strings.Split(joined, "&")
|
||||
sort.Strings(paramList)
|
||||
params := strings.Join(paramList, "&")
|
||||
|
||||
return fullURL + "?" + params
|
||||
// form 编码 + 排序(机型含空格等特殊字符时保证 URL 与签名一致)
|
||||
return fullURL + "?" + buildSortedQuery(allParams...)
|
||||
}
|
||||
|
||||
// ============ 缓存处理工具函数 ============
|
||||
|
||||
Reference in new issue
Block a user