Compare commits

..

8 Commits

Author SHA1 Message Date
b997528bad docs: README 注明维护暂停状态
Some checks failed
CI / Lint + Build (push) Has been cancelled
2026-06-30 19:16:23 +08:00
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
1eb3cc7c86 debug: 添加站点设置持久化调试日志,追踪 Set/Get 链路
Some checks failed
CI / Lint + Build (push) Has been cancelled
2026-06-30 19:01:25 +08:00
20067f7640 fix(admin): 修复多页面设置加载失败 — renderOtherSettings 空指针崩溃
Some checks failed
CI / Lint + Build (push) Has been cancelled
根因:site-settings.js 的 renderOtherSettings() 无条件访问
#general-settings 元素,但 brand/security/registration 三个模板
无此容器,导致 TypeError 被 catch 吞掉 → '加载设置失败'。

额外修复:
- footer.html: ExtraJS 添加 ?v= 缓存版本哈希
- site-settings.js: API 失败时显式展示服务端错误消息(不再静默 return)
- site_settings.go: Set() 改用 UPSERT(clause.OnConflict),
  修复 GORM Save() 对不存在 key 首次保存时返回 ErrRecordNotFound
2026-06-30 18:56:42 +08:00
ecbae7c4fa fix(admin): 修复设置页面 CSP style-src 违规导致加载失败
Some checks failed
CI / Lint + Build (push) Has been cancelled
- 移除 site-settings 模板中的内联 style 属性(display:none, width:100px)
- 将内联样式迁移至 site-settings.css 的 CSS 类中
- site-settings-theme.js 用 className 替代 style.display 操作
2026-06-30 18:19:50 +08:00
b10828c851 chore: 排除自用主题 templates/jay-pub/ 版本控制 2026-06-30 18:06:35 +08:00
13 changed files with 72 additions and 23 deletions

3
.gitignore vendored
View File

@ -23,6 +23,9 @@ storage/logs/
# 设计文档(本地使用,不纳入版本控制)
_Design/
# 自用模板(不纳入版本控制)
templates/jay-pub/
# IDE
.idea/
.vscode/

View File

@ -11,6 +11,10 @@
<p align="center"><strong>MCE</strong> — 下一代开源社区引擎,为 BBS 3.0 而生</p>
> **⚠️ 维护状态**
> 因个人原因本项目暂时无法继续活跃维护。现有功能可正常使用Issues/PR 可能无法及时响应。
> 如有兴趣接手或继续开发,欢迎 Fork。
---
## 关于项目

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

@ -1,12 +1,14 @@
package config
import (
"log"
"strconv"
"sync"
"metazone.cc/mce/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// SiteSettings 站点设置管理器DB 持久化 + 内存缓存,不重启生效)
@ -69,15 +71,22 @@ func (s *SiteSettings) GetBool(key string, defaultVal bool) bool {
return b
}
// Set 写入设置(同步写 DB + 更新内存)
// Set 写入设置(同步写 DB + 更新内存,使用 UPSERT 兼容首次保存
func (s *SiteSettings) Set(key, value string) error {
entry := model.SiteSetting{Key: key, Value: value}
if err := s.db.Save(&entry).Error; err != nil {
return err
result := s.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}).Create(&entry)
if result.Error != nil {
log.Printf("[SiteSettings] Set FAIL key=%s value=%s err=%v", key, value, result.Error)
return result.Error
}
s.mu.Lock()
oldVal, existed := s.data[key]
s.data[key] = value
s.mu.Unlock()
log.Printf("[SiteSettings] Set OK key=%s old=%q new=%q existed=%v rows=%d", key, oldVal, value, existed, result.RowsAffected)
return nil
}

View File

@ -1,6 +1,7 @@
package admin
import (
"log"
"net/http"
"strconv"
@ -87,6 +88,10 @@ func (sc *SiteSettingController) ThemePage(c *gin.Context) {
func (sc *SiteSettingController) GetSettings(c *gin.Context) {
all := sc.settingService.GetSettings()
audit := sc.settingService.GetAuditSettings(sc.defaults)
log.Printf("[GetSettings] all=%d keys audit=%+v", len(all), audit)
if v, ok := all["registration.enabled"]; ok {
log.Printf("[GetSettings] registration.enabled=%q", v)
}
common.Ok(c, gin.H{
"all": all,
"audit": audit,

BIN
main Executable file

Binary file not shown.

View File

@ -3,7 +3,7 @@
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}" nonce="{{.CSPNonce}}"></script>
<script src="/admin/static/js/common.js?v={{assetV "/admin/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
{{if .ExtraJS}}
<script src="{{.ExtraJS}}" nonce="{{.CSPNonce}}"></script>
<script src="{{.ExtraJS}}?v={{assetV .ExtraJS}}" nonce="{{.CSPNonce}}"></script>
{{end}}
</body>
</html>

View File

@ -227,7 +227,7 @@
<span class="setting-desc">强制阅读时的倒计时秒数</span>
</div>
<div class="setting-input-wrap">
<input type="number" id="setting-guidelines-timer" class="setting-input" placeholder="60" min="1" style="width:100px" value="{{.GuidelinesTimer}}">
<input type="number" id="setting-guidelines-timer" class="setting-input setting-input-narrow" placeholder="60" min="1" value="{{.GuidelinesTimer}}">
<button class="btn btn-sm btn-primary btn-save" data-key="registration.guidelines_timer" data-input="setting-guidelines-timer">保存</button>
</div>
</div>

View File

@ -46,7 +46,7 @@
<span class="setting-desc">强制阅读时的倒计时秒数</span>
</div>
<div class="setting-input-wrap">
<input type="number" id="setting-guidelines-timer" class="setting-input" placeholder="60" min="1" style="width:100px" value="{{.GuidelinesTimer}}">
<input type="number" id="setting-guidelines-timer" class="setting-input setting-input-narrow" placeholder="60" min="1" value="{{.GuidelinesTimer}}">
<button class="btn btn-sm btn-primary btn-save" data-key="registration.guidelines_timer" data-input="setting-guidelines-timer">保存</button>
</div>
</div>

View File

@ -33,7 +33,7 @@
</div>
</div>
<div id="theme-status" class="settings-section" style="display:none">
<div id="theme-status" class="settings-section">
<div class="section-body">
<div class="setting-item">
<span id="theme-status-msg"></span>
@ -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

@ -141,6 +141,13 @@
font-size: 13px; color: var(--text-muted);
}
/* --- 隐藏工具类 --- */
#theme-status { display: none; }
#theme-status.visible { display: block; }
/* --- 窄输入框 --- */
.setting-input-narrow { width: 100px; }
/* --- 主题状态提示 --- */
.status-success .section-body {
background: #e8f5e9; border-left: 3px solid #4caf50;

View File

@ -95,8 +95,7 @@
}
function showStatus(msg, type) {
statusSection.style.display = 'block';
statusSection.className = 'settings-section';
statusSection.className = 'settings-section visible';
if (type === 'success') {
statusSection.classList.add('status-success');
} else if (type === 'error') {

View File

@ -36,7 +36,10 @@
async function loadSettings() {
try {
var res = await api.get('/api/admin/site-settings');
if (!res.success) return;
if (!res.success) {
showToast(res.message || '加载设置失败', 'error');
return;
}
// 所有开关
var audit = res.data.audit || {};
@ -47,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;
@ -60,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) {
@ -138,6 +147,7 @@
// --- 渲染其他设置过滤已有专属UI的key ---
function renderOtherSettings(all) {
var container = document.getElementById('general-settings');
if (!container) return; // 部分页面无此容器
var filtered = [];
Object.keys(all).forEach(function(k) {
if (!DEDICATED_KEYS[k]) filtered.push(k);