package fqapi import ( "encoding/json" "fanqie/internal/common" "fmt" "strconv" "strings" ) var catalogLogger = common.NewLogger("catalog", "INFO", "") // HandleGetCatalog 处理获取书籍目录,目录数据基本不变,缓存 24 小时 func HandleGetCatalog(bookID string) (string, error) { return withCache("catalog_"+bookID, 86400, func() (string, error) { return fetchCatalog(bookID) }) } func fetchCatalog(bookID string) (string, error) { if bookID == "" { return BuildErrorResponse("需要参数 book_id"), nil } // 构建URL reqURL := BuildURL("https://reading.snssdk.com/reading/bookapi/directory/all_items/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 { catalogLogger.Error(fmt.Sprintf("获取目录请求失败: %v", err)) // 尝试备用接口 return offShelfCatalog(bookID) } respJSON, err := resp.JSON() if err != nil { catalogLogger.Error(fmt.Sprintf("解析响应失败: %v", err)) return offShelfCatalog(bookID) } if code, ok := respJSON["code"].(float64); ok && code == 0 { // 删除不需要的字段 if data, ok := respJSON["data"].(map[string]interface{}); ok { delete(data, "item_list") delete(data, "book_info") delete(data, "catalog_data") } // 重新序列化(解包 JSON 字符串字段) resultBytes, err := json.Marshal(UnwrapJSONStrings(respJSON)) if err != nil { return BuildErrorResponse("序列化失败"), err } return string(resultBytes), nil } // 如果主接口失败,尝试备用接口 return offShelfCatalog(bookID) } // offShelfCatalog 下架书籍目录获取(备用接口) func offShelfCatalog(bookID string) (string, error) { // 第一步:获取所有item_id列表 url := "https://fanqienovel.com/api/reader/directory/detail?bookId=" + bookID client := common.NewHTTPClient() resp, err := client.Get(url, map[string]string{ "Referer": "https://fanqienovel.com/page/" + bookID, "Origin": "https://fanqienovel.com", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", }) if err != nil { catalogLogger.Error(fmt.Sprintf("备用接口请求失败: %v", err)) return BuildErrorResponse("FAILED"), err } var itemListResponse map[string]interface{} err = json.Unmarshal(resp.Content, &itemListResponse) if err != nil { return BuildErrorResponse("FAILED"), err } data, ok := itemListResponse["data"].(map[string]interface{}) if !ok { return BuildErrorResponse("响应格式错误"), fmt.Errorf("响应格式错误") } allItemIDs, ok := data["allItemIds"].([]interface{}) if !ok { return BuildErrorResponse("无法获取item列表"), fmt.Errorf("无法获取item列表") } // 转换为字符串数组 itemList := make([]string, len(allItemIDs)) for i, id := range allItemIDs { itemList[i] = fmt.Sprintf("%v", id) } // 分批处理(每批100个) chunkSize := 100 var result []map[string]interface{} for i := 0; i < len(itemList); i += chunkSize { end := i + chunkSize if end > len(itemList) { end = len(itemList) } chunk := itemList[i:end] details, err := itemMultiDetail(chunk) if err == nil { result = append(result, details...) } } // 构建响应 response := map[string]interface{}{ "code": 0, "message": "SUCCESS", "data": map[string]interface{}{ "item_data_list": result, }, } return marshalNoEscape(UnwrapJSONStrings(response)), nil } // itemMultiDetail 批量获取章节详情 func itemMultiDetail(itemList []string) ([]map[string]interface{}, error) { url := "https://novel.snssdk.com/api/novel/book/directory/detail/v/?aid=1319&item_ids=" + strings.Join(itemList, ",") client := common.NewHTTPClient() resp, err := client.Get(url, map[string]string{ "User-Agent": "okhttp/4.10.0", }) if err != nil { return nil, err } var dataResp map[string]interface{} err = json.Unmarshal(resp.Content, &dataResp) if err != nil { return nil, err } if code, ok := dataResp["code"].(float64); ok && code == 0 { chapters, ok := dataResp["data"].([]interface{}) if !ok { return nil, fmt.Errorf("响应格式错误") } var result []map[string]interface{} for _, ch := range chapters { chapter, ok := ch.(map[string]interface{}) if !ok { continue } versionStr := "" if version, ok := chapter["version"].(string); ok && len(version) >= 32 { versionStr = version[:32] } // 安全解析 first_pass_time firstPassTime := 0 if fpt, ok := chapter["first_pass_time"].(float64); ok { firstPassTime = int(fpt) } else if fptStr, ok := chapter["first_pass_time"].(string); ok && fptStr != "" { // 尝试解析字符串形式的数字 if fptVal, err := strconv.ParseFloat(fptStr, 64); err == nil { firstPassTime = int(fptVal) } } // 安全解析 chapter_word_number chapterWordNumber := 0 if cwn, ok := chapter["chapter_word_number"].(float64); ok { chapterWordNumber = int(cwn) } else if cwnStr, ok := chapter["chapter_word_number"].(string); ok && cwnStr != "" { if cwnVal, err := strconv.ParseFloat(cwnStr, 64); err == nil { chapterWordNumber = int(cwnVal) } } result = append(result, map[string]interface{}{ "item_id": chapter["item_id"], "version": nil, "content_md5": versionStr, "first_pass_time": firstPassTime, "title": chapter["title"], "volume_name": chapter["volume_name"], "chapter_type": chapter["chapter_type"], "chapter_word_number": chapterWordNumber, "is_review": nil, }) } return result, nil } return nil, fmt.Errorf("请求失败") }