package fqapi import ( "bytes" "compress/gzip" "encoding/json" "fanqie/internal/common" "fmt" "strings" ) var ( reviewLogger = common.NewLogger("review", "INFO", "") ) // compressJSON 压缩 JSON 数据为 Gzip 格式 func compressJSON(data interface{}) ([]byte, error) { jsonData, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("JSON序列化失败: %w", err) } var compressedData bytes.Buffer gzw := gzip.NewWriter(&compressedData) if _, err := gzw.Write(jsonData); err != nil { gzw.Close() return nil, fmt.Errorf("Gzip压缩失败: %w", err) } if err := gzw.Close(); err != nil { return nil, fmt.Errorf("Gzip关闭失败: %w", err) } return compressedData.Bytes(), nil } // postReviewAPI 发送段评相关的 POST 请求 func postReviewAPI(url string, requestBody interface{}) (map[string]interface{}, error) { // 压缩 JSON compressedData, err := compressJSON(requestBody) if err != nil { return nil, err } // 使用 GorgonRequest 添加签名 resp, err := GorgonRequest(url, map[string]interface{}{ "method": "POST", "body": compressedData, "headers": map[string]string{ "User-Agent": "com.dragon.read", "Content-Type": "application/json", "Content-Encoding": "gzip", }, }) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } // 解析响应 var result map[string]interface{} if err := json.Unmarshal(resp.Content, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 提取 data 字段 if data, ok := result["data"].(map[string]interface{}); ok { return data, nil } return result, nil } // CommentData 评论数据结构 type CommentData struct { Data map[string]struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` } `json:"data"` ItemVersion string `json:"item_version"` } // GetChapterReview 获取章节的段落评论统计(想法评论) func GetChapterReview(itemID string) (map[string]interface{}, error) { // 使用 BuildURL 添加设备参数 url := BuildURL(fmt.Sprintf("https://reading.snssdk.com/novel/commentapi/idea/list/%s/v1", itemID)) requestBody := map[string]interface{}{ "comment_source": 3, "item_id": itemID, "item_version": "cdd11695939bcc41d1ff8c3f10c67802_1_e5c2474f6821deb", } return postReviewAPI(url, requestBody) } // GetChapterMixData 获取章节混合数据(评论) func GetChapterMixData(bookID, itemID string) (map[string]interface{}, error) { // 使用 BuildURL 添加设备参数 url := BuildURL("https://api5-normal-sinfonlinec.fqnovel.com/reading/ugc/item/mix_data/get/v") requestBody := map[string]interface{}{ "count": 5000, "item_id": itemID, "source_type": 51, "book_id": bookID, } return postReviewAPI(url, requestBody) } // GetParaReview 获取段落评论或评论回复 // sort: 1=热门, 3=最新 // offset: 分页偏移量 // count: 每页数量 func GetParaReview(itemID, bookID, commentID string, para, sort, offset, count int) (map[string]interface{}, error) { // 默认排序值 if sort != 1 && sort != 3 { sort = 1 // 默认热门 } // 默认数量 if count <= 0 { count = 20 } var url string if commentID != "" { // 获取评论的回复列表 - 使用 BuildURL 添加设备参数 url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/reply/topic/v/", fmt.Sprintf("offset=%d", offset), fmt.Sprintf("group_id=%s", itemID), "service_id=1", fmt.Sprintf("count=%d", count), "source_type=63", fmt.Sprintf("book_id=%s", bookID), fmt.Sprintf("comment_id=%s", commentID), ) } else { // 获取段落的评论列表 - 使用 BuildURL 添加设备参数 url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/idea/comment_list/v/", "item_version=c63f0e6195259adec926ae3209291854_1_1099f2b8324de5f", fmt.Sprintf("offset=%d", offset), "author_user_id=104957777655", fmt.Sprintf("item_id=%s", itemID), fmt.Sprintf("count=%d", count), fmt.Sprintf("para_index=%d", para), fmt.Sprintf("book_id=%s", bookID), fmt.Sprintf("sort=%d", sort), ) } // 使用 GorgonRequest 添加签名 resp, err := GorgonRequest(url, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", }, }) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } // 解析响应 var result map[string]interface{} if err := json.Unmarshal(resp.Content, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 提取 data 字段 if data, ok := result["data"].(map[string]interface{}); ok { return data, nil } return result, nil } // GenerateParagraphsWithComments 生成带评论标记的内容 func GenerateParagraphsWithComments(itemID, bookID, content, baseURL string) (string, error) { // 获取段评数据 data, err := GetChapterReview(itemID) if err != nil { reviewLogger.Warning("获取段评数据失败: " + err.Error()) // 如果获取失败,返回原始内容 return content, nil } // 解析评论数据 commentData := make(map[string]struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` }) if dataMap, ok := data["data"].(map[string]interface{}); ok { for key, val := range dataMap { if valMap, ok := val.(map[string]interface{}); ok { count := 0 if c, ok := valMap["count"].(float64); ok { count = int(c) } hot := "" if h, ok := valMap["hot"].(string); ok { hot = h } isAuthor := false if ia, ok := valMap["is_author_comment"].(bool); ok { isAuthor = ia } userCount := 0 if uc, ok := valMap["user_count"].(float64); ok { userCount = int(uc) } commentData[key] = struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` }{ Count: count, Hot: hot, IsAuthorComment: isAuthor, UserCount: userCount, } } } } // 检测内容格式:是否包含HTML标签(检测实际内容,而非配置) isHTMLContent := strings.Contains(content, "

") || strings.Contains(content, "标签) // 因为标签需要XML兼容的格式 hasAnyComments := len(commentData) > 0 if hasAnyComments && !isHTMLContent { isHTMLContent = true } lines := strings.Split(content, "\n") var result strings.Builder paragraphIndex := 0 for i, line := range lines { isLastLine := i == len(lines)-1 trimmedLine := strings.TrimSpace(line) // 完全空行:直接输出换行符 if trimmedLine == "" { if !isLastLine { result.WriteString("\n") } continue } // 跳过图片标签和div标签 if strings.HasPrefix(trimmedLine, "") || strings.HasPrefix(trimmedLine, "标签,在

之前插入段评标记 if strings.HasPrefix(trimmedLine, "

") && strings.HasSuffix(trimmedLine, "

") { // 提取

和

之间的内容 innerContent := trimmedLine[3 : len(trimmedLine)-4] result.WriteString("

") result.WriteString(innerContent) // 添加评论标记 key := fmt.Sprintf("%d", paragraphIndex) if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { commentTag := fmt.Sprintf(``, baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) result.WriteString(commentTag) } result.WriteString("

") if !isLastLine { result.WriteString("\n") } paragraphIndex++ continue } // 如果是HTML模式,包裹

标签 if isHTMLContent { result.WriteString("

") result.WriteString(line) // 添加评论标记 key := fmt.Sprintf("%d", paragraphIndex) if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { commentTag := fmt.Sprintf(``, baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) result.WriteString(commentTag) } result.WriteString("

") if !isLastLine { result.WriteString("\n") } } else { // 纯文本模式:只在行尾添加comment标记 result.WriteString(line) // 添加评论标记(纯文本格式) key := fmt.Sprintf("%d", paragraphIndex) if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 { commentTag := fmt.Sprintf(``, baseURL, itemID, bookID, paragraphIndex, commentInfo.Count) result.WriteString(commentTag) } if !isLastLine { result.WriteString("\n") } } paragraphIndex++ } return result.String(), nil } // extractDeviceID 从 device header 中提取 device_id func extractDeviceID(header string) string { if header == "" { return "0" } // 从 header 中提取 device_id parts := strings.Split(header, "&") for _, part := range parts { if strings.HasPrefix(part, "device_id=") { return strings.TrimPrefix(part, "device_id=") } } return "0" } // ParseJSON 解析 JSON 字符串 func ParseJSON(data []byte, v interface{}) error { return json.Unmarshal(data, v) } // ToJSON 将对象序列化为 JSON 字符串 func ToJSON(v interface{}) (string, error) { buffer := &strings.Builder{} encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) // 禁用HTML转义,避免 " 被转义为 \" err := encoder.Encode(v) if err != nil { return "", err } // Encode 会在末尾添加换行符,需要去掉 result := buffer.String() return strings.TrimSuffix(result, "\n"), nil }