Compare commits
6 Commits
ecbae7c4fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b997528bad | |||
| 426bedda65 | |||
| d4a833f149 | |||
| 3f4d827926 | |||
| 1eb3cc7c86 | |||
| 20067f7640 |
@ -11,6 +11,10 @@
|
|||||||
|
|
||||||
<p align="center"><strong>MCE</strong> — 下一代开源社区引擎,为 BBS 3.0 而生</p>
|
<p align="center"><strong>MCE</strong> — 下一代开源社区引擎,为 BBS 3.0 而生</p>
|
||||||
|
|
||||||
|
> **⚠️ 维护状态**
|
||||||
|
> 因个人原因,本项目暂时无法继续活跃维护。现有功能可正常使用,Issues/PR 可能无法及时响应。
|
||||||
|
> 如有兴趣接手或继续开发,欢迎 Fork。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 关于项目
|
## 关于项目
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"metazone.cc/mce/internal/model"
|
"metazone.cc/mce/internal/model"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SiteSettings 站点设置管理器(DB 持久化 + 内存缓存,不重启生效)
|
// SiteSettings 站点设置管理器(DB 持久化 + 内存缓存,不重启生效)
|
||||||
@ -69,15 +71,22 @@ func (s *SiteSettings) GetBool(key string, defaultVal bool) bool {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set 写入设置(同步写 DB + 更新内存)
|
// Set 写入设置(同步写 DB + 更新内存,使用 UPSERT 兼容首次保存)
|
||||||
func (s *SiteSettings) Set(key, value string) error {
|
func (s *SiteSettings) Set(key, value string) error {
|
||||||
entry := model.SiteSetting{Key: key, Value: value}
|
entry := model.SiteSetting{Key: key, Value: value}
|
||||||
if err := s.db.Save(&entry).Error; err != nil {
|
result := s.db.Clauses(clause.OnConflict{
|
||||||
return err
|
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()
|
s.mu.Lock()
|
||||||
|
oldVal, existed := s.data[key]
|
||||||
s.data[key] = value
|
s.data[key] = value
|
||||||
s.mu.Unlock()
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -87,6 +88,10 @@ func (sc *SiteSettingController) ThemePage(c *gin.Context) {
|
|||||||
func (sc *SiteSettingController) GetSettings(c *gin.Context) {
|
func (sc *SiteSettingController) GetSettings(c *gin.Context) {
|
||||||
all := sc.settingService.GetSettings()
|
all := sc.settingService.GetSettings()
|
||||||
audit := sc.settingService.GetAuditSettings(sc.defaults)
|
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{
|
common.Ok(c, gin.H{
|
||||||
"all": all,
|
"all": all,
|
||||||
"audit": audit,
|
"audit": audit,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}" nonce="{{.CSPNonce}}"></script>
|
<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>
|
<script src="/admin/static/js/common.js?v={{assetV "/admin/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||||||
{{if .ExtraJS}}
|
{{if .ExtraJS}}
|
||||||
<script src="{{.ExtraJS}}" nonce="{{.CSPNonce}}"></script>
|
<script src="{{.ExtraJS}}?v={{assetV .ExtraJS}}" nonce="{{.CSPNonce}}"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -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>
|
|
||||||
|
|||||||
@ -36,7 +36,10 @@
|
|||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
var res = await api.get('/api/admin/site-settings');
|
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 || {};
|
var audit = res.data.audit || {};
|
||||||
@ -47,7 +50,7 @@
|
|||||||
|
|
||||||
if (t.id === 'maintenance-enabled') {
|
if (t.id === 'maintenance-enabled') {
|
||||||
el.checked = allSettings[t.key] === 'true';
|
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';
|
el.checked = allSettings[t.key] !== 'false';
|
||||||
} else {
|
} else {
|
||||||
el.checked = audit[t.key.split('.')[1]] === true;
|
el.checked = audit[t.key.split('.')[1]] === true;
|
||||||
@ -60,6 +63,12 @@
|
|||||||
// 文本输入保存按钮
|
// 文本输入保存按钮
|
||||||
bindSaveButtons();
|
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');
|
var guidelinesEl = document.getElementById('setting-site-guidelines');
|
||||||
if (guidelinesEl) {
|
if (guidelinesEl) {
|
||||||
@ -138,6 +147,7 @@
|
|||||||
// --- 渲染其他设置(过滤已有专属UI的key) ---
|
// --- 渲染其他设置(过滤已有专属UI的key) ---
|
||||||
function renderOtherSettings(all) {
|
function renderOtherSettings(all) {
|
||||||
var container = document.getElementById('general-settings');
|
var container = document.getElementById('general-settings');
|
||||||
|
if (!container) return; // 部分页面无此容器
|
||||||
var filtered = [];
|
var filtered = [];
|
||||||
Object.keys(all).forEach(function(k) {
|
Object.keys(all).forEach(function(k) {
|
||||||
if (!DEDICATED_KEYS[k]) filtered.push(k);
|
if (!DEDICATED_KEYS[k]) filtered.push(k);
|
||||||
|
|||||||
Reference in New Issue
Block a user