package fqapi import ( "fanqie/internal/common" "fmt" "strings" ) var shelfLogger = common.NewLogger("shelf", "INFO", "") // HandleBookshelf 获取登录用户书架。个人动态数据不缓存。 // group 非空时只返回该分组的书;书架接口对无效/过期 sessionid 也返回空书架,无法区分—— // 空结果时提示重新登录 func HandleBookshelf(sessionID, group string) (string, error) { if sessionID == "" { return BuildErrorResponse("需要参数 sessionid(在 fanqienovel.com 登录后从浏览器 Cookie 中获取)"), nil } resp, err := signedBookAPIGet("/reading/bookapi/bookshelf/info/v/", nil, sessionID) if err != nil { shelfLogger.Error(fmt.Sprintf("获取书架失败: %v", err)) return BuildErrorResponse("获取书架失败"), err } items, err := bookAPIList(resp, "book_shelf_info") if err != nil { return BuildErrorResponse("获取书架失败: " + err.Error()), nil } // 过滤分组并收集 book_id type shelfRow struct { id string groupName string } rows := make([]shelfRow, 0, len(items)) for _, it := range items { m, ok := it.(map[string]interface{}) if !ok { continue } id := bookIDOf(m) if id == "" { continue } gn, _ := m["group_name"].(string) if group != "" && gn != group { continue } rows = append(rows, shelfRow{id: id, groupName: gn}) } if len(rows) == 0 { return BuildErrorResponse("书架为空:sessionid 可能无效或已过期,请重新获取"), nil } ids := make([]string, len(rows)) for i, row := range rows { ids[i] = row.id } details := detailMapByIDs(ids) result := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { d := details[row.id] if d == nil { d = map[string]interface{}{"book_id": row.id} } d["group_name"] = row.groupName result = append(result, d) } return marshalNoEscape(UnwrapJSONStrings(buildCacheData(result))), nil } // HandleReadHistory 获取登录用户阅读历史。个人动态数据不缓存。 // offset 从 0 起,每页 20 条;每项合并书籍详情并保留原始记录(进度等)于 history 字段 func HandleReadHistory(sessionID string, offset int) (string, error) { if sessionID == "" { return BuildErrorResponse("需要参数 sessionid(在 fanqienovel.com 登录后从浏览器 Cookie 中获取)"), nil } if offset < 0 { offset = 0 } resp, err := signedBookAPIGet("/reading/bookapi/read_history/list/v/", []string{fmt.Sprintf("offset=%d", offset), "book_type=0"}, sessionID) if err != nil { shelfLogger.Error(fmt.Sprintf("获取阅读历史失败: %v", err)) return BuildErrorResponse("获取阅读历史失败"), err } items, err := bookAPIList(resp, "data_list") if err != nil { return BuildErrorResponse("获取阅读历史失败: " + err.Error()), nil } type historyRow struct { id string raw map[string]interface{} } rows := make([]historyRow, 0, len(items)) for _, it := range items { m, ok := it.(map[string]interface{}) if !ok { continue } id := bookIDOf(m) if id == "" { continue } rows = append(rows, historyRow{id: id, raw: m}) } if len(rows) == 0 { return marshalNoEscape(buildCacheData([]interface{}{})), nil } ids := make([]string, len(rows)) for i, row := range rows { ids[i] = row.id } details := detailMapByIDs(ids) result := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { d := details[row.id] if d == nil { d = map[string]interface{}{"book_id": row.id} } // 原始记录(含阅读进度/时间)挂 history 字段保留 d["history"] = row.raw result = append(result, d) } return marshalNoEscape(UnwrapJSONStrings(buildCacheData(result))), nil } // signedBookAPIGet 个人书架/历史接口:常规签名请求 + 登录态 Cookie func signedBookAPIGet(path string, extraParams []string, sessionID string) (*common.ClientResponse, error) { reqURL := BuildURL("https://reading.snssdk.com"+path, extraParams...) return GorgonRequest(reqURL, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", "Cookie": "sessionid=" + sessionID, }, }) } // bookAPIList 校验上游响应并取出 data 下的指定列表字段 func bookAPIList(resp *common.ClientResponse, field string) ([]interface{}, error) { respJSON, err := resp.JSON() if err != nil { return nil, fmt.Errorf("响应解析失败: %w", err) } if code, ok := respJSON["code"].(float64); !ok || code != 0 { msg, _ := respJSON["message"].(string) return nil, fmt.Errorf("code=%.0f msg=%s", respJSON["code"], msg) } data, _ := respJSON["data"].(map[string]interface{}) items, _ := data[field].([]interface{}) return items, nil } // bookIDOf 兼容提取 book_id / book_id_str(数字或字符串) func bookIDOf(m map[string]interface{}) string { for _, k := range []string{"book_id_str", "book_id", "bookId"} { switch v := m[k].(type) { case string: if v != "" { return v } case float64: return fmt.Sprintf("%.0f", v) } } return "" } // detailMapByIDs 按 book_id 建立详情映射(复用批量详情接口,分批请求) func detailMapByIDs(ids []string) map[string]map[string]interface{} { m := make(map[string]map[string]interface{}, len(ids)) for _, chunk := range chunkArray(ids, 100) { list, err := multiDetail(strings.Join(chunk, ",")) if err != nil { shelfLogger.Warning(fmt.Sprintf("批量获取详情失败(该批保留 book_id): %v", err)) continue } for _, d := range list { if id, ok := d["book_id"].(string); ok { m[id] = d } } } return m }