fix: ComputeAssetHashes 并发安全 + theme 页重复加载 JS 导致双写 panic
Some checks failed
CI / Lint + Build (push) Has been cancelled

- helper.go: 用 sync.RWMutex 保护 assetHashes,构建本地 map 后原子替换
- theme.html: 删除硬编码 script 标签(footer ExtraJS 已加载),
  修复 site-settings-theme.js 执行两次 → applyTheme 触发两次 → 并发写 map panic
This commit is contained in:
2026-06-30 19:10:11 +08:00
parent d4a833f149
commit 426bedda65
2 changed files with 24 additions and 12 deletions

View File

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"sync"
"time" "time"
"metazone.cc/mce/internal/model" "metazone.cc/mce/internal/model"
@ -19,22 +20,31 @@ import (
) )
// assetHashes 静态资源文件哈希表URL 路径 → 短哈希,文件不变哈希不变) // assetHashes 静态资源文件哈希表URL 路径 → 短哈希,文件不变哈希不变)
var assetHashes map[string]string var (
assetHashes map[string]string
assetMu sync.RWMutex
)
// ComputeAssetHashes 遍历 static 目录,计算每个文件的 CRC32 作为版本号 // ComputeAssetHashes 遍历 static 目录,计算每个文件的 CRC32 作为版本号
// 构建本地 map 后原子替换,避免并发读写冲突
func ComputeAssetHashes(templateDir string) { func ComputeAssetHashes(templateDir string) {
assetHashes = make(map[string]string) mu := make(map[string]string)
// 前端主题templates/MetaLab-2026/static → URL /static/ // 前端主题templates/MetaLab-2026/static → URL /static/
walkStaticDir(filepath.Join(templateDir, "static"), "/static") walkStaticDirInto(mu, filepath.Join(templateDir, "static"), "/static")
// 共享资源templates/shared/static → URL /shared/static/ // 共享资源templates/shared/static → URL /shared/static/
walkStaticDir(filepath.Join(filepath.Dir(templateDir), "shared", "static"), "/shared/static") walkStaticDirInto(mu, filepath.Join(filepath.Dir(templateDir), "shared", "static"), "/shared/static")
// 管理后台templates/admin/static → URL /admin/static/ // 管理后台templates/admin/static → URL /admin/static/
walkStaticDir(filepath.Join(filepath.Dir(templateDir), "admin", "static"), "/admin/static") walkStaticDirInto(mu, filepath.Join(filepath.Dir(templateDir), "admin", "static"), "/admin/static")
log.Printf("[AssetHashes] computed %d file hashes", len(assetHashes))
assetMu.Lock()
assetHashes = mu
assetMu.Unlock()
log.Printf("[AssetHashes] computed %d file hashes", len(mu))
} }
// walkStaticDir 遍历目录,计算每个 .js/.css 的 CRC32存入 assetHashes // walkStaticDirInto 遍历目录,计算每个 .js/.css 的 CRC32存入目标 map
func walkStaticDir(dir, urlPrefix string) { func walkStaticDirInto(dst map[string]string, dir, urlPrefix string) {
_ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() { if err != nil || d.IsDir() {
return nil return nil
@ -52,14 +62,17 @@ func walkStaticDir(dir, urlPrefix string) {
// 生成 URL 路径urlPrefix + 相对于 dir 的子路径 // 生成 URL 路径urlPrefix + 相对于 dir 的子路径
rel, _ := filepath.Rel(dir, path) rel, _ := filepath.Rel(dir, path)
urlPath := urlPrefix + "/" + filepath.ToSlash(rel) urlPath := urlPrefix + "/" + filepath.ToSlash(rel)
assetHashes[urlPath] = shortHash dst[urlPath] = shortHash
return nil return nil
}) })
} }
// AssetV 返回指定静态资源的哈希版本号(模板函数) // AssetV 返回指定静态资源的哈希版本号(模板函数,并发安全
func AssetV(path string) string { func AssetV(path string) string {
if h, ok := assetHashes[path]; ok { assetMu.RLock()
h, ok := assetHashes[path]
assetMu.RUnlock()
if ok {
return h return h
} }
return "0" return "0"

View File

@ -41,4 +41,3 @@
</div> </div>
</div> </div>
{{template "admin/layout/footer.html" .}} {{template "admin/layout/footer.html" .}}
<script src="/admin/static/js/site-settings-theme.js?v={{assetV "/admin/static/js/site-settings-theme.js"}}" nonce="{{.CSPNonce}}"></script>