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
}