Compare commits

..

3 Commits

Author SHA1 Message Date
426bedda65 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
2026-06-30 19:10:11 +08:00
d4a833f149 fix(admin): security 页 password.strength 下拉未从 API 恢复当前值 2026-06-30 19:08:11 +08:00
3f4d827926 fix(admin): 注册设置 toggle 读错数据源 — 从 audit 读 registration.* 导致状态丢失 2026-06-30 19:06:37 +08:00
3 changed files with 31 additions and 13 deletions

View File

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

View File

@ -41,4 +41,3 @@
</div>
</div>
{{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>

View File

@ -50,7 +50,7 @@
if (t.id === 'maintenance-enabled') {
el.checked = allSettings[t.key] === 'true';
} else if (t.id === 'registration-enabled') {
} else if (t.key.indexOf('registration.') === 0) {
el.checked = allSettings[t.key] !== 'false';
} else {
el.checked = audit[t.key.split('.')[1]] === true;
@ -63,6 +63,12 @@
// 文本输入保存按钮
bindSaveButtons();
// 密码强度下拉(服务端渲染默认值,需从 API 覆盖以显示最新保存值)
var strengthSelect = document.getElementById('setting-password-strength');
if (strengthSelect && allSettings['password.strength']) {
strengthSelect.value = allSettings['password.strength'];
}
// 社区准则文本域
var guidelinesEl = document.getElementById('setting-site-guidelines');
if (guidelinesEl) {