From 5bcc8a962ec923cf5078b389b0d71bb99d02c456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=AD=86?= <2685475410@qq.com> Date: Wed, 30 Sep 2026 00:25:09 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(handlers):=20stat?= =?UTF-8?q?us=20=E4=B8=8E=20review=20=E9=A1=B5=E9=9D=A2=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HTML 提取至 templates/status.html、templates/review.html(go:embed 内嵌) - status.go 1022→271 行、review.go 1281→438 行,动态数据全部经 html/template 按上下文自动转义 - 评论循环/作者徽章/贴纸/图片条件渲染翻译为模板 range/if;emojiMap 以 template.JS 注入 - 与旧版渲染输出实测对比:22 条评论结构完全一致;差异仅为 JS 上下文安全转义(& 等)与 html/template 剥除 CSS 注释,均无功能影响 - 其余文件为 gofmt 顺手格式化 --- internal/fqapi/audio.go | 1 - internal/fqapi/cache_manager.go | 14 +- internal/fqapi/catalog.go | 3 +- internal/fqapi/category_front.go | 1 - internal/fqapi/category_landing.go | 1 - internal/fqapi/content.go | 7 +- internal/fqapi/content_parser.go | 10 +- internal/fqapi/content_utils.go | 25 +- internal/fqapi/detail.go | 3 +- internal/fqapi/device_key_pool.go | 1090 ++++++++++++++-------------- internal/fqapi/device_register.go | 47 +- internal/fqapi/dhcry.go | 369 +++++----- internal/fqapi/emoji.go | 459 ++++++------ internal/fqapi/gorgon_utils.go | 1 - internal/fqapi/item_summary.go | 3 +- internal/fqapi/manga.go | 37 +- internal/fqapi/manga_proxy.go | 143 ++-- internal/fqapi/multi_content.go | 3 +- internal/fqapi/multi_detail.go | 1 - internal/fqapi/register_key.go | 1 - internal/fqapi/search.go | 1 - internal/fqapi/ttencrypt.go | 1 - internal/fqapi/video.go | 35 +- internal/handlers/book.go | 1 - internal/handlers/category.go | 1 - internal/handlers/content.go | 25 +- internal/handlers/manga_proxy.go | 11 +- internal/handlers/review.go | 1006 +++---------------------- internal/handlers/status.go | 860 ++-------------------- 29 files changed, 1267 insertions(+), 2893 deletions(-) diff --git a/internal/fqapi/audio.go b/internal/fqapi/audio.go index 6089466..1410dd0 100644 --- a/internal/fqapi/audio.go +++ b/internal/fqapi/audio.go @@ -132,4 +132,3 @@ func HandleAudio(itemIDs interface{}, toneID interface{}, output interface{}) (s audioLogger.Error(fmt.Sprintf("获取音频失败: code=%v, msg=%s", respCode, errMsg)) return BuildErrorResponse(errMsg), nil } - diff --git a/internal/fqapi/cache_manager.go b/internal/fqapi/cache_manager.go index c4e9180..217de69 100644 --- a/internal/fqapi/cache_manager.go +++ b/internal/fqapi/cache_manager.go @@ -379,7 +379,7 @@ func CleanupAllCache() error { freeBytes := freelistCount * pageSize // 阈值:空闲空间超过 5MB 时才执行压缩 const vacuumThreshold = 5 * 1024 * 1024 // 5MB - + if freeBytes >= vacuumThreshold { cacheLogger.Info(fmt.Sprintf("空闲空间 %.2f MB,开始执行压缩...", float64(freeBytes)/(1024*1024))) _, err = cacheDB.Exec("VACUUM") @@ -550,7 +550,7 @@ func GetDatabaseStats() (map[string]interface{}, error) { if err == nil { stats["page_count"] = pageCount } - + err = cacheDB.QueryRow("PRAGMA page_size").Scan(&pageSize) if err == nil { stats["page_size"] = pageSize @@ -589,7 +589,7 @@ func SaveStatistics(stats map[string]int64) error { `), stats["total"], stats["search"], stats["detail"], stats["catalog"], stats["content"], stats["manga"], stats["audio"], stats["video"], stats["category"], time.Now().Unix()) - + if err != nil { cacheLogger.Error("保存统计数据失败: " + err.Error()) return err @@ -605,13 +605,13 @@ func LoadStatistics() (map[string]int64, error) { } var total, search, detail, catalog, content, manga, audio, video, category int64 - + err := cacheDB.QueryRow(` SELECT total_requests, search_requests, detail_requests, catalog_requests, content_requests, manga_requests, audio_requests, video_requests, category_requests FROM statistics WHERE id = 1 `).Scan(&total, &search, &detail, &catalog, &content, &manga, &audio, &video, &category) - + if err == sql.ErrNoRows { // 没有数据,返回空统计 return map[string]int64{ @@ -619,7 +619,7 @@ func LoadStatistics() (map[string]int64, error) { "content": 0, "manga": 0, "audio": 0, "video": 0, "category": 0, }, nil } - + if err != nil { cacheLogger.Error("加载统计数据失败: " + err.Error()) return nil, err @@ -638,4 +638,4 @@ func LoadStatistics() (map[string]int64, error) { } return stats, nil -} \ No newline at end of file +} diff --git a/internal/fqapi/catalog.go b/internal/fqapi/catalog.go index 68ef370..5cbd711 100644 --- a/internal/fqapi/catalog.go +++ b/internal/fqapi/catalog.go @@ -12,7 +12,7 @@ var catalogLogger = common.NewLogger("catalog", "INFO", "") // HandleGetCatalog 处理获取书籍目录,目录数据基本不变,缓存 24 小时 func HandleGetCatalog(bookID string) (string, error) { - return withCache("catalog_" + bookID, 86400, func() (string, error) { + return withCache("catalog_"+bookID, 86400, func() (string, error) { return fetchCatalog(bookID) }) } @@ -210,4 +210,3 @@ func itemMultiDetail(itemList []string) ([]map[string]interface{}, error) { return nil, fmt.Errorf("请求失败") } - diff --git a/internal/fqapi/category_front.go b/internal/fqapi/category_front.go index 0bdd1a7..a91ad54 100644 --- a/internal/fqapi/category_front.go +++ b/internal/fqapi/category_front.go @@ -47,4 +47,3 @@ func fetchCategoryFront(tab interface{}) (string, error) { return BuildErrorResponse("获取失败"), nil } - diff --git a/internal/fqapi/category_landing.go b/internal/fqapi/category_landing.go index f99eeae..8742bb8 100644 --- a/internal/fqapi/category_landing.go +++ b/internal/fqapi/category_landing.go @@ -59,4 +59,3 @@ func fetchCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy int return BuildErrorResponse("获取失败"), nil } - diff --git a/internal/fqapi/content.go b/internal/fqapi/content.go index c75f004..2a83314 100644 --- a/internal/fqapi/content.go +++ b/internal/fqapi/content.go @@ -66,7 +66,7 @@ func fetchContentWithRetry(itemID string, textMode, imageMode bool, retryCount i contentLogger.Warning(fmt.Sprintf("已重试 %d 次,尝试备用接口", maxRetries)) return tryBackupAPI(itemID, textMode, imageMode) } - + // 从设备密钥池获取设备 pool := GetDeviceKeyPool() item, err := pool.GetNext() @@ -74,7 +74,7 @@ func fetchContentWithRetry(itemID string, textMode, imageMode bool, retryCount i contentLogger.Error("从设备池获取设备失败: " + err.Error()) return `{"code": -1, "msg": "设备池暂无可用设备"}`, err } - + header := item.DeviceHeader currentKeyInfo := &KeyInfo{ Key: item.Key, @@ -142,7 +142,7 @@ func fetchContentWithRetry(itemID string, textMode, imageMode bool, retryCount i keyVersion, _ := data["key_version"].(float64) if currentKeyInfo != nil && int(keyVersion) != currentKeyInfo.Ver { contentLogger.Warning("密钥版本不匹配,重新获取密钥") - + // 更新池中的密钥 newKeyInfo, err := GetKey(header) if err == nil { @@ -276,4 +276,3 @@ func tryBackupAPI(itemID string, textMode, imageMode bool) (string, error) { contentLogger.Warning("备用接口1获取正文出错!") return `{"code": -1, "msg": "获取失败"}`, fmt.Errorf("所有接口都失败") } - diff --git a/internal/fqapi/content_parser.go b/internal/fqapi/content_parser.go index da82eac..22bdd9e 100644 --- a/internal/fqapi/content_parser.go +++ b/internal/fqapi/content_parser.go @@ -13,7 +13,6 @@ var ( parserLogger = common.NewLogger("content_parser", "INFO", "") ) - // ContentParser 内容解析器 type ContentParser struct { cfg config.ContentConfig @@ -143,13 +142,13 @@ func (p *ContentParser) extractText(content string) string { // 拼接结果 var result string - + // 根据配置决定段落间的换行符 separator := "\n" if p.cfg.DoubleNewline { separator = "\n\n" } - + if p.cfg.AddParagraphIndent && len(finalResult) > 0 { // 添加段落缩进(两个全角空格) result = "\u3000\u3000" + strings.Join(finalResult, separator+"\u3000\u3000") @@ -170,10 +169,10 @@ func (p *ContentParser) extractText(content string) string { func (p *ContentParser) processHTMLMode(content string) string { // 标准化img属性 content = normalizeImgAttributes(content) - + // 解码HTML实体 content = decodeHTMLEntities(content) - + // 根据配置决定是否移除空行 if p.cfg.RemoveBlankLines { // 对HTML内容移除空的
标签
@@ -184,4 +183,3 @@ func (p *ContentParser) processHTMLMode(content string) string {
return content
}
-
diff --git a/internal/fqapi/content_utils.go b/internal/fqapi/content_utils.go
index 1a735bf..3bfca8a 100644
--- a/internal/fqapi/content_utils.go
+++ b/internal/fqapi/content_utils.go
@@ -57,10 +57,10 @@ func removeBlankLines(content string) string {
for strings.Contains(content, "\n\n\n") {
content = strings.ReplaceAll(content, "\n\n\n", "\n\n")
}
-
+
// 移除首尾的空白
content = strings.TrimSpace(content)
-
+
return content
}
@@ -127,22 +127,22 @@ func BuildURL(fullURL string, customParams ...string) string {
"os_version=0",
"version_code=58932",
}
-
+
// 添加设备header
header := getDeviceHeader()
if header != "" {
allParams = append(allParams, header)
}
-
+
// 添加自定义参数
allParams = append(allParams, customParams...)
-
+
// 合并、拆分、排序
joined := strings.Join(allParams, "&")
paramList := strings.Split(joined, "&")
sort.Strings(paramList)
params := strings.Join(paramList, "&")
-
+
return fullURL + "?" + params
}
@@ -218,11 +218,11 @@ func parseStringToInt(s string) (int, error) {
// 简单实现,避免导入strconv
result := 0
negative := false
-
+
if len(s) == 0 {
return 0, nil
}
-
+
start := 0
if s[0] == '-' {
negative = true
@@ -230,18 +230,18 @@ func parseStringToInt(s string) (int, error) {
} else if s[0] == '+' {
start = 1
}
-
+
for i := start; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
break
}
result = result*10 + int(s[i]-'0')
}
-
+
if negative {
result = -result
}
-
+
return result, nil
}
@@ -250,7 +250,7 @@ func ParseStringParam(param interface{}, defaultVal string) string {
if param == nil || param == "" {
return defaultVal
}
-
+
switch v := param.(type) {
case string:
if v != "" {
@@ -262,4 +262,3 @@ func ParseStringParam(param interface{}, defaultVal string) string {
}
return defaultVal
}
-
diff --git a/internal/fqapi/detail.go b/internal/fqapi/detail.go
index c973800..485f7d4 100644
--- a/internal/fqapi/detail.go
+++ b/internal/fqapi/detail.go
@@ -10,7 +10,7 @@ var detailLogger = common.NewLogger("detail", "INFO", "")
// HandleGetDetail 处理获取书籍详情,详情数据基本不变,缓存 24 小时
func HandleGetDetail(bookID string) (string, error) {
- return withCache("detail_" + bookID, 86400, func() (string, error) {
+ return withCache("detail_"+bookID, 86400, func() (string, error) {
return fetchDetail(bookID)
})
}
@@ -58,4 +58,3 @@ func fetchDetail(bookID string) (string, error) {
return BuildErrorResponse("获取失败"), nil
}
-
diff --git a/internal/fqapi/device_key_pool.go b/internal/fqapi/device_key_pool.go
index 4afd5e4..1a4e6ec 100644
--- a/internal/fqapi/device_key_pool.go
+++ b/internal/fqapi/device_key_pool.go
@@ -1,547 +1,543 @@
-package fqapi
-
-import (
- "fanqie/internal/common"
- "fanqie/internal/config"
- "fmt"
- "sync"
- "time"
-)
-
-var (
- poolLogger = common.NewLogger("device_key_pool", "INFO", "")
- globalPool *DeviceKeyPool
- poolOnce sync.Once
-)
-
-// DeviceKeyItem 设备密钥项
-type DeviceKeyItem struct {
- ID int `json:"id"`
- DeviceHeader string `json:"device_header"`
- Key string `json:"key"`
- KeyVer int `json:"key_ver"`
- IsValid bool `json:"is_valid"`
- CreatedAt time.Time `json:"created_at"`
- LastUsedAt time.Time `json:"last_used_at"`
-}
-
-// DeviceKeyPool 设备密钥池
-type DeviceKeyPool struct {
- mu sync.RWMutex
- currentIndex int
- items []*DeviceKeyItem
- enabled bool
-}
-
-// GetDeviceKeyPool 获取全局设备密钥池实例
-func GetDeviceKeyPool() *DeviceKeyPool {
- poolOnce.Do(func() {
- globalPool = &DeviceKeyPool{
- items: make([]*DeviceKeyItem, 0),
- enabled: false,
- }
- })
- return globalPool
-}
-
-// Initialize 初始化设备密钥池
-func (p *DeviceKeyPool) Initialize() error {
- cfg := config.GetConfig()
- if cfg == nil || !cfg.DeviceKeyPool.Enabled {
- poolLogger.Info("设备密钥池未启用")
- p.enabled = false
- return nil
- }
-
- p.enabled = true
- poolLogger.Info("开始初始化设备密钥池...")
-
- // 从数据库加载已有的设备
- if err := p.loadFromDB(); err != nil {
- poolLogger.Warning("从数据库加载设备失败: " + err.Error())
- }
-
- // 检查并补充设备到初始化数量
- validCount := p.getValidCount()
- initialSize := cfg.DeviceKeyPool.InitialSize
-
- if validCount < initialSize {
- needCount := initialSize - validCount
- poolLogger.Info(fmt.Sprintf("当前有效设备数: %d, 需要补充: %d", validCount, needCount))
-
- for i := 0; i < needCount; i++ {
- if err := p.addNewDevice(); err != nil {
- poolLogger.Error(fmt.Sprintf("添加设备失败: %v", err))
- }
- }
- }
-
- poolLogger.Info(fmt.Sprintf("设备密钥池初始化完成,当前有效设备数: %d", p.getValidCount()))
- return nil
-}
-
-// IsEnabled 检查设备密钥池是否启用
-func (p *DeviceKeyPool) IsEnabled() bool {
- return p.enabled
-}
-
-// GetNext 获取下一个可用的设备密钥(轮询方式)
-func (p *DeviceKeyPool) GetNext() (*DeviceKeyItem, error) {
- if !p.enabled {
- return nil, fmt.Errorf("设备密钥池未启用")
- }
-
- p.mu.Lock()
- defer p.mu.Unlock()
-
- // 过滤有效设备
- validItems := make([]*DeviceKeyItem, 0)
- for _, item := range p.items {
- if item.IsValid {
- validItems = append(validItems, item)
- }
- }
-
- if len(validItems) == 0 {
- return nil, fmt.Errorf("没有可用的设备")
- }
-
- // 轮询获取下一个设备
- item := validItems[p.currentIndex%len(validItems)]
- p.currentIndex++
-
- // 更新最后使用时间
- item.LastUsedAt = time.Now()
- p.updateDeviceInDB(item)
-
- // poolLogger.Debug(fmt.Sprintf("使用设备 ID=%d", item.ID))
-
- // 异步检查是否需要补充
- go p.checkAndRefill()
-
- return item, nil
-}
-
-// MarkInvalid 标记设备为无效
-func (p *DeviceKeyPool) MarkInvalid(deviceHeader string) error {
- if !p.enabled {
- return nil
- }
-
- p.mu.Lock()
- defer p.mu.Unlock()
-
- for _, item := range p.items {
- if item.DeviceHeader == deviceHeader {
- item.IsValid = false
- // poolLogger.Warning(fmt.Sprintf("设备 ID=%d 已标记为无效", item.ID))
-
- // 更新数据库
- if err := p.updateDeviceInDB(item); err != nil {
- return err
- }
-
- // 异步补充新设备
- go p.checkAndRefill()
-
- return nil
- }
- }
-
- return fmt.Errorf("未找到设备")
-}
-
-// RemoveInvalid 移除所有无效设备并自动补充
-func (p *DeviceKeyPool) RemoveInvalid() error {
- if !p.enabled {
- return nil
- }
-
- p.mu.Lock()
-
- // 从数据库中删除无效设备
- if err := p.removeInvalidFromDB(); err != nil {
- p.mu.Unlock()
- return err
- }
-
- // 从内存中移除无效设备
- validItems := make([]*DeviceKeyItem, 0)
- for _, item := range p.items {
- if item.IsValid {
- validItems = append(validItems, item)
- }
- }
-
- removedCount := len(p.items) - len(validItems)
- p.items = validItems
-
- if removedCount > 0 {
- poolLogger.Info(fmt.Sprintf("已移除 %d 个无效设备", removedCount))
-
- // 立即检查并补充设备(使用已持有锁的版本)
- p.checkAndRefillLocked()
- }
-
- p.mu.Unlock()
- return nil
-}
-
-// GetStats 获取池统计信息
-func (p *DeviceKeyPool) GetStats() map[string]interface{} {
- p.mu.RLock()
- defer p.mu.RUnlock()
-
- validCount := 0
- invalidCount := 0
-
- for _, item := range p.items {
- if item.IsValid {
- validCount++
- } else {
- invalidCount++
- }
- }
-
- return map[string]interface{}{
- "enabled": p.enabled,
- "total": len(p.items),
- "valid": validCount,
- "invalid": invalidCount,
- "current_index": p.currentIndex,
- }
-}
-
-// checkAndRefill 检查并补充设备(私有方法,会自动加锁)
-func (p *DeviceKeyPool) checkAndRefill() {
- if !p.enabled {
- return
- }
-
- cfg := config.GetConfig()
- if cfg == nil {
- return
- }
-
- p.mu.Lock()
- validCount := p.getValidCount()
- p.mu.Unlock()
-
- minSize := cfg.DeviceKeyPool.MinSize
- maxSize := cfg.DeviceKeyPool.MaxSize
-
- if validCount < minSize {
- needCount := minSize - validCount
- // 限制不超过最大值
- if validCount+needCount > maxSize {
- needCount = maxSize - validCount
- }
-
- if needCount > 0 {
- poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
-
- for i := 0; i < needCount; i++ {
- if err := p.addNewDevice(); err != nil {
- poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
- }
- }
- }
- }
-}
-
-// checkAndRefillLocked 检查并补充设备(已持有锁的内部版本)
-func (p *DeviceKeyPool) checkAndRefillLocked() {
- if !p.enabled {
- return
- }
-
- cfg := config.GetConfig()
- if cfg == nil {
- return
- }
-
- validCount := p.getValidCount()
- minSize := cfg.DeviceKeyPool.MinSize
- maxSize := cfg.DeviceKeyPool.MaxSize
-
- if validCount < minSize {
- needCount := minSize - validCount
- // 限制不超过最大值
- if validCount+needCount > maxSize {
- needCount = maxSize - validCount
- }
-
- if needCount > 0 {
- poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
-
- // 临时释放锁,因为addNewDevice需要加锁
- p.mu.Unlock()
- for i := 0; i < needCount; i++ {
- if err := p.addNewDevice(); err != nil {
- poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
- }
- }
- p.mu.Lock()
- }
- }
-}
-
-// getValidCount 获取有效设备数量(调用前需已加锁或在RLock内)
-func (p *DeviceKeyPool) getValidCount() int {
- count := 0
- for _, item := range p.items {
- if item.IsValid {
- count++
- }
- }
- return count
-}
-
-// addNewDevice 添加新设备(会自动加锁)
-func (p *DeviceKeyPool) addNewDevice() error {
- // 注册新设备
- deviceHeader, err := Register()
- if err != nil {
- return fmt.Errorf("注册设备失败: %w", err)
- }
-
- // 获取密钥
- keyInfo, err := GetKey(deviceHeader)
- if err != nil {
- return fmt.Errorf("获取密钥失败: %w", err)
- }
-
- // 创建设备项
- item := &DeviceKeyItem{
- DeviceHeader: deviceHeader,
- Key: keyInfo.Key,
- KeyVer: keyInfo.Ver,
- IsValid: true,
- CreatedAt: time.Now(),
- LastUsedAt: time.Now(),
- }
-
- // 保存到数据库
- if err := p.saveDeviceToDB(item); err != nil {
- return fmt.Errorf("保存设备到数据库失败: %w", err)
- }
-
- // 添加到内存
- p.mu.Lock()
- p.items = append(p.items, item)
- p.mu.Unlock()
-
- // poolLogger.Info(fmt.Sprintf("已添加新设备 ID=%d", item.ID))
- return nil
-}
-
-// ===== 数据库操作方法 =====
-
-// loadFromDB 从数据库加载设备
-func (p *DeviceKeyPool) loadFromDB() error {
- if cacheDB == nil {
- return fmt.Errorf("数据库未初始化")
- }
-
- rows, err := cacheDB.Query(`
- SELECT id, device_header, key, key_ver, is_valid, created_at, last_used_at
- FROM device_pool
- ORDER BY id
- `)
- if err != nil {
- return err
- }
- defer rows.Close()
-
- items := make([]*DeviceKeyItem, 0)
- for rows.Next() {
- var item DeviceKeyItem
- var createdAt, lastUsedAt int64
-
- err := rows.Scan(
- &item.ID,
- &item.DeviceHeader,
- &item.Key,
- &item.KeyVer,
- &item.IsValid,
- &createdAt,
- &lastUsedAt,
- )
- if err != nil {
- poolLogger.Error("扫描行失败: " + err.Error())
- continue
- }
-
- item.CreatedAt = time.Unix(createdAt, 0)
- item.LastUsedAt = time.Unix(lastUsedAt, 0)
- items = append(items, &item)
- }
-
- p.mu.Lock()
- p.items = items
- p.mu.Unlock()
-
- poolLogger.Info(fmt.Sprintf("从数据库加载了 %d 个设备", len(items)))
- return nil
-}
-
-// boolToInt 布尔转整数存储(is_valid 列为 INTEGER,PostgreSQL 严格要求类型匹配)
-func boolToInt(b bool) int {
- if b {
- return 1
- }
- return 0
-}
-
-// saveDeviceToDB 保存设备到数据库
-func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
- if cacheDB == nil {
- return fmt.Errorf("数据库未初始化")
- }
-
-
- // RETURNING id 同时兼容 SQLite(3.35+) 与 PostgreSQL,替代 LastInsertId()
- err := cacheDB.QueryRow(q(`
- INSERT INTO device_pool (device_header, key, key_ver, is_valid, created_at, last_used_at)
- VALUES (?, ?, ?, ?, ?, ?)
- RETURNING id
- `), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
- item.CreatedAt.Unix(), item.LastUsedAt.Unix()).Scan(&item.ID)
-
- return err
-}
-
-// updateDeviceInDB 更新设备信息
-func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error {
- if cacheDB == nil {
- return fmt.Errorf("数据库未初始化")
- }
-
-
- _, err := cacheDB.Exec(q(`
- UPDATE device_pool
- SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ?
- WHERE id = ?
- `), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
- item.LastUsedAt.Unix(), item.ID)
-
- return err
-}
-
-// removeInvalidFromDB 从数据库移除无效设备
-func (p *DeviceKeyPool) removeInvalidFromDB() error {
- if cacheDB == nil {
- return fmt.Errorf("数据库未初始化")
- }
-
-
- result, err := cacheDB.Exec(`DELETE FROM device_pool WHERE is_valid = 0`)
- if err != nil {
- return err
- }
-
- affected, _ := result.RowsAffected()
- if affected > 0 {
- poolLogger.Info(fmt.Sprintf("从数据库删除了 %d 个无效设备", affected))
- }
-
- return nil
-}
-
-// GetDeviceHeaderFromPool 从池中获取设备header(兼容旧接口)
-func GetDeviceHeaderFromPool() string {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return ""
- }
-
- item, err := pool.GetNext()
- if err != nil {
- poolLogger.Error("从池中获取设备失败: " + err.Error())
- return ""
- }
-
- return item.DeviceHeader
-}
-
-// GetKeyInfoFromPool 从池中获取密钥信息(兼容旧接口)
-func GetKeyInfoFromPool(deviceHeader string) *KeyInfo {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return nil
- }
-
- pool.mu.RLock()
- defer pool.mu.RUnlock()
-
- for _, item := range pool.items {
- if item.DeviceHeader == deviceHeader && item.IsValid {
- return &KeyInfo{
- Key: item.Key,
- Ver: item.KeyVer,
- }
- }
- }
-
- return nil
-}
-
-// UpdateKeyInPool 更新池中设备的密钥
-func UpdateKeyInPool(deviceHeader string, key string, ver int) error {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return nil
- }
-
- pool.mu.Lock()
- defer pool.mu.Unlock()
-
- for _, item := range pool.items {
- if item.DeviceHeader == deviceHeader {
- item.Key = key
- item.KeyVer = ver
- return pool.updateDeviceInDB(item)
- }
- }
-
- return fmt.Errorf("未找到设备")
-}
-
-// GetAnyValidDeviceHeader 获取任意一个有效设备的header(用于辅助功能)
-func GetAnyValidDeviceHeader() string {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return ""
- }
-
- pool.mu.RLock()
- defer pool.mu.RUnlock()
-
- for _, item := range pool.items {
- if item.IsValid {
- return item.DeviceHeader
- }
- }
-
- return ""
-}
-
-// GetValidDevice 获取一个有效的设备
-func GetValidDevice() (*DeviceKeyItem, error) {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return nil, fmt.Errorf("设备池未启用")
- }
- return pool.GetNext()
-}
-
-// GetCurrentDevice 获取当前使用的设备(和GetValidDevice一样,返回下一个可用设备)
-func GetCurrentDevice() *DeviceKeyItem {
- pool := GetDeviceKeyPool()
- if !pool.IsEnabled() {
- return nil
- }
- item, _ := pool.GetNext()
- return item
-}
-
+package fqapi
+
+import (
+ "fanqie/internal/common"
+ "fanqie/internal/config"
+ "fmt"
+ "sync"
+ "time"
+)
+
+var (
+ poolLogger = common.NewLogger("device_key_pool", "INFO", "")
+ globalPool *DeviceKeyPool
+ poolOnce sync.Once
+)
+
+// DeviceKeyItem 设备密钥项
+type DeviceKeyItem struct {
+ ID int `json:"id"`
+ DeviceHeader string `json:"device_header"`
+ Key string `json:"key"`
+ KeyVer int `json:"key_ver"`
+ IsValid bool `json:"is_valid"`
+ CreatedAt time.Time `json:"created_at"`
+ LastUsedAt time.Time `json:"last_used_at"`
+}
+
+// DeviceKeyPool 设备密钥池
+type DeviceKeyPool struct {
+ mu sync.RWMutex
+ currentIndex int
+ items []*DeviceKeyItem
+ enabled bool
+}
+
+// GetDeviceKeyPool 获取全局设备密钥池实例
+func GetDeviceKeyPool() *DeviceKeyPool {
+ poolOnce.Do(func() {
+ globalPool = &DeviceKeyPool{
+ items: make([]*DeviceKeyItem, 0),
+ enabled: false,
+ }
+ })
+ return globalPool
+}
+
+// Initialize 初始化设备密钥池
+func (p *DeviceKeyPool) Initialize() error {
+ cfg := config.GetConfig()
+ if cfg == nil || !cfg.DeviceKeyPool.Enabled {
+ poolLogger.Info("设备密钥池未启用")
+ p.enabled = false
+ return nil
+ }
+
+ p.enabled = true
+ poolLogger.Info("开始初始化设备密钥池...")
+
+ // 从数据库加载已有的设备
+ if err := p.loadFromDB(); err != nil {
+ poolLogger.Warning("从数据库加载设备失败: " + err.Error())
+ }
+
+ // 检查并补充设备到初始化数量
+ validCount := p.getValidCount()
+ initialSize := cfg.DeviceKeyPool.InitialSize
+
+ if validCount < initialSize {
+ needCount := initialSize - validCount
+ poolLogger.Info(fmt.Sprintf("当前有效设备数: %d, 需要补充: %d", validCount, needCount))
+
+ for i := 0; i < needCount; i++ {
+ if err := p.addNewDevice(); err != nil {
+ poolLogger.Error(fmt.Sprintf("添加设备失败: %v", err))
+ }
+ }
+ }
+
+ poolLogger.Info(fmt.Sprintf("设备密钥池初始化完成,当前有效设备数: %d", p.getValidCount()))
+ return nil
+}
+
+// IsEnabled 检查设备密钥池是否启用
+func (p *DeviceKeyPool) IsEnabled() bool {
+ return p.enabled
+}
+
+// GetNext 获取下一个可用的设备密钥(轮询方式)
+func (p *DeviceKeyPool) GetNext() (*DeviceKeyItem, error) {
+ if !p.enabled {
+ return nil, fmt.Errorf("设备密钥池未启用")
+ }
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ // 过滤有效设备
+ validItems := make([]*DeviceKeyItem, 0)
+ for _, item := range p.items {
+ if item.IsValid {
+ validItems = append(validItems, item)
+ }
+ }
+
+ if len(validItems) == 0 {
+ return nil, fmt.Errorf("没有可用的设备")
+ }
+
+ // 轮询获取下一个设备
+ item := validItems[p.currentIndex%len(validItems)]
+ p.currentIndex++
+
+ // 更新最后使用时间
+ item.LastUsedAt = time.Now()
+ p.updateDeviceInDB(item)
+
+ // poolLogger.Debug(fmt.Sprintf("使用设备 ID=%d", item.ID))
+
+ // 异步检查是否需要补充
+ go p.checkAndRefill()
+
+ return item, nil
+}
+
+// MarkInvalid 标记设备为无效
+func (p *DeviceKeyPool) MarkInvalid(deviceHeader string) error {
+ if !p.enabled {
+ return nil
+ }
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ for _, item := range p.items {
+ if item.DeviceHeader == deviceHeader {
+ item.IsValid = false
+ // poolLogger.Warning(fmt.Sprintf("设备 ID=%d 已标记为无效", item.ID))
+
+ // 更新数据库
+ if err := p.updateDeviceInDB(item); err != nil {
+ return err
+ }
+
+ // 异步补充新设备
+ go p.checkAndRefill()
+
+ return nil
+ }
+ }
+
+ return fmt.Errorf("未找到设备")
+}
+
+// RemoveInvalid 移除所有无效设备并自动补充
+func (p *DeviceKeyPool) RemoveInvalid() error {
+ if !p.enabled {
+ return nil
+ }
+
+ p.mu.Lock()
+
+ // 从数据库中删除无效设备
+ if err := p.removeInvalidFromDB(); err != nil {
+ p.mu.Unlock()
+ return err
+ }
+
+ // 从内存中移除无效设备
+ validItems := make([]*DeviceKeyItem, 0)
+ for _, item := range p.items {
+ if item.IsValid {
+ validItems = append(validItems, item)
+ }
+ }
+
+ removedCount := len(p.items) - len(validItems)
+ p.items = validItems
+
+ if removedCount > 0 {
+ poolLogger.Info(fmt.Sprintf("已移除 %d 个无效设备", removedCount))
+
+ // 立即检查并补充设备(使用已持有锁的版本)
+ p.checkAndRefillLocked()
+ }
+
+ p.mu.Unlock()
+ return nil
+}
+
+// GetStats 获取池统计信息
+func (p *DeviceKeyPool) GetStats() map[string]interface{} {
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+
+ validCount := 0
+ invalidCount := 0
+
+ for _, item := range p.items {
+ if item.IsValid {
+ validCount++
+ } else {
+ invalidCount++
+ }
+ }
+
+ return map[string]interface{}{
+ "enabled": p.enabled,
+ "total": len(p.items),
+ "valid": validCount,
+ "invalid": invalidCount,
+ "current_index": p.currentIndex,
+ }
+}
+
+// checkAndRefill 检查并补充设备(私有方法,会自动加锁)
+func (p *DeviceKeyPool) checkAndRefill() {
+ if !p.enabled {
+ return
+ }
+
+ cfg := config.GetConfig()
+ if cfg == nil {
+ return
+ }
+
+ p.mu.Lock()
+ validCount := p.getValidCount()
+ p.mu.Unlock()
+
+ minSize := cfg.DeviceKeyPool.MinSize
+ maxSize := cfg.DeviceKeyPool.MaxSize
+
+ if validCount < minSize {
+ needCount := minSize - validCount
+ // 限制不超过最大值
+ if validCount+needCount > maxSize {
+ needCount = maxSize - validCount
+ }
+
+ if needCount > 0 {
+ poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
+
+ for i := 0; i < needCount; i++ {
+ if err := p.addNewDevice(); err != nil {
+ poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
+ }
+ }
+ }
+ }
+}
+
+// checkAndRefillLocked 检查并补充设备(已持有锁的内部版本)
+func (p *DeviceKeyPool) checkAndRefillLocked() {
+ if !p.enabled {
+ return
+ }
+
+ cfg := config.GetConfig()
+ if cfg == nil {
+ return
+ }
+
+ validCount := p.getValidCount()
+ minSize := cfg.DeviceKeyPool.MinSize
+ maxSize := cfg.DeviceKeyPool.MaxSize
+
+ if validCount < minSize {
+ needCount := minSize - validCount
+ // 限制不超过最大值
+ if validCount+needCount > maxSize {
+ needCount = maxSize - validCount
+ }
+
+ if needCount > 0 {
+ poolLogger.Info(fmt.Sprintf("有效设备数 %d 低于最小值 %d,开始补充 %d 个设备", validCount, minSize, needCount))
+
+ // 临时释放锁,因为addNewDevice需要加锁
+ p.mu.Unlock()
+ for i := 0; i < needCount; i++ {
+ if err := p.addNewDevice(); err != nil {
+ poolLogger.Error(fmt.Sprintf("补充设备失败: %v", err))
+ }
+ }
+ p.mu.Lock()
+ }
+ }
+}
+
+// getValidCount 获取有效设备数量(调用前需已加锁或在RLock内)
+func (p *DeviceKeyPool) getValidCount() int {
+ count := 0
+ for _, item := range p.items {
+ if item.IsValid {
+ count++
+ }
+ }
+ return count
+}
+
+// addNewDevice 添加新设备(会自动加锁)
+func (p *DeviceKeyPool) addNewDevice() error {
+ // 注册新设备
+ deviceHeader, err := Register()
+ if err != nil {
+ return fmt.Errorf("注册设备失败: %w", err)
+ }
+
+ // 获取密钥
+ keyInfo, err := GetKey(deviceHeader)
+ if err != nil {
+ return fmt.Errorf("获取密钥失败: %w", err)
+ }
+
+ // 创建设备项
+ item := &DeviceKeyItem{
+ DeviceHeader: deviceHeader,
+ Key: keyInfo.Key,
+ KeyVer: keyInfo.Ver,
+ IsValid: true,
+ CreatedAt: time.Now(),
+ LastUsedAt: time.Now(),
+ }
+
+ // 保存到数据库
+ if err := p.saveDeviceToDB(item); err != nil {
+ return fmt.Errorf("保存设备到数据库失败: %w", err)
+ }
+
+ // 添加到内存
+ p.mu.Lock()
+ p.items = append(p.items, item)
+ p.mu.Unlock()
+
+ // poolLogger.Info(fmt.Sprintf("已添加新设备 ID=%d", item.ID))
+ return nil
+}
+
+// ===== 数据库操作方法 =====
+
+// loadFromDB 从数据库加载设备
+func (p *DeviceKeyPool) loadFromDB() error {
+ if cacheDB == nil {
+ return fmt.Errorf("数据库未初始化")
+ }
+
+ rows, err := cacheDB.Query(`
+ SELECT id, device_header, key, key_ver, is_valid, created_at, last_used_at
+ FROM device_pool
+ ORDER BY id
+ `)
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ items := make([]*DeviceKeyItem, 0)
+ for rows.Next() {
+ var item DeviceKeyItem
+ var createdAt, lastUsedAt int64
+
+ err := rows.Scan(
+ &item.ID,
+ &item.DeviceHeader,
+ &item.Key,
+ &item.KeyVer,
+ &item.IsValid,
+ &createdAt,
+ &lastUsedAt,
+ )
+ if err != nil {
+ poolLogger.Error("扫描行失败: " + err.Error())
+ continue
+ }
+
+ item.CreatedAt = time.Unix(createdAt, 0)
+ item.LastUsedAt = time.Unix(lastUsedAt, 0)
+ items = append(items, &item)
+ }
+
+ p.mu.Lock()
+ p.items = items
+ p.mu.Unlock()
+
+ poolLogger.Info(fmt.Sprintf("从数据库加载了 %d 个设备", len(items)))
+ return nil
+}
+
+// boolToInt 布尔转整数存储(is_valid 列为 INTEGER,PostgreSQL 严格要求类型匹配)
+func boolToInt(b bool) int {
+ if b {
+ return 1
+ }
+ return 0
+}
+
+// saveDeviceToDB 保存设备到数据库
+func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
+ if cacheDB == nil {
+ return fmt.Errorf("数据库未初始化")
+ }
+
+ // RETURNING id 同时兼容 SQLite(3.35+) 与 PostgreSQL,替代 LastInsertId()
+ err := cacheDB.QueryRow(q(`
+ INSERT INTO device_pool (device_header, key, key_ver, is_valid, created_at, last_used_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ RETURNING id
+ `), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
+ item.CreatedAt.Unix(), item.LastUsedAt.Unix()).Scan(&item.ID)
+
+ return err
+}
+
+// updateDeviceInDB 更新设备信息
+func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error {
+ if cacheDB == nil {
+ return fmt.Errorf("数据库未初始化")
+ }
+
+ _, err := cacheDB.Exec(q(`
+ UPDATE device_pool
+ SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ?
+ WHERE id = ?
+ `), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
+ item.LastUsedAt.Unix(), item.ID)
+
+ return err
+}
+
+// removeInvalidFromDB 从数据库移除无效设备
+func (p *DeviceKeyPool) removeInvalidFromDB() error {
+ if cacheDB == nil {
+ return fmt.Errorf("数据库未初始化")
+ }
+
+ result, err := cacheDB.Exec(`DELETE FROM device_pool WHERE is_valid = 0`)
+ if err != nil {
+ return err
+ }
+
+ affected, _ := result.RowsAffected()
+ if affected > 0 {
+ poolLogger.Info(fmt.Sprintf("从数据库删除了 %d 个无效设备", affected))
+ }
+
+ return nil
+}
+
+// GetDeviceHeaderFromPool 从池中获取设备header(兼容旧接口)
+func GetDeviceHeaderFromPool() string {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return ""
+ }
+
+ item, err := pool.GetNext()
+ if err != nil {
+ poolLogger.Error("从池中获取设备失败: " + err.Error())
+ return ""
+ }
+
+ return item.DeviceHeader
+}
+
+// GetKeyInfoFromPool 从池中获取密钥信息(兼容旧接口)
+func GetKeyInfoFromPool(deviceHeader string) *KeyInfo {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return nil
+ }
+
+ pool.mu.RLock()
+ defer pool.mu.RUnlock()
+
+ for _, item := range pool.items {
+ if item.DeviceHeader == deviceHeader && item.IsValid {
+ return &KeyInfo{
+ Key: item.Key,
+ Ver: item.KeyVer,
+ }
+ }
+ }
+
+ return nil
+}
+
+// UpdateKeyInPool 更新池中设备的密钥
+func UpdateKeyInPool(deviceHeader string, key string, ver int) error {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return nil
+ }
+
+ pool.mu.Lock()
+ defer pool.mu.Unlock()
+
+ for _, item := range pool.items {
+ if item.DeviceHeader == deviceHeader {
+ item.Key = key
+ item.KeyVer = ver
+ return pool.updateDeviceInDB(item)
+ }
+ }
+
+ return fmt.Errorf("未找到设备")
+}
+
+// GetAnyValidDeviceHeader 获取任意一个有效设备的header(用于辅助功能)
+func GetAnyValidDeviceHeader() string {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return ""
+ }
+
+ pool.mu.RLock()
+ defer pool.mu.RUnlock()
+
+ for _, item := range pool.items {
+ if item.IsValid {
+ return item.DeviceHeader
+ }
+ }
+
+ return ""
+}
+
+// GetValidDevice 获取一个有效的设备
+func GetValidDevice() (*DeviceKeyItem, error) {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return nil, fmt.Errorf("设备池未启用")
+ }
+ return pool.GetNext()
+}
+
+// GetCurrentDevice 获取当前使用的设备(和GetValidDevice一样,返回下一个可用设备)
+func GetCurrentDevice() *DeviceKeyItem {
+ pool := GetDeviceKeyPool()
+ if !pool.IsEnabled() {
+ return nil
+ }
+ item, _ := pool.GetNext()
+ return item
+}
diff --git a/internal/fqapi/device_register.go b/internal/fqapi/device_register.go
index d127a20..c2002d5 100644
--- a/internal/fqapi/device_register.go
+++ b/internal/fqapi/device_register.go
@@ -40,7 +40,7 @@ func currentMillis() int64 {
// custom_bt = current_millis - elapsed_ms_since_boot
func getCustomBT() string {
var uptimeSeconds float64
-
+
if runtime.GOOS == "linux" {
// 在Linux上读取/proc/uptime
data, err := os.ReadFile("/proc/uptime")
@@ -54,7 +54,7 @@ func getCustomBT() string {
}
}
}
-
+
// 如果读取失败,使用time.monotonic作为备选
if uptimeSeconds == 0 {
// Go没有直接的monotonic API,使用启动时间估算
@@ -65,17 +65,17 @@ func getCustomBT() string {
// 这里可以使用进程启动时间作为近似
uptimeSeconds = float64(time.Now().Unix() % 86400)
}
-
+
return fmt.Sprintf("%d", currentMillis()-int64(uptimeSeconds*1000))
}
// generateDeviceInfo 随机生成设备及操作系统信息
func generateDeviceInfo() map[string]interface{} {
osVersion := androidVersions[randInt(len(androidVersions))]
-
+
// 随机选择品牌
deviceBrand := deviceBrands[randInt(len(deviceBrands))]
-
+
// 根据品牌选择对应的型号
models := brandModels[deviceBrand]
deviceModel := models[randInt(len(models))]
@@ -88,19 +88,19 @@ func generateDeviceInfo() map[string]interface{} {
rand.Read(openUDIDBytes)
return map[string]interface{}{
- "os": "Android",
- "os_version": osVersion[0],
- "os_api": osVersion[1],
- "device_model": deviceModel,
- "device_brand": deviceBrand,
- "device_manufacturer": deviceBrand,
- "cpu_abi": "arm64-v8a",
- "release_build": releaseBuild,
- "cdid": generateUUID(),
- "sig_hash": "a4a27c2633195374c15651ffc3c4a497",
- "openudid": hex.EncodeToString(openUDIDBytes),
- "clientudid": generateUUID(),
- "req_id": generateUUID(),
+ "os": "Android",
+ "os_version": osVersion[0],
+ "os_api": osVersion[1],
+ "device_model": deviceModel,
+ "device_brand": deviceBrand,
+ "device_manufacturer": deviceBrand,
+ "cpu_abi": "arm64-v8a",
+ "release_build": releaseBuild,
+ "cdid": generateUUID(),
+ "sig_hash": "a4a27c2633195374c15651ffc3c4a497",
+ "openudid": hex.EncodeToString(openUDIDBytes),
+ "clientudid": generateUUID(),
+ "req_id": generateUUID(),
}
}
@@ -130,9 +130,9 @@ func generateOptionalFields() (map[string]interface{}, map[string]int64) {
genTime := currentMillis()
return map[string]interface{}{
- "rom": rom,
- "rom_version": fmt.Sprintf("coloros__%s.%s.%s release-keys", part1, part2, part3),
- "apk_first_install_time": genTime - int64(randInt(365))*86400000,
+ "rom": rom,
+ "rom_version": fmt.Sprintf("coloros__%s.%s.%s release-keys", part1, part2, part3),
+ "apk_first_install_time": genTime - int64(randInt(365))*86400000,
}, map[string]int64{
"_gen_time": genTime,
}
@@ -307,7 +307,7 @@ func Register() (string, error) {
// 生成完整请求体数据
data := GenerateFullRequestBody()
-
+
// 只记录关键设备信息
header := data["header"].(map[string]interface{})
logger.Info(fmt.Sprintf("生成设备信息 - 品牌: %s, 型号: %s, 系统: Android %v",
@@ -370,7 +370,7 @@ func Register() (string, error) {
deviceModel,
respData["install_id_str"])
- logger.Info("设备注册成功!" )
+ logger.Info("设备注册成功!")
// 尝试激活特权
tryActivatePremium(header)
@@ -381,4 +381,3 @@ func Register() (string, error) {
logger.Warning("设备注册失败!")
return "", fmt.Errorf("设备注册失败")
}
-
diff --git a/internal/fqapi/dhcry.go b/internal/fqapi/dhcry.go
index 10589a8..8efd71a 100644
--- a/internal/fqapi/dhcry.go
+++ b/internal/fqapi/dhcry.go
@@ -1,185 +1,184 @@
-package fqapi
-
-import (
- "crypto/aes"
- "crypto/cipher"
- "crypto/rand"
- "encoding/base64"
- "fmt"
- "math/big"
-)
-
-// CM 加密管理器
-type CM struct {
- bigC *big.Int // 大素数C
- bigB int64 // 底数B
- arryD []byte // 固定密钥
- thisD [2]*big.Int // 密钥对(私钥, 公钥)
- thisC []byte // 随机IV
-}
-
-// NewCM 创建新的CM实例
-func NewCM() *CM {
- // 大素数C
- bigCStr := "2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919"
- bigC := new(big.Int)
- bigC.SetString(bigCStr, 10)
-
- // 固定密钥(base64解码)
- arryD, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==")
-
- return &CM{
- bigC: bigC,
- bigB: 2,
- arryD: arryD,
- }
-}
-
-// generateKeyPair 生成密钥对
-func (cm *CM) generateKeyPair() [2]*big.Int {
- // 生成随机私钥
- randomBytes := make([]byte, 32)
- rand.Read(randomBytes)
-
- x := new(big.Int).SetBytes(randomBytes)
- x = x.Mod(x, new(big.Int).Sub(cm.bigC, big.NewInt(1)))
-
- // 计算公钥: y = b^x mod c
- y := new(big.Int).Exp(big.NewInt(cm.bigB), x, cm.bigC)
-
- cm.thisD = [2]*big.Int{x, y}
- return cm.thisD
-}
-
-// pairToBytes 将公钥转换为字节数组
-func (cm *CM) pairToBytes(keyPair [2]*big.Int) []byte {
- return keyPair[1].Bytes()
-}
-
-// randIV 生成随机IV
-func (cm *CM) randIV() []byte {
- iv := make([]byte, 16)
- rand.Read(iv)
- return iv
-}
-
-// encodeAES AES-CBC加密
-func (cm *CM) encodeAES(key, iv, data []byte) ([]byte, error) {
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
-
- // PKCS7填充
- padding := aes.BlockSize - len(data)%aes.BlockSize
- padText := make([]byte, padding)
- for i := range padText {
- padText[i] = byte(padding)
- }
- paddedData := append(data, padText...)
-
- ciphertext := make([]byte, len(paddedData))
- mode := cipher.NewCBCEncrypter(block, iv)
- mode.CryptBlocks(ciphertext, paddedData)
-
- return ciphertext, nil
-}
-
-// decodeAES AES-CBC解密
-func (cm *CM) decodeAES(key, iv, data []byte) ([]byte, error) {
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
-
- mode := cipher.NewCBCDecrypter(block, iv)
- decrypted := make([]byte, len(data))
- mode.CryptBlocks(decrypted, data)
-
- // 移除PKCS7填充
- padding := int(decrypted[len(decrypted)-1])
- if padding > len(decrypted) || padding > aes.BlockSize {
- return nil, fmt.Errorf("无效的填充")
- }
-
- return decrypted[:len(decrypted)-padding], nil
-}
-
-// ClientHandshake 客户端握手,生成请求头y
-func (cm *CM) ClientHandshake() string {
- keyPair := cm.generateKeyPair()
- cm.thisC = cm.randIV()
-
- publicKeyBytes := cm.pairToBytes(keyPair)
- encrypted, _ := cm.encodeAES(cm.arryD, cm.thisC, publicKeyBytes)
-
- combined := append(cm.thisC, encrypted...)
- return base64.StdEncoding.EncodeToString(combined)
-}
-
-// decryptF 解密函数
-func (cm *CM) decryptF(peerPublicKey []byte, keyPair [2]*big.Int, iv, data []byte) ([]byte, error) {
- x := keyPair[0]
-
- // 计算共享密钥: shared_secret = peer_public_key^x mod c
- peerPubInt := new(big.Int).SetBytes(peerPublicKey)
- sharedSecret := new(big.Int).Exp(peerPubInt, x, cm.bigC)
-
- key := sharedSecret.Bytes()
- // 确保key长度为32字节
- if len(key) < 32 {
- // 前面补0
- paddedKey := make([]byte, 32)
- copy(paddedKey[32-len(key):], key)
- key = paddedKey
- } else if len(key) > 32 {
- key = key[:32]
- }
-
- return cm.decodeAES(key, iv, data)
-}
-
-// Decrypt 解密响应内容
-func (cm *CM) Decrypt(c, y, content string) (string, error) {
- // c 应该为 "1" 表示启用加密
- // y 是服务器返回的公钥
- // content 是加密的内容
-
- if c != "1" || y == "" {
- return content, nil
- }
-
- // 解码content
- contentDecoded, err := base64.StdEncoding.DecodeString(content)
- if err != nil {
- return "", err
- }
-
- if len(contentDecoded) < 16 {
- return "", fmt.Errorf("内容太短")
- }
-
- // 提取IV和数据
- iv := contentDecoded[:16]
- data := contentDecoded[16:]
-
- // 解码服务器公钥
- peerPublicKey, err := base64.StdEncoding.DecodeString(y)
- if err != nil {
- return "", err
- }
-
- // 检查密钥对是否已初始化
- if cm.thisD[0] == nil {
- return "", fmt.Errorf("密钥对未初始化")
- }
-
- // 解密
- decrypted, err := cm.decryptF(peerPublicKey, cm.thisD, iv, data)
- if err != nil {
- return "", err
- }
-
- return string(decrypted), nil
-}
-
+package fqapi
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "math/big"
+)
+
+// CM 加密管理器
+type CM struct {
+ bigC *big.Int // 大素数C
+ bigB int64 // 底数B
+ arryD []byte // 固定密钥
+ thisD [2]*big.Int // 密钥对(私钥, 公钥)
+ thisC []byte // 随机IV
+}
+
+// NewCM 创建新的CM实例
+func NewCM() *CM {
+ // 大素数C
+ bigCStr := "2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919"
+ bigC := new(big.Int)
+ bigC.SetString(bigCStr, 10)
+
+ // 固定密钥(base64解码)
+ arryD, _ := base64.StdEncoding.DecodeString("rCXGfd2POMGzeiNIgo4iLg==")
+
+ return &CM{
+ bigC: bigC,
+ bigB: 2,
+ arryD: arryD,
+ }
+}
+
+// generateKeyPair 生成密钥对
+func (cm *CM) generateKeyPair() [2]*big.Int {
+ // 生成随机私钥
+ randomBytes := make([]byte, 32)
+ rand.Read(randomBytes)
+
+ x := new(big.Int).SetBytes(randomBytes)
+ x = x.Mod(x, new(big.Int).Sub(cm.bigC, big.NewInt(1)))
+
+ // 计算公钥: y = b^x mod c
+ y := new(big.Int).Exp(big.NewInt(cm.bigB), x, cm.bigC)
+
+ cm.thisD = [2]*big.Int{x, y}
+ return cm.thisD
+}
+
+// pairToBytes 将公钥转换为字节数组
+func (cm *CM) pairToBytes(keyPair [2]*big.Int) []byte {
+ return keyPair[1].Bytes()
+}
+
+// randIV 生成随机IV
+func (cm *CM) randIV() []byte {
+ iv := make([]byte, 16)
+ rand.Read(iv)
+ return iv
+}
+
+// encodeAES AES-CBC加密
+func (cm *CM) encodeAES(key, iv, data []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ // PKCS7填充
+ padding := aes.BlockSize - len(data)%aes.BlockSize
+ padText := make([]byte, padding)
+ for i := range padText {
+ padText[i] = byte(padding)
+ }
+ paddedData := append(data, padText...)
+
+ ciphertext := make([]byte, len(paddedData))
+ mode := cipher.NewCBCEncrypter(block, iv)
+ mode.CryptBlocks(ciphertext, paddedData)
+
+ return ciphertext, nil
+}
+
+// decodeAES AES-CBC解密
+func (cm *CM) decodeAES(key, iv, data []byte) ([]byte, error) {
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ mode := cipher.NewCBCDecrypter(block, iv)
+ decrypted := make([]byte, len(data))
+ mode.CryptBlocks(decrypted, data)
+
+ // 移除PKCS7填充
+ padding := int(decrypted[len(decrypted)-1])
+ if padding > len(decrypted) || padding > aes.BlockSize {
+ return nil, fmt.Errorf("无效的填充")
+ }
+
+ return decrypted[:len(decrypted)-padding], nil
+}
+
+// ClientHandshake 客户端握手,生成请求头y
+func (cm *CM) ClientHandshake() string {
+ keyPair := cm.generateKeyPair()
+ cm.thisC = cm.randIV()
+
+ publicKeyBytes := cm.pairToBytes(keyPair)
+ encrypted, _ := cm.encodeAES(cm.arryD, cm.thisC, publicKeyBytes)
+
+ combined := append(cm.thisC, encrypted...)
+ return base64.StdEncoding.EncodeToString(combined)
+}
+
+// decryptF 解密函数
+func (cm *CM) decryptF(peerPublicKey []byte, keyPair [2]*big.Int, iv, data []byte) ([]byte, error) {
+ x := keyPair[0]
+
+ // 计算共享密钥: shared_secret = peer_public_key^x mod c
+ peerPubInt := new(big.Int).SetBytes(peerPublicKey)
+ sharedSecret := new(big.Int).Exp(peerPubInt, x, cm.bigC)
+
+ key := sharedSecret.Bytes()
+ // 确保key长度为32字节
+ if len(key) < 32 {
+ // 前面补0
+ paddedKey := make([]byte, 32)
+ copy(paddedKey[32-len(key):], key)
+ key = paddedKey
+ } else if len(key) > 32 {
+ key = key[:32]
+ }
+
+ return cm.decodeAES(key, iv, data)
+}
+
+// Decrypt 解密响应内容
+func (cm *CM) Decrypt(c, y, content string) (string, error) {
+ // c 应该为 "1" 表示启用加密
+ // y 是服务器返回的公钥
+ // content 是加密的内容
+
+ if c != "1" || y == "" {
+ return content, nil
+ }
+
+ // 解码content
+ contentDecoded, err := base64.StdEncoding.DecodeString(content)
+ if err != nil {
+ return "", err
+ }
+
+ if len(contentDecoded) < 16 {
+ return "", fmt.Errorf("内容太短")
+ }
+
+ // 提取IV和数据
+ iv := contentDecoded[:16]
+ data := contentDecoded[16:]
+
+ // 解码服务器公钥
+ peerPublicKey, err := base64.StdEncoding.DecodeString(y)
+ if err != nil {
+ return "", err
+ }
+
+ // 检查密钥对是否已初始化
+ if cm.thisD[0] == nil {
+ return "", fmt.Errorf("密钥对未初始化")
+ }
+
+ // 解密
+ decrypted, err := cm.decryptF(peerPublicKey, cm.thisD, iv, data)
+ if err != nil {
+ return "", err
+ }
+
+ return string(decrypted), nil
+}
diff --git a/internal/fqapi/emoji.go b/internal/fqapi/emoji.go
index 3f5c756..2c760c0 100644
--- a/internal/fqapi/emoji.go
+++ b/internal/fqapi/emoji.go
@@ -1,230 +1,229 @@
-package fqapi
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
-)
-
-// EmojiManager 表情管理器
-type EmojiManager struct {
- emojiMap map[string]string // 表情文本 -> 图片URL的映射
- mutex sync.RWMutex
- lastUpdate time.Time
- cacheExpiry time.Duration
-}
-
-// DouyinEmojiResponse 抖音表情API响应结构
-type DouyinEmojiResponse struct {
- EmojiList []DouyinEmoji `json:"emoji_list"`
-}
-
-// DouyinEmoji 抖音单个表情结构
-type DouyinEmoji struct {
- OriginURI string `json:"origin_uri"`
- DisplayName string `json:"display_name"`
- Hide int `json:"hide"`
- EmojiURL struct {
- URI string `json:"uri"`
- URLList []string `json:"url_list"`
- } `json:"emoji_url"`
-}
-
-// LocalEmojiData 本地表情数据结构
-type LocalEmojiData struct {
- Version int `json:"version"`
- Size int `json:"size"`
- Datas map[string]string `json:"datas"`
-}
-
-var (
- globalEmojiManager *EmojiManager
- emojiOnce sync.Once
-)
-
-// GetEmojiManager 获取全局表情管理器实例
-func GetEmojiManager() *EmojiManager {
- emojiOnce.Do(func() {
- globalEmojiManager = &EmojiManager{
- emojiMap: make(map[string]string),
- cacheExpiry: 24 * time.Hour, // 缓存24小时
- }
- // 初始化时加载表情
- globalEmojiManager.LoadEmojis()
- })
- return globalEmojiManager
-}
-
-// LoadEmojis 加载表情映射(优先API,其次本地)
-func (em *EmojiManager) LoadEmojis() error {
- em.mutex.Lock()
- defer em.mutex.Unlock()
-
- // 检查缓存是否过期
- if time.Since(em.lastUpdate) < em.cacheExpiry && len(em.emojiMap) > 0 {
- return nil
- }
-
- newMap := make(map[string]string)
-
- // 1. 先尝试从抖音API获取表情
- apiEmojis, err := em.fetchDouyinEmojis()
- if err == nil && len(apiEmojis) > 0 {
- for displayName, url := range apiEmojis {
- newMap[displayName] = url
- }
- }
-
- // 2. 加载本地表情作为补充
- localEmojis, err := em.loadLocalEmojis()
- if err == nil {
- for displayName, url := range localEmojis {
- // 如果API中没有,则使用本地的
- if _, exists := newMap[displayName]; !exists {
- newMap[displayName] = url
- }
- }
- }
-
- // 3. 更新缓存
- if len(newMap) > 0 {
- em.emojiMap = newMap
- em.lastUpdate = time.Now()
- }
-
- return nil
-}
-
-// fetchDouyinEmojis 从抖音API获取表情映射
-func (em *EmojiManager) fetchDouyinEmojis() (map[string]string, error) {
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
-
- resp, err := client.Get("https://www.douyin.com/aweme/v1/web/emoji/list")
- if err != nil {
- return nil, fmt.Errorf("获取抖音表情API失败: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("抖音表情API返回状态码: %d", resp.StatusCode)
- }
-
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("读取响应失败: %w", err)
- }
-
- var apiResp DouyinEmojiResponse
- if err := json.Unmarshal(body, &apiResp); err != nil {
- return nil, fmt.Errorf("解析JSON失败: %w", err)
- }
-
- emojiMap := make(map[string]string)
- for _, emoji := range apiResp.EmojiList {
- if len(emoji.EmojiURL.URLList) > 0 {
- // 使用第一个URL
- emojiMap[emoji.DisplayName] = emoji.EmojiURL.URLList[0]
- }
- }
-
- return emojiMap, nil
-}
-
-// loadLocalEmojis 从本地文件加载表情映射
-func (em *EmojiManager) loadLocalEmojis() (map[string]string, error) {
- // 读取 data.json
- dataPath := filepath.Join("emoji", "data.json")
- data, err := ioutil.ReadFile(dataPath)
- if err != nil {
- return nil, fmt.Errorf("读取emoji/data.json失败: %w", err)
- }
-
- var localData LocalEmojiData
- if err := json.Unmarshal(data, &localData); err != nil {
- return nil, fmt.Errorf("解析emoji/data.json失败: %w", err)
- }
-
- emojiMap := make(map[string]string)
- for id, displayName := range localData.Datas {
- // 生成本地图片路径:emoji/images/emoji_dr_{id}.png
- // 转换为相对URL路径
- localPath := fmt.Sprintf("/emoji/images/emoji_dr_%s.png", id)
- emojiMap[displayName] = localPath
- }
-
- return emojiMap, nil
-}
-
-// GetEmojiURL 获取表情图片URL
-func (em *EmojiManager) GetEmojiURL(displayName string) (string, bool) {
- em.mutex.RLock()
- defer em.mutex.RUnlock()
-
- url, exists := em.emojiMap[displayName]
- return url, exists
-}
-
-// GetAllEmojis 获取所有表情映射(用于生成JavaScript代码)
-func (em *EmojiManager) GetAllEmojis() map[string]string {
- em.mutex.RLock()
- defer em.mutex.RUnlock()
-
- // 返回副本
- result := make(map[string]string, len(em.emojiMap))
- for k, v := range em.emojiMap {
- result[k] = v
- }
- return result
-}
-
-// RefreshEmojis 手动刷新表情缓存
-func (em *EmojiManager) RefreshEmojis() error {
- em.mutex.Lock()
- em.lastUpdate = time.Time{} // 重置更新时间
- em.mutex.Unlock()
-
- return em.LoadEmojis()
-}
-
-// CheckLocalEmojiFiles 检查本地表情文件是否存在
-func CheckLocalEmojiFiles() error {
- dataPath := filepath.Join("emoji", "data.json")
- if _, err := os.Stat(dataPath); os.IsNotExist(err) {
- return fmt.Errorf("emoji/data.json 文件不存在")
- }
-
- imagesDir := filepath.Join("emoji", "images")
- if _, err := os.Stat(imagesDir); os.IsNotExist(err) {
- return fmt.Errorf("emoji/images 目录不存在")
- }
-
- return nil
-}
-
-// ReplaceEmojisInText 替换文本中的表情为HTML img标签
-func ReplaceEmojisInText(text string) string {
- em := GetEmojiManager()
-
- // 匹配 [表情] 格式
- result := text
- for displayName, url := range em.GetAllEmojis() {
- // 转义特殊字符
- escapedName := strings.ReplaceAll(displayName, "[", "\\[")
- escapedName = strings.ReplaceAll(escapedName, "]", "\\]")
-
- // 替换为 img 标签
- imgTag := fmt.Sprintf(``, url, displayName)
- result = strings.ReplaceAll(result, displayName, imgTag)
- }
-
- return result
-}
-
+package fqapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+)
+
+// EmojiManager 表情管理器
+type EmojiManager struct {
+ emojiMap map[string]string // 表情文本 -> 图片URL的映射
+ mutex sync.RWMutex
+ lastUpdate time.Time
+ cacheExpiry time.Duration
+}
+
+// DouyinEmojiResponse 抖音表情API响应结构
+type DouyinEmojiResponse struct {
+ EmojiList []DouyinEmoji `json:"emoji_list"`
+}
+
+// DouyinEmoji 抖音单个表情结构
+type DouyinEmoji struct {
+ OriginURI string `json:"origin_uri"`
+ DisplayName string `json:"display_name"`
+ Hide int `json:"hide"`
+ EmojiURL struct {
+ URI string `json:"uri"`
+ URLList []string `json:"url_list"`
+ } `json:"emoji_url"`
+}
+
+// LocalEmojiData 本地表情数据结构
+type LocalEmojiData struct {
+ Version int `json:"version"`
+ Size int `json:"size"`
+ Datas map[string]string `json:"datas"`
+}
+
+var (
+ globalEmojiManager *EmojiManager
+ emojiOnce sync.Once
+)
+
+// GetEmojiManager 获取全局表情管理器实例
+func GetEmojiManager() *EmojiManager {
+ emojiOnce.Do(func() {
+ globalEmojiManager = &EmojiManager{
+ emojiMap: make(map[string]string),
+ cacheExpiry: 24 * time.Hour, // 缓存24小时
+ }
+ // 初始化时加载表情
+ globalEmojiManager.LoadEmojis()
+ })
+ return globalEmojiManager
+}
+
+// LoadEmojis 加载表情映射(优先API,其次本地)
+func (em *EmojiManager) LoadEmojis() error {
+ em.mutex.Lock()
+ defer em.mutex.Unlock()
+
+ // 检查缓存是否过期
+ if time.Since(em.lastUpdate) < em.cacheExpiry && len(em.emojiMap) > 0 {
+ return nil
+ }
+
+ newMap := make(map[string]string)
+
+ // 1. 先尝试从抖音API获取表情
+ apiEmojis, err := em.fetchDouyinEmojis()
+ if err == nil && len(apiEmojis) > 0 {
+ for displayName, url := range apiEmojis {
+ newMap[displayName] = url
+ }
+ }
+
+ // 2. 加载本地表情作为补充
+ localEmojis, err := em.loadLocalEmojis()
+ if err == nil {
+ for displayName, url := range localEmojis {
+ // 如果API中没有,则使用本地的
+ if _, exists := newMap[displayName]; !exists {
+ newMap[displayName] = url
+ }
+ }
+ }
+
+ // 3. 更新缓存
+ if len(newMap) > 0 {
+ em.emojiMap = newMap
+ em.lastUpdate = time.Now()
+ }
+
+ return nil
+}
+
+// fetchDouyinEmojis 从抖音API获取表情映射
+func (em *EmojiManager) fetchDouyinEmojis() (map[string]string, error) {
+ client := &http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ resp, err := client.Get("https://www.douyin.com/aweme/v1/web/emoji/list")
+ if err != nil {
+ return nil, fmt.Errorf("获取抖音表情API失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("抖音表情API返回状态码: %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("读取响应失败: %w", err)
+ }
+
+ var apiResp DouyinEmojiResponse
+ if err := json.Unmarshal(body, &apiResp); err != nil {
+ return nil, fmt.Errorf("解析JSON失败: %w", err)
+ }
+
+ emojiMap := make(map[string]string)
+ for _, emoji := range apiResp.EmojiList {
+ if len(emoji.EmojiURL.URLList) > 0 {
+ // 使用第一个URL
+ emojiMap[emoji.DisplayName] = emoji.EmojiURL.URLList[0]
+ }
+ }
+
+ return emojiMap, nil
+}
+
+// loadLocalEmojis 从本地文件加载表情映射
+func (em *EmojiManager) loadLocalEmojis() (map[string]string, error) {
+ // 读取 data.json
+ dataPath := filepath.Join("emoji", "data.json")
+ data, err := ioutil.ReadFile(dataPath)
+ if err != nil {
+ return nil, fmt.Errorf("读取emoji/data.json失败: %w", err)
+ }
+
+ var localData LocalEmojiData
+ if err := json.Unmarshal(data, &localData); err != nil {
+ return nil, fmt.Errorf("解析emoji/data.json失败: %w", err)
+ }
+
+ emojiMap := make(map[string]string)
+ for id, displayName := range localData.Datas {
+ // 生成本地图片路径:emoji/images/emoji_dr_{id}.png
+ // 转换为相对URL路径
+ localPath := fmt.Sprintf("/emoji/images/emoji_dr_%s.png", id)
+ emojiMap[displayName] = localPath
+ }
+
+ return emojiMap, nil
+}
+
+// GetEmojiURL 获取表情图片URL
+func (em *EmojiManager) GetEmojiURL(displayName string) (string, bool) {
+ em.mutex.RLock()
+ defer em.mutex.RUnlock()
+
+ url, exists := em.emojiMap[displayName]
+ return url, exists
+}
+
+// GetAllEmojis 获取所有表情映射(用于生成JavaScript代码)
+func (em *EmojiManager) GetAllEmojis() map[string]string {
+ em.mutex.RLock()
+ defer em.mutex.RUnlock()
+
+ // 返回副本
+ result := make(map[string]string, len(em.emojiMap))
+ for k, v := range em.emojiMap {
+ result[k] = v
+ }
+ return result
+}
+
+// RefreshEmojis 手动刷新表情缓存
+func (em *EmojiManager) RefreshEmojis() error {
+ em.mutex.Lock()
+ em.lastUpdate = time.Time{} // 重置更新时间
+ em.mutex.Unlock()
+
+ return em.LoadEmojis()
+}
+
+// CheckLocalEmojiFiles 检查本地表情文件是否存在
+func CheckLocalEmojiFiles() error {
+ dataPath := filepath.Join("emoji", "data.json")
+ if _, err := os.Stat(dataPath); os.IsNotExist(err) {
+ return fmt.Errorf("emoji/data.json 文件不存在")
+ }
+
+ imagesDir := filepath.Join("emoji", "images")
+ if _, err := os.Stat(imagesDir); os.IsNotExist(err) {
+ return fmt.Errorf("emoji/images 目录不存在")
+ }
+
+ return nil
+}
+
+// ReplaceEmojisInText 替换文本中的表情为HTML img标签
+func ReplaceEmojisInText(text string) string {
+ em := GetEmojiManager()
+
+ // 匹配 [表情] 格式
+ result := text
+ for displayName, url := range em.GetAllEmojis() {
+ // 转义特殊字符
+ escapedName := strings.ReplaceAll(displayName, "[", "\\[")
+ escapedName = strings.ReplaceAll(escapedName, "]", "\\]")
+
+ // 替换为 img 标签
+ imgTag := fmt.Sprintf(`
`, url, displayName)
+ result = strings.ReplaceAll(result, displayName, imgTag)
+ }
+
+ return result
+}
diff --git a/internal/fqapi/gorgon_utils.go b/internal/fqapi/gorgon_utils.go
index 0207359..593f121 100644
--- a/internal/fqapi/gorgon_utils.go
+++ b/internal/fqapi/gorgon_utils.go
@@ -114,4 +114,3 @@ func ContentRequest(urlStr string, options map[string]interface{}) (*common.Clie
client := common.NewHTTPClient()
return client.Request(urlStr, options)
}
-
diff --git a/internal/fqapi/item_summary.go b/internal/fqapi/item_summary.go
index d2f54cd..d9946dc 100644
--- a/internal/fqapi/item_summary.go
+++ b/internal/fqapi/item_summary.go
@@ -9,7 +9,7 @@ var itemSummaryLogger = common.NewLogger("item_summary", "INFO", "")
// HandleItemSummary 处理获取章节摘要,章节摘要数据基本不变,缓存 24 小时
func HandleItemSummary(itemIDs, bookID string) (string, error) {
- return withCache("item_summary_" + bookID + "_" + itemIDs, 86400, func() (string, error) {
+ return withCache("item_summary_"+bookID+"_"+itemIDs, 86400, func() (string, error) {
return fetchItemSummary(itemIDs, bookID)
})
}
@@ -48,4 +48,3 @@ func fetchItemSummary(itemIDs, bookID string) (string, error) {
return BuildErrorResponse("获取失败"), nil
}
-
diff --git a/internal/fqapi/manga.go b/internal/fqapi/manga.go
index 20bd570..5680549 100644
--- a/internal/fqapi/manga.go
+++ b/internal/fqapi/manga.go
@@ -71,14 +71,14 @@ func getMangaWithRetry(itemID string, showHTML bool, urlOutputMode string, retry
mangaLogger.Warning(fmt.Sprintf("已重试 %d 次,获取漫画失败", maxRetries))
return nil, fmt.Errorf("已重试 %d 次,所有设备均失败", maxRetries)
}
-
+
// 1. 从设备密钥池获取设备
pool := GetDeviceKeyPool()
item, err := pool.GetNext()
if err != nil {
return nil, fmt.Errorf("从设备池获取设备失败: %w", err)
}
-
+
header := item.DeviceHeader
keyInfo := &KeyInfo{
Key: item.Key,
@@ -171,18 +171,18 @@ func getMangaWithRetry(itemID string, showHTML bool, urlOutputMode string, retry
// 是漫画,决定输出模式
// 优先级:URL参数 > 配置文件 > 默认值
outputMode := "proxy" // 默认值
-
+
// 从配置文件读取
cfg := config.GetConfig()
if cfg != nil && cfg.Fanqie.Manga.OutputMode != "" {
outputMode = cfg.Fanqie.Manga.OutputMode
}
-
+
// URL参数优先级最高
if urlOutputMode != "" {
outputMode = urlOutputMode
}
-
+
// 解析漫画图片(传入keyInfo用于解密模式)
images, err := parseMangaImages(decrypted, showHTML, outputMode, itemID, keyInfo)
if err != nil {
@@ -279,7 +279,7 @@ func buildMangaResponse(images []ImageResult, showHTML bool) interface{} {
"content": htmlContent.String(),
}
}
-
+
return map[string]interface{}{
"images": images,
}
@@ -289,7 +289,7 @@ func buildMangaResponse(images []ImageResult, showHTML bool) interface{} {
func processURLMode(mangaData *MangaData) []ImageResult {
baseDomain := "https://p6-novel.byteimg.com/origin/"
validPath := detectMangaPath(baseDomain, mangaData.PicInfos[0].Md5)
-
+
images := make([]ImageResult, len(mangaData.PicInfos))
for i, pic := range mangaData.PicInfos {
imageURL := baseDomain + validPath + pic.Md5
@@ -309,7 +309,7 @@ func processProxyMode(mangaData *MangaData, itemID string) []ImageResult {
images := make([]ImageResult, len(mangaData.PicInfos))
for i, pic := range mangaData.PicInfos {
cacheKey := fmt.Sprintf("%s_%d", itemID, i)
-
+
// 存储图片信息到缓存
mangaCacheMutex.Lock()
mangaImageCache[cacheKey] = &MangaImageInfo{
@@ -320,7 +320,7 @@ func processProxyMode(mangaData *MangaData, itemID string) []ImageResult {
CachedAt: time.Now(),
}
mangaCacheMutex.Unlock()
-
+
// 返回短的代理URL
imageData := fmt.Sprintf("/manga/image/%s/%d", itemID, i)
images[i] = ImageResult{
@@ -390,7 +390,7 @@ func processDataURIMode(mangaData *MangaData) []ImageResult {
// Base64编码图片数据
base64Data := base64.StdEncoding.EncodeToString(decryptedData)
-
+
// Data URI模式:返回完整的Base64编码
mimeType := fmt.Sprintf("image/%s", format)
if format == "jpg" {
@@ -542,31 +542,31 @@ func decryptImage(encryptedData, keyBytes []byte) ([]byte, error) {
func detectMangaPath(baseDomain, firstMd5 string) string {
// 优先尝试 novel-images/
testURL := baseDomain + "novel-images/" + firstMd5
-
+
client := &http.Client{
Timeout: 8 * time.Second,
}
-
+
resp, err := client.Get(testURL)
if err != nil {
// 请求失败,使用novel-pic/
return "novel-pic/"
}
defer resp.Body.Close()
-
+
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return "novel-pic/"
}
-
+
// 检查是否为JSON响应(错误响应通常是JSON)
var jsonData interface{}
if json.Unmarshal(body, &jsonData) == nil {
// 是JSON,说明路径无效,切换到novel-pic/
return "novel-pic/"
}
-
+
// 不是JSON,说明返回的是图片,路径有效
return "novel-images/"
}
@@ -605,12 +605,11 @@ func getImageFormat(data []byte, url string) string {
}
// WEBP: RIFF...WEBP
- if bytes.HasPrefix(data, []byte{0x52, 0x49, 0x46, 0x46}) &&
- len(data) >= 12 &&
- bytes.Equal(data[8:12], []byte{0x57, 0x45, 0x42, 0x50}) {
+ if bytes.HasPrefix(data, []byte{0x52, 0x49, 0x46, 0x46}) &&
+ len(data) >= 12 &&
+ bytes.Equal(data[8:12], []byte{0x57, 0x45, 0x42, 0x50}) {
return "webp"
}
return "png"
}
-
diff --git a/internal/fqapi/manga_proxy.go b/internal/fqapi/manga_proxy.go
index 13b5ab3..b10d240 100644
--- a/internal/fqapi/manga_proxy.go
+++ b/internal/fqapi/manga_proxy.go
@@ -1,72 +1,71 @@
-package fqapi
-
-import (
- "encoding/hex"
- "fmt"
- "time"
-)
-
-// HandleMangaImageProxy 处理漫画图片代理请求
-func HandleMangaImageProxy(itemID string, index int) ([]byte, string, error) {
- // 从缓存获取图片信息
- cacheKey := fmt.Sprintf("%s_%d", itemID, index)
-
- mangaCacheMutex.RLock()
- imageInfo, exists := mangaImageCache[cacheKey]
- mangaCacheMutex.RUnlock()
-
- if !exists {
- return nil, "", fmt.Errorf("图片信息不存在,请先调用 /manga 接口")
- }
-
- // 检查缓存是否过期(5分钟)
- if time.Since(imageInfo.CachedAt) > 5*time.Minute {
- // 清理过期缓存
- mangaCacheMutex.Lock()
- delete(mangaImageCache, cacheKey)
- mangaCacheMutex.Unlock()
- return nil, "", fmt.Errorf("图片信息已过期,请重新调用 /manga 接口")
- }
-
- // 下载加密图片
- encryptedData, err := downloadImageWithRetry(imageInfo.PicURL, 2)
- if err != nil {
- return nil, "", fmt.Errorf("下载图片失败: %w", err)
- }
-
- // 解密图片
- keyBytes, err := hex.DecodeString(imageInfo.EncryptKey)
- if err != nil {
- return nil, "", fmt.Errorf("密钥解码失败: %w", err)
- }
-
- decryptedData, err := decryptImage(encryptedData, keyBytes)
- if err != nil {
- return nil, "", fmt.Errorf("解密失败: %w", err)
- }
-
- // 识别格式
- format := getImageFormat(decryptedData, imageInfo.PicURL)
-
- // 返回图片数据和MIME类型
- mimeType := fmt.Sprintf("image/%s", format)
- if format == "jpg" {
- mimeType = "image/jpeg"
- }
-
- return decryptedData, mimeType, nil
-}
-
-// CleanExpiredMangaCache 清理过期的漫画图片缓存
-func CleanExpiredMangaCache() {
- mangaCacheMutex.Lock()
- defer mangaCacheMutex.Unlock()
-
- now := time.Now()
- for key, info := range mangaImageCache {
- if now.Sub(info.CachedAt) > 5*time.Minute {
- delete(mangaImageCache, key)
- }
- }
-}
-
+package fqapi
+
+import (
+ "encoding/hex"
+ "fmt"
+ "time"
+)
+
+// HandleMangaImageProxy 处理漫画图片代理请求
+func HandleMangaImageProxy(itemID string, index int) ([]byte, string, error) {
+ // 从缓存获取图片信息
+ cacheKey := fmt.Sprintf("%s_%d", itemID, index)
+
+ mangaCacheMutex.RLock()
+ imageInfo, exists := mangaImageCache[cacheKey]
+ mangaCacheMutex.RUnlock()
+
+ if !exists {
+ return nil, "", fmt.Errorf("图片信息不存在,请先调用 /manga 接口")
+ }
+
+ // 检查缓存是否过期(5分钟)
+ if time.Since(imageInfo.CachedAt) > 5*time.Minute {
+ // 清理过期缓存
+ mangaCacheMutex.Lock()
+ delete(mangaImageCache, cacheKey)
+ mangaCacheMutex.Unlock()
+ return nil, "", fmt.Errorf("图片信息已过期,请重新调用 /manga 接口")
+ }
+
+ // 下载加密图片
+ encryptedData, err := downloadImageWithRetry(imageInfo.PicURL, 2)
+ if err != nil {
+ return nil, "", fmt.Errorf("下载图片失败: %w", err)
+ }
+
+ // 解密图片
+ keyBytes, err := hex.DecodeString(imageInfo.EncryptKey)
+ if err != nil {
+ return nil, "", fmt.Errorf("密钥解码失败: %w", err)
+ }
+
+ decryptedData, err := decryptImage(encryptedData, keyBytes)
+ if err != nil {
+ return nil, "", fmt.Errorf("解密失败: %w", err)
+ }
+
+ // 识别格式
+ format := getImageFormat(decryptedData, imageInfo.PicURL)
+
+ // 返回图片数据和MIME类型
+ mimeType := fmt.Sprintf("image/%s", format)
+ if format == "jpg" {
+ mimeType = "image/jpeg"
+ }
+
+ return decryptedData, mimeType, nil
+}
+
+// CleanExpiredMangaCache 清理过期的漫画图片缓存
+func CleanExpiredMangaCache() {
+ mangaCacheMutex.Lock()
+ defer mangaCacheMutex.Unlock()
+
+ now := time.Now()
+ for key, info := range mangaImageCache {
+ if now.Sub(info.CachedAt) > 5*time.Minute {
+ delete(mangaImageCache, key)
+ }
+ }
+}
diff --git a/internal/fqapi/multi_content.go b/internal/fqapi/multi_content.go
index 7753ab4..c88f0fb 100644
--- a/internal/fqapi/multi_content.go
+++ b/internal/fqapi/multi_content.go
@@ -116,7 +116,7 @@ func MultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignor
if err != nil {
return `{"code": -1, "msg": "从设备池获取设备失败"}`, err
}
-
+
header := item.DeviceHeader
currentKeyInfo := &KeyInfo{
Key: item.Key,
@@ -270,4 +270,3 @@ func MultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignor
func HandleMultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignoreFailed interface{}) (string, error) {
return MultiContent(itemIDs, bookID, textMode, imageMode, ignoreFailed, 0)
}
-
diff --git a/internal/fqapi/multi_detail.go b/internal/fqapi/multi_detail.go
index c404247..7fc4176 100644
--- a/internal/fqapi/multi_detail.go
+++ b/internal/fqapi/multi_detail.go
@@ -218,4 +218,3 @@ func fetchMultiDetail(bookID interface{}) (string, error) {
resultBytes, _ := json.Marshal(response)
return string(resultBytes), nil
}
-
diff --git a/internal/fqapi/register_key.go b/internal/fqapi/register_key.go
index 5831af0..fb30e06 100644
--- a/internal/fqapi/register_key.go
+++ b/internal/fqapi/register_key.go
@@ -246,4 +246,3 @@ func GetKeyInfo() *KeyInfo {
func init() {
rand.Seed(time.Now().UnixNano())
}
-
diff --git a/internal/fqapi/search.go b/internal/fqapi/search.go
index 8c2a248..152e392 100644
--- a/internal/fqapi/search.go
+++ b/internal/fqapi/search.go
@@ -97,4 +97,3 @@ func filterSearchTabs(respJSON map[string]interface{}, tabType int) {
respJSON["search_tabs"] = filtered
}
}
-
diff --git a/internal/fqapi/ttencrypt.go b/internal/fqapi/ttencrypt.go
index 23fd07f..a1c7e56 100644
--- a/internal/fqapi/ttencrypt.go
+++ b/internal/fqapi/ttencrypt.go
@@ -193,4 +193,3 @@ func Decrypt(encryptedStr string) ([]byte, error) {
}
return TTDecrypt(encryptedData)
}
-
diff --git a/internal/fqapi/video.go b/internal/fqapi/video.go
index 72d43b1..2d7f825 100644
--- a/internal/fqapi/video.go
+++ b/internal/fqapi/video.go
@@ -144,10 +144,10 @@ func decodeVideoModelAsJSON(videoModelStr string) interface{} {
// 解析失败,返回空对象
return map[string]interface{}{}
}
-
+
// 构建精简的返回结果,只包含关键字段
result := make(map[string]interface{})
-
+
// 提取关键字段
if videoID, ok := videoModel["video_id"].(string); ok {
result["video_id"] = videoID
@@ -158,16 +158,16 @@ func decodeVideoModelAsJSON(videoModelStr string) interface{} {
if posterURL, ok := videoModel["poster_url"].(string); ok {
result["poster_url"] = posterURL
}
-
+
// 处理 video_list
if videoList, ok := videoModel["video_list"].(map[string]interface{}); ok {
processedVideoList := make(map[string]interface{})
-
+
for quality, qualityData := range videoList {
if qualityMap, ok := qualityData.(map[string]interface{}); ok {
// 只保留关键字段
qualityInfo := make(map[string]interface{})
-
+
if definition, ok := qualityMap["definition"].(string); ok {
qualityInfo["definition"] = definition
}
@@ -189,39 +189,39 @@ func decodeVideoModelAsJSON(videoModelStr string) interface{} {
if fps, ok := qualityMap["fps"].(float64); ok {
qualityInfo["fps"] = int(fps)
}
-
+
// 解码 main_url
if mainURL, ok := qualityMap["main_url"].(string); ok {
if decoded, err := base64.StdEncoding.DecodeString(mainURL); err == nil {
qualityInfo["main_url"] = string(decoded)
}
}
-
+
// 解码 backup_url_1
if backupURL, ok := qualityMap["backup_url_1"].(string); ok {
if decoded, err := base64.StdEncoding.DecodeString(backupURL); err == nil {
qualityInfo["backup_url_1"] = string(decoded)
}
}
-
+
processedVideoList[quality] = qualityInfo
}
}
-
+
result["video_list"] = processedVideoList
}
-
+
return result
}
// formatDuration 将秒数转换为可读的时间格式
func formatDuration(seconds float64) string {
totalSeconds := int(math.Round(seconds))
-
+
hours := totalSeconds / 3600
minutes := (totalSeconds % 3600) / 60
secs := totalSeconds % 60
-
+
if hours > 0 {
return fmt.Sprintf("%d小时%d分%d秒", hours, minutes, secs)
} else if minutes > 0 {
@@ -236,17 +236,17 @@ func formatFileSize(bytes float64) string {
if bytes < unit {
return fmt.Sprintf("%.0f B", bytes)
}
-
+
kb := bytes / unit
if kb < unit {
return fmt.Sprintf("%.2f KB", kb)
}
-
+
mb := kb / unit
if mb < unit {
return fmt.Sprintf("%.2f MB", mb)
}
-
+
gb := mb / unit
return fmt.Sprintf("%.2f GB", gb)
}
@@ -255,12 +255,11 @@ func formatFileSize(bytes float64) string {
func formatBitrate(bitrate float64) string {
const unit = 1000.0
kbps := bitrate / unit
-
+
if kbps < unit {
return fmt.Sprintf("%.0f Kbps", kbps)
}
-
+
mbps := kbps / unit
return fmt.Sprintf("%.2f Mbps", mbps)
}
-
diff --git a/internal/handlers/book.go b/internal/handlers/book.go
index 7d84829..314ed95 100644
--- a/internal/handlers/book.go
+++ b/internal/handlers/book.go
@@ -103,4 +103,3 @@ func HandleMultiDetail(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, result)
}
-
diff --git a/internal/handlers/category.go b/internal/handlers/category.go
index 3549d05..462b18f 100644
--- a/internal/handlers/category.go
+++ b/internal/handlers/category.go
@@ -41,4 +41,3 @@ func HandleCategoryLanding(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, result)
}
-
diff --git a/internal/handlers/content.go b/internal/handlers/content.go
index 5cd6d23..5f1f3ae 100644
--- a/internal/handlers/content.go
+++ b/internal/handlers/content.go
@@ -20,9 +20,9 @@ func HandleContent(w http.ResponseWriter, r *http.Request) {
itemID := params["item_id"]
// 获取URL参数(支持动态覆盖配置)
- textMode := r.URL.Query().Get("text_mode") // 1=纯文本模式
- imageMode := r.URL.Query().Get("image_mode") // 0=移除图片, 1=保留图片
- reviewParam := r.URL.Query().Get("review") // 1=启用段评, 0=禁用段评
+ textMode := r.URL.Query().Get("text_mode") // 1=纯文本模式
+ imageMode := r.URL.Query().Get("image_mode") // 0=移除图片, 1=保留图片
+ reviewParam := r.URL.Query().Get("review") // 1=启用段评, 0=禁用段评
result, err := fqapi.HandleGetContent(itemID, textMode, imageMode)
if err != nil {
@@ -103,19 +103,19 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
if err := fqapi.ParseJSON([]byte(jsonResult), &resultData); err != nil {
return jsonResult
}
-
+
// 获取 data 字段
data, ok := resultData["data"].(map[string]interface{})
if !ok {
return jsonResult
}
-
+
// 获取 content 字段
content, ok := data["content"].(string)
if !ok || content == "" {
return jsonResult
}
-
+
// 从响应数据中提取 book_id (data.novel_data.book_id)
bookID := ""
if novelData, ok := data["novel_data"].(map[string]interface{}); ok {
@@ -125,7 +125,7 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
bookID = fmt.Sprintf("%.0f", bookIDVal)
}
}
-
+
// 获取基础 URL
cfg := config.GetConfig()
baseURL := ""
@@ -136,7 +136,7 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
}
port := cfg.Server.Port
scheme := "http"
-
+
// 检查是否来自外部请求
if r.Host != "" {
baseURL = scheme + "://" + r.Host
@@ -144,24 +144,23 @@ func addReviewToContent(jsonResult, itemID string, r *http.Request) string {
baseURL = scheme + "://" + host + ":" + port
}
}
-
+
// 生成带段评标记的内容
newContent, err := fqapi.GenerateParagraphsWithComments(itemID, bookID, content, baseURL)
if err != nil {
// 如果失败,返回原始内容
return jsonResult
}
-
+
// 替换 content 字段
data["content"] = newContent
resultData["data"] = data
-
+
// 重新序列化为 JSON
newJSON, err := fqapi.ToJSON(resultData)
if err != nil {
return jsonResult
}
-
+
return newJSON
}
-
diff --git a/internal/handlers/manga_proxy.go b/internal/handlers/manga_proxy.go
index 69067b9..3fce4b6 100644
--- a/internal/handlers/manga_proxy.go
+++ b/internal/handlers/manga_proxy.go
@@ -13,32 +13,31 @@ func HandleMangaImageProxy(w http.ResponseWriter, r *http.Request) {
// 从URL路径提取参数: /manga/image/{itemID}/{index}
path := strings.TrimPrefix(r.URL.Path, "/manga/image/")
parts := strings.Split(path, "/")
-
+
if len(parts) != 2 {
http.Error(w, "Invalid URL format. Expected: /manga/image/{itemID}/{index}", http.StatusBadRequest)
return
}
-
+
itemID := parts[0]
index, err := strconv.Atoi(parts[1])
if err != nil {
http.Error(w, "Invalid index parameter", http.StatusBadRequest)
return
}
-
+
// 获取解密后的图片数据
imageData, mimeType, err := fqapi.HandleMangaImageProxy(itemID, index)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get image: %s", err.Error()), http.StatusInternalServerError)
return
}
-
+
// 设置响应头
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Cache-Control", "public, max-age=3600") // 缓存1小时
w.Header().Set("Content-Length", strconv.Itoa(len(imageData)))
-
+
// 返回图片数据
w.Write(imageData)
}
-
diff --git a/internal/handlers/review.go b/internal/handlers/review.go
index 51d59b8..9fd00d1 100644
--- a/internal/handlers/review.go
+++ b/internal/handlers/review.go
@@ -1,9 +1,11 @@
package handlers
import (
+ _ "embed"
"encoding/json"
"fanqie/internal/fqapi"
"fmt"
+ "html/template"
"net/http"
"strconv"
"strings"
@@ -48,11 +50,11 @@ func parseIntParam(value string, paramName string) (int, error) {
func handleReviewError(w http.ResponseWriter, errType string, err error) {
status := http.StatusInternalServerError
msg := fmt.Sprintf("%s失败: %s", errType, err.Error())
-
+
if strings.Contains(err.Error(), "缺少") || strings.Contains(err.Error(), "格式错误") {
status = http.StatusBadRequest
}
-
+
respondJSON(w, status, map[string]interface{}{
"code": -1,
"msg": msg,
@@ -71,38 +73,38 @@ func handleReviewSuccess(w http.ResponseWriter, data interface{}) {
// HandleChapterReview 处理章节评论请求(混合数据)
func HandleChapterReview(w http.ResponseWriter, r *http.Request) {
incrementStat("total")
-
+
// 验证必需参数
params, err := validateParams(r, "item_id")
if err != nil {
handleReviewError(w, "参数验证", err)
return
}
-
+
// 获取可选参数
bookID := getOptionalParam(r, "book_id", "")
-
+
// 调用 API
data, err := fqapi.GetChapterMixData(bookID, params["item_id"])
if err != nil {
handleReviewError(w, "获取章节评论", err)
return
}
-
+
handleReviewSuccess(w, data)
}
// HandleReview 处理段落评论请求
func HandleReview(w http.ResponseWriter, r *http.Request) {
incrementStat("total")
-
+
// 验证必需参数
params, err := validateParams(r, "item_id")
if err != nil {
handleReviewError(w, "参数验证", err)
return
}
-
+
// 获取可选参数
bookID := getOptionalParam(r, "book_id", "")
commentID := getOptionalParam(r, "comment_id", "")
@@ -110,39 +112,39 @@ func HandleReview(w http.ResponseWriter, r *http.Request) {
sortStr := getOptionalParam(r, "sort", "1")
offsetStr := getOptionalParam(r, "offset", "0")
countStr := getOptionalParam(r, "count", "20")
-
+
// 解析整数参数
para, err := parseIntParam(paraStr, "para")
if err != nil {
handleReviewError(w, "参数解析", err)
return
}
-
+
sort, err := parseIntParam(sortStr, "sort")
if err != nil {
sort = 1 // 默认热门
}
-
+
offset, _ := parseIntParam(offsetStr, "offset")
count, _ := parseIntParam(countStr, "count")
if count <= 0 {
count = 20
}
-
+
// 调用 API
data, err := fqapi.GetParaReview(params["item_id"], bookID, commentID, para, sort, offset, count)
if err != nil {
handleReviewError(w, "获取段落评论", err)
return
}
-
+
handleReviewSuccess(w, data)
}
// HandleReviewHTML 处理段落评论 HTML 显示
func HandleReviewHTML(w http.ResponseWriter, r *http.Request) {
incrementStat("total")
-
+
// 验证必需参数
params, err := validateParams(r, "item_id", "para")
if err != nil {
@@ -150,7 +152,7 @@ func HandleReviewHTML(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "
评论页渲染失败: " + htmlEscape(err.Error()) + "
" + } + return b.String() } // htmlEscape HTML 转义 @@ -1252,7 +409,7 @@ func htmlEscape(s string) string { func generateEmojiMapJS() string { em := fqapi.GetEmojiManager() emojiMap := em.GetAllEmojis() - + var sb strings.Builder sb.WriteString("{") first := true @@ -1278,4 +435,3 @@ func respondJSON(w http.ResponseWriter, statusCode int, data interface{}) { w.WriteHeader(statusCode) json.NewEncoder(w).Encode(data) } - diff --git a/internal/handlers/status.go b/internal/handlers/status.go index 0276dd1..722aa31 100644 --- a/internal/handlers/status.go +++ b/internal/handlers/status.go @@ -1,11 +1,13 @@ package handlers import ( + _ "embed" "encoding/json" "fanqie/internal/algos" "fanqie/internal/config" "fanqie/internal/fqapi" "fmt" + "html/template" "net/http" "time" ) @@ -13,7 +15,7 @@ import ( // HandleVacuum 处理数据库压缩请求 func HandleVacuum(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - + // 只允许 POST 请求 if r.Method != http.MethodPost { json.NewEncoder(w).Encode(map[string]interface{}{ @@ -22,7 +24,7 @@ func HandleVacuum(w http.ResponseWriter, r *http.Request) { }) return } - + // 执行 VACUUM err := fqapi.VacuumDatabase() if err != nil { @@ -32,18 +34,25 @@ func HandleVacuum(w http.ResponseWriter, r *http.Request) { }) return } - + json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "数据库压缩完成", }) } +// 状态页模板(embed 内嵌,动态数据由 html/template 自动转义) +// +//go:embed templates/status.html +var statusHTML string + +var statusTmpl = template.Must(template.New("status").Parse(statusHTML)) + // HandleStatus 状态页面处理 func HandleStatus(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") uptime := time.Since(startTime) - + // 格式化运行时间 days := int(uptime.Hours() / 24) hours := int(uptime.Hours()) % 24 @@ -59,10 +68,10 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) { } else { uptimeStr = fmt.Sprintf("%d秒", seconds) } - + // 获取签名信息 currentSign := algos.GlobalSignatureSwitcher.GetCurrentMethod() - + // 生成签名详情显示 var signatureDetails string if currentSign == "sign2" { @@ -70,800 +79,40 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) { } else { signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos" } - + // 获取统计数据 stats := getStatsSnapshot() - + // 获取设备密钥池状态 poolStatus := getDeviceKeyPoolStatus() - - html := fmt.Sprintf(` - - - - - -运行状态监控面板
-