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(`%s`, url, displayName) result = strings.ReplaceAll(result, displayName, imgTag) } return result }