372 lines
10 KiB
Go
372 lines
10 KiB
Go
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, "<p>") ||
|
|
strings.Contains(content, "<div") ||
|
|
strings.Contains(content, "<article")
|
|
|
|
// 关键:如果检测到有段评数据,强制使用HTML模式(包裹<p>标签)
|
|
// 因为<comment>标签需要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, "<img") ||
|
|
strings.HasPrefix(trimmedLine, "<div") ||
|
|
strings.HasPrefix(trimmedLine, "</div>") ||
|
|
strings.HasPrefix(trimmedLine, "<article") ||
|
|
strings.HasPrefix(trimmedLine, "</article") {
|
|
result.WriteString(line)
|
|
if !isLastLine {
|
|
result.WriteString("\n")
|
|
}
|
|
continue
|
|
}
|
|
|
|
// 如果原内容已经有<p>标签,在</p>之前插入段评标记
|
|
if strings.HasPrefix(trimmedLine, "<p>") && strings.HasSuffix(trimmedLine, "</p>") {
|
|
// 提取<p>和</p>之间的内容
|
|
innerContent := trimmedLine[3 : len(trimmedLine)-4]
|
|
result.WriteString("<p>")
|
|
result.WriteString(innerContent)
|
|
|
|
// 添加评论标记
|
|
key := fmt.Sprintf("%d", paragraphIndex)
|
|
if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 {
|
|
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s¶=%d" count="%d" />`,
|
|
baseURL, itemID, bookID, paragraphIndex, commentInfo.Count)
|
|
result.WriteString(commentTag)
|
|
}
|
|
|
|
result.WriteString("</p>")
|
|
if !isLastLine {
|
|
result.WriteString("\n")
|
|
}
|
|
paragraphIndex++
|
|
continue
|
|
}
|
|
|
|
// 如果是HTML模式,包裹<p>标签
|
|
if isHTMLContent {
|
|
result.WriteString("<p>")
|
|
result.WriteString(line)
|
|
|
|
// 添加评论标记
|
|
key := fmt.Sprintf("%d", paragraphIndex)
|
|
if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 {
|
|
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s¶=%d" count="%d" />`,
|
|
baseURL, itemID, bookID, paragraphIndex, commentInfo.Count)
|
|
result.WriteString(commentTag)
|
|
}
|
|
|
|
result.WriteString("</p>")
|
|
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(`<comment ident="%s/review_html?item_id=%s&book_id=%s¶=%d" count="%d" />`,
|
|
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
|
|
}
|