🔧 chore: 初始化番茄小说 API 项目结构
新增项目基础框架,包含核心 API 实现、算法模块、HTTP 处理器等完整功能模块
This commit is contained in:
commit
6981148727
107 files changed
+12463
No files matched your search
@@ -0,0 +1,230 @@
|
||||
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(`<img src="%s" class="emoji-img" alt="%s" />`, url, displayName)
|
||||
result = strings.ReplaceAll(result, displayName, imgTag)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user