feat(admin): 站点信息 + 注册开关可后台设置,模板动态渲染

新增 4 项可配置站点设置(DB持久化+内存缓存,即时生效):
- site.brand          品牌名(页面标题/页脚/管理面板,默认 MetaLab)
- site.framework      框架名(页脚标识,默认 MetaZone)
- site.copyright_start 版权起始年(页脚,默认 2024)
- registration.enabled 注册开关(关闭后禁止新用户注册,默认 true)

实现方式:
- InjectSiteInfo 中间件:全局注入 Brand/Framework/CopyrightStart 到 gin.Context
- BuildPageData 读取 context,所有 SSR 页面自动获取站点信息
- AuthService.Register 最先检查 IsRegistrationEnabled()
- 管理后台新增「基本设置」+「注册设置」区块,文本输入手动保存 + Toggle 即时生效
This commit is contained in:
2026-05-27 15:12:28 +08:00
parent 7bc2dbd970
commit 0b948cf70f
16 changed files with 216 additions and 28 deletions

View File

@ -34,4 +34,5 @@ var (
ErrLoginBeforeDelete = errors.New("请先登录再注销")
ErrNeedsConfirmRestore = errors.New("需要二次确认恢复账号")
ErrOwnerCannotDelete = errors.New("站长不可自主注销,请先转让站长权限")
ErrRegistrationDisabled = errors.New("注册功能已关闭")
)

View File

@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
)
// BuildPageData 构建页面模板数据,自动注入登录状态与 CSRF token
// BuildPageData 构建页面模板数据,自动注入登录状态、站点信息与 CSRF token
func BuildPageData(c *gin.Context, extra gin.H) gin.H {
data := gin.H{}
for k, v := range extra {
@ -20,6 +20,8 @@ func BuildPageData(c *gin.Context, extra gin.H) gin.H {
if token, exists := c.Get("csrf_token"); exists {
data["CSRFToken"] = token
}
// 注入站点信息(由 InjectSiteInfo 中间件设置)
injectSiteInfo(c, data)
return data
}
@ -48,3 +50,16 @@ func BuildAdminPageData(c *gin.Context, extra gin.H) gin.H {
}
return data
}
// injectSiteInfo 从 context 注入站点展示信息
func injectSiteInfo(c *gin.Context, data gin.H) {
if brand, exists := c.Get("site_brand"); exists {
data["Brand"] = brand
}
if framework, exists := c.Get("site_framework"); exists {
data["Framework"] = framework
}
if copyrightStart, exists := c.Get("site_copyright_start"); exists {
data["CopyrightStart"] = copyrightStart
}
}

View File

@ -107,3 +107,26 @@ func (s *SiteSettings) IsAuditEnabled() bool {
func (s *SiteSettings) IsAuditTypeEnabled(auditType string, defaultVal bool) bool {
return s.GetBool("audit."+auditType, defaultVal)
}
// --- 站点信息设置 ---
// SiteInfoDefaults 站点展示信息的默认值
type SiteInfoDefaults struct {
Brand string // 品牌名,默认 "MetaLab"
Framework string // 框架名,默认 "MetaZone"
CopyrightStart string // 版权起始年份,默认 "2024"
}
// SiteInfo 获取站点展示信息(优先 DB 设置fallback 默认值)
func (s *SiteSettings) SiteInfo() SiteInfoDefaults {
return SiteInfoDefaults{
Brand: s.Get("site.brand", "MetaLab"),
Framework: s.Get("site.framework", "MetaZone"),
CopyrightStart: s.Get("site.copyright_start", "2024"),
}
}
// IsRegistrationEnabled 是否允许新用户注册
func (s *SiteSettings) IsRegistrationEnabled() bool {
return s.GetBool("registration.enabled", true)
}

View File

@ -51,6 +51,8 @@ func (ac *AuthController) Register(c *gin.Context) {
common.Error(c, http.StatusConflict, "该邮箱已注册")
case common.ErrWeakPassword:
common.Error(c, http.StatusBadRequest, err.Error())
case common.ErrRegistrationDisabled:
common.Error(c, http.StatusForbidden, "注册功能已关闭")
default:
common.Error(c, http.StatusInternalServerError, "注册失败,请稍后重试")
}

View File

@ -23,7 +23,7 @@ func NewAuthController(authService authUseCase, tokenSvc tokenRefresher, limiter
return &AuthController{authService: authService, tokenService: tokenSvc, rateLimiter: limiter, cfg: cfg}
}
// RegisterPage 注册页面(已登录用户重定向到首页)
// RegisterPage 注册页面(已登录用户重定向到首页,注册关闭时重定向到登录页
func (ac *AuthController) RegisterPage(c *gin.Context) {
if _, exists := c.Get("uid"); exists {
c.Redirect(http.StatusFound, "/")

View File

@ -0,0 +1,19 @@
package middleware
import (
"metazone.cc/metalab/internal/config"
"github.com/gin-gonic/gin"
)
// InjectSiteInfo 注入站点展示信息到请求上下文(供 BuildPageData 使用)
// 应用于引擎级全局中间件,确保所有 SSR 页面都能读取 Brand/Framework/CopyrightStart
func InjectSiteInfo(siteSettings *config.SiteSettings) gin.HandlerFunc {
return func(c *gin.Context) {
info := siteSettings.SiteInfo()
c.Set("site_brand", info.Brand)
c.Set("site_framework", info.Framework)
c.Set("site_copyright_start", info.CopyrightStart)
c.Next()
}
}

View File

@ -22,14 +22,14 @@ type dependencies struct {
tokenCtrl *controller.AuthController
}
func buildDepsCore(db *gorm.DB, cfg *config.Config) (
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) (
*repository.UserRepo, *service.TokenService, *service.AuthService,
*middleware.RateLimiter, *controller.AuthController,
*service.AvatarService, *middleware.AuthMiddleware, *adminCtrl.AdminController,
) {
userRepo := repository.NewUserRepo(db)
tokenSvc := service.NewTokenService(cfg, userRepo)
authService := service.NewAuthService(userRepo, tokenSvc, cfg)
authService := service.NewAuthService(userRepo, tokenSvc, cfg, siteSettings)
rateLimiter := middleware.NewRateLimiter()
authCtrl := controller.NewAuthController(authService, tokenSvc, rateLimiter, cfg)
avatarSvc := service.NewAvatarService(userRepo)

View File

@ -11,7 +11,7 @@ import (
)
func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) *dependencies {
userRepo, _, authService, _, authCtrl, avatarSvc, authMdw, adminController := buildDepsCore(db, cfg)
userRepo, _, authService, _, authCtrl, avatarSvc, authMdw, adminController := buildDepsCore(db, cfg, siteSettings)
auditRepo := repository.NewAuditRepo(db)
notificationRepo := repository.NewNotificationRepo(db)

View File

@ -11,6 +11,7 @@ import (
// Setup 注册所有路由
func Setup(r *gin.Engine, db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings) {
r.Use(middleware.SecurityHeaders())
r.Use(middleware.InjectSiteInfo(siteSettings))
deps := buildDeps(db, cfg, siteSettings)

View File

@ -18,11 +18,12 @@ type AuthService struct {
userRepo userAuthStore
tokenService tokenProvider
cfg *config.Config
siteSettings *config.SiteSettings
}
// NewAuthService 构造函数
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, cfg *config.Config) *AuthService {
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg}
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, cfg *config.Config, siteSettings *config.SiteSettings) *AuthService {
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg, siteSettings: siteSettings}
}
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
@ -56,6 +57,11 @@ func validatePassword(pw string) error {
// rememberMe=true: refresh Cookie 持久化30天false: session cookie关浏览器即清除
// regIP: 注册 IP 地址
func (s *AuthService) Register(req model.RegisterRequest, regIP string) (string, string, *model.User, error) {
// 0. 检查注册开关
if !s.siteSettings.IsRegistrationEnabled() {
return "", "", nil, common.ErrRegistrationDisabled
}
// 1. 密码强度
if err := validatePassword(req.Password); err != nil {
return "", "", nil, err