♻️ refactor(handlers): status 与 review 页面模板化

- 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 顺手格式化
This commit is contained in:
chenxin committed 2026-09-30 00:25:09 +08:00
1 parent ad6d28fd85
commit 5bcc8a962e
29 files changed
+1267 -2893

No files matched your search

+229 -230
View File
@@ -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(`<img src="%s" class="emoji-img" alt="%s" />`, 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(`<img src="%s" class="emoji-img" alt="%s" />`, url, displayName)
result = strings.ReplaceAll(result, displayName, imgTag)
}
return result
}