Files
mce/internal/common/helper.go
Victor_Jay fdb636f98f feat: 实现积分与等级系统 + 签到功能 + 下拉菜单优化
- User 模型增加 exp 字段,等级规则 Lv0-Lv6
- 新增签到记录(UserCheckIn)与任务记录(UserTask)模型,唯一索引防重
- 自动签到:AuthMiddleware 验证会话后,基于 Asia/Shanghai 自然日自动签到 +5 经验
- 手动签到:下拉菜单"签到"按钮兜底,已签到显示"今日已签到"并禁用
- 首次任务经验:上传头像/设置用户名/设置个性签名各 +10,审核通过与直接更新均触发
- 经验值变动后自动比对等级,升级时通过通知系统发送"恭喜提升至 LvX"
- 下拉菜单重构:顶部头像+用户名+LvX 等级标签,菜单项增加左侧图标,优化布局样式
- 新增 API:POST /api/level/checkin、GET /api/level/info
2026-05-31 20:58:14 +08:00

161 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package common
import (
"fmt"
"hash/crc32"
"io/fs"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"metazone.cc/metalab/internal/model"
"github.com/gin-gonic/gin"
)
// assetHashes 静态资源文件哈希表URL 路径 → 短哈希,文件不变哈希不变)
var assetHashes map[string]string
// ComputeAssetHashes 遍历 static 目录,计算每个文件的 CRC32 作为版本号
func ComputeAssetHashes(templateDir string) {
assetHashes = make(map[string]string)
// 前端主题templates/MetaLab-2026/static → URL /static/
walkStaticDir(filepath.Join(templateDir, "static"), "/static")
// 共享资源templates/shared/static → URL /shared/static/
walkStaticDir(filepath.Join(filepath.Dir(templateDir), "shared", "static"), "/shared/static")
// 管理后台templates/admin/static → URL /admin/static/
walkStaticDir(filepath.Join(filepath.Dir(templateDir), "admin", "static"), "/admin/static")
log.Printf("[AssetHashes] computed %d file hashes", len(assetHashes))
}
// walkStaticDir 遍历目录,计算每个 .js/.css 的 CRC32存入 assetHashes
func walkStaticDir(dir, urlPrefix string) {
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
ext := filepath.Ext(path)
if ext != ".js" && ext != ".css" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
h := crc32.ChecksumIEEE(data)
shortHash := strconv.FormatUint(uint64(h), 16)
// 生成 URL 路径urlPrefix + 相对于 dir 的子路径
rel, _ := filepath.Rel(dir, path)
urlPath := urlPrefix + "/" + filepath.ToSlash(rel)
assetHashes[urlPath] = shortHash
return nil
})
}
// AssetV 返回指定静态资源的哈希版本号(模板函数)
func AssetV(path string) string {
if h, ok := assetHashes[path]; ok {
return h
}
return "0"
}
// mdImageRe 匹配 Markdown 图片语法 ![alt](url)
var mdImageRe = regexp.MustCompile(`!\[.*?\]\((.*?)\)`)
// ExtractFirstImage 从 Markdown 正文提取第一张图片 URL
func ExtractFirstImage(md string) string {
match := mdImageRe.FindStringSubmatch(md)
if len(match) > 1 {
return match[1]
}
return ""
}
// BuildPageData 构建页面模板数据,自动注入登录状态、站点信息与 CSRF token
func BuildPageData(c *gin.Context, extra gin.H) gin.H {
data := gin.H{}
for k, v := range extra {
data[k] = v
}
if username, exists := c.Get("username"); exists {
data["IsLoggedIn"] = true
data["Username"] = username
}
if avatar, exists := c.Get("avatar"); exists {
data["Avatar"] = avatar
}
if expVal, exists := c.Get("exp"); exists {
if exp, ok := expVal.(int); ok {
data["Exp"] = exp
data["Level"] = model.GetLevelByExp(exp)
}
}
// 注入 CSRF token由 CSRF 中间件设置到上下文中)
if token, exists := c.Get("csrf_token"); exists {
data["CSRFToken"] = token
}
// 注入站点信息(由 InjectSiteInfo 中间件设置)
injectSiteInfo(c, data)
// 注入管理入口权限moderator 及以上可看到页脚管理面板链接)
if role, exists := c.Get("role"); exists {
data["CanAccessAdmin"] = model.HasMinRole(role.(string), model.RoleModerator)
data["IsOwner"] = model.HasMinRole(role.(string), model.RoleOwner)
}
// 注入维护状态
if isMaintenance, exists := c.Get("is_maintenance"); exists {
data["IsMaintenance"] = isMaintenance
}
return data
}
// BuildAdminPageData 构建管理后台模板数据
// 额外注入 UID、Role、CanManageUsers、CurrentPath 等权限信息
func BuildAdminPageData(c *gin.Context, extra gin.H) gin.H {
data := BuildPageData(c, extra)
data["IsAdmin"] = true
data["CurrentPath"] = c.Request.URL.Path
if uid, exists := c.Get("uid"); exists {
data["UID"] = uid
}
if role, exists := c.Get("role"); exists {
roleStr := role.(string)
data["Role"] = roleStr
if name, ok := model.RoleDisplayNames[roleStr]; ok {
data["RoleDisplayName"] = name
} else {
data["RoleDisplayName"] = roleStr
}
data["CanManageUsers"] = model.HasMinRole(roleStr, model.RoleAdmin)
data["CanManageAudits"] = model.HasMinRole(roleStr, model.RoleAdmin)
data["CanManageSettings"] = model.HasMinRole(roleStr, model.RoleOwner)
data["IsOwner"] = model.HasMinRole(roleStr, model.RoleOwner)
}
return data
}
// RedirectToLogin 重定向到登录页,携带当前页面路径作为 redirect 参数
func RedirectToLogin(c *gin.Context) {
returnURL := url.QueryEscape(c.Request.URL.Path)
if c.Request.URL.RawQuery != "" {
returnURL = url.QueryEscape(c.Request.URL.Path + "?" + c.Request.URL.RawQuery)
}
c.Redirect(http.StatusFound, fmt.Sprintf("/auth/login?redirect=%s", returnURL))
c.Abort()
}
// injectSiteInfo 从 context 注入站点展示信息
func injectSiteInfo(c *gin.Context, data gin.H) {
if framework, exists := c.Get("site_framework"); exists {
data["Framework"] = framework
}
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
data["CopyrightStart"] = copyrightStart
}
}