Files
jiang13-bbs/backend/handler/setting.go
freefire d784b0ea7a feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 05:27:33 +08:00

381 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"errors"
"net/http"
"github.com/freefire/jiang13-bbs/realtime"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
// GET /api/settings
func (h *Handlers) PublicSettings(c *gin.Context) {
s, err := h.Setting.Public()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
c.JSON(http.StatusOK, s)
}
// updateSettingsRequest 字段均为可选指针:只更新请求里出现的项,避免外观页覆盖其它设置
type updateSettingsRequest struct {
Accent *string `json:"accent"`
TrustReviewedPublish *bool `json:"trust_reviewed_publish"`
SiteName *string `json:"site_name"`
SiteDescription *string `json:"site_description"`
AllowRegister *bool `json:"allow_register"`
AllowComments *bool `json:"allow_comments"`
AllowMessages *bool `json:"allow_messages"`
PostCooldownHours *int `json:"post_cooldown_hours"`
CodeBlockAutoFold *bool `json:"code_block_auto_fold"`
CodeBlockFoldLines *int `json:"code_block_fold_lines"`
UIAnimations *bool `json:"ui_animations"`
AnimCodeFold *bool `json:"anim_code_fold"`
AnimSmoothScroll *bool `json:"anim_smooth_scroll"`
AnimChrome *bool `json:"anim_chrome"`
PostLinkNewTab *bool `json:"post_link_new_tab"`
AttachmentExtLimit *bool `json:"attachment_ext_limit"`
AttachmentExts *[]string `json:"attachment_exts"`
AttachmentMaxMB *int `json:"attachment_max_mb"`
AttachmentMaxCount *int `json:"attachment_max_count"`
ImageMaxMB *int `json:"image_max_mb"`
BgSiteURL *string `json:"bg_site_url"`
BgSiteMode *string `json:"bg_site_mode"`
BgAdminURL *string `json:"bg_admin_url"`
BgAdminMode *string `json:"bg_admin_mode"`
TimelineGitImport *string `json:"timeline_git_import"` // 超管专用;不进公开 settings / WS 广播
}
func settingsPayload(saved service.PublicSiteSettings) gin.H {
return gin.H{
"accent": saved.Accent,
"trust_reviewed_publish": saved.TrustReviewedPublish,
"site_name": saved.SiteName,
"site_description": saved.SiteDescription,
"allow_register": saved.AllowRegister,
"allow_comments": saved.AllowComments,
"allow_messages": saved.AllowMessages,
"post_cooldown_hours": saved.PostCooldownHours,
"code_block_auto_fold": saved.CodeBlockAutoFold,
"code_block_fold_lines": saved.CodeBlockFoldLines,
"ui_animations": saved.UIAnimations,
"anim_code_fold": saved.AnimCodeFold,
"anim_smooth_scroll": saved.AnimSmoothScroll,
"anim_chrome": saved.AnimChrome,
"post_link_new_tab": saved.PostLinkNewTab,
"attachment_ext_limit": saved.AttachmentExtLimit,
"attachment_exts": saved.AttachmentExts,
"attachment_max_mb": saved.AttachmentMaxMB,
"attachment_max_count": saved.AttachmentMaxCount,
"image_max_mb": saved.ImageMaxMB,
"bg_site_url": saved.BgSiteURL,
"bg_site_mode": saved.BgSiteMode,
"bg_admin_url": saved.BgAdminURL,
"bg_admin_mode": saved.BgAdminMode,
}
}
func (req *updateSettingsRequest) hasAny() bool {
return req.Accent != nil || req.TrustReviewedPublish != nil || req.SiteName != nil ||
req.SiteDescription != nil || req.AllowRegister != nil || req.AllowComments != nil ||
req.AllowMessages != nil || req.PostCooldownHours != nil || req.CodeBlockAutoFold != nil ||
req.CodeBlockFoldLines != nil || req.UIAnimations != nil || req.AnimCodeFold != nil ||
req.AnimSmoothScroll != nil || req.AnimChrome != nil || req.PostLinkNewTab != nil ||
req.AttachmentExtLimit != nil || req.AttachmentExts != nil || req.AttachmentMaxMB != nil ||
req.AttachmentMaxCount != nil || req.ImageMaxMB != nil ||
req.BgSiteURL != nil || req.BgSiteMode != nil ||
req.BgAdminURL != nil || req.BgAdminMode != nil || req.TimelineGitImport != nil
}
// AdminGetSettings 超管读取站点设置(含 timeline_git_import,不进公开 /api/settings)
func (h *Handlers) AdminGetSettings(c *gin.Context) {
saved, err := h.Setting.Public()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
adapter, err := h.Setting.TimelineGitAdapterJSON()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取提交导入配置失败"})
return
}
out := settingsPayload(saved)
out["timeline_git_import"] = adapter
c.JSON(http.StatusOK, out)
}
// PUT /api/admin/settings
func (h *Handlers) UpdateSettings(c *gin.Context) {
var req updateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
if !req.hasAny() {
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供任何可更新的设置项"})
return
}
if req.Accent != nil {
if err := h.Setting.SetAccent(*req.Accent); err != nil {
if errors.Is(err, service.ErrInvalidAccent) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.TrustReviewedPublish != nil {
if err := h.Setting.SetTrustReviewedPublish(*req.TrustReviewedPublish); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.SiteName != nil {
if err := h.Setting.SetSiteName(*req.SiteName); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "站点名称过长或无效"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.SiteDescription != nil {
if err := h.Setting.SetSiteDescription(*req.SiteDescription); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "站点简介过长"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AllowRegister != nil {
if err := h.Setting.SetAllowRegister(*req.AllowRegister); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AllowComments != nil {
if err := h.Setting.SetAllowComments(*req.AllowComments); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AllowMessages != nil {
if err := h.Setting.SetAllowMessages(*req.AllowMessages); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.PostCooldownHours != nil {
if err := h.Setting.SetPostCooldownHours(*req.PostCooldownHours); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "冷静期须为 0–168 小时"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.CodeBlockAutoFold != nil {
if err := h.Setting.SetCodeBlockAutoFold(*req.CodeBlockAutoFold); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.CodeBlockFoldLines != nil {
if err := h.Setting.SetCodeBlockFoldLines(*req.CodeBlockFoldLines); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "折叠行数须为 0–100"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.UIAnimations != nil {
if err := h.Setting.SetUIAnimations(*req.UIAnimations); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AnimCodeFold != nil {
if err := h.Setting.SetAnimCodeFold(*req.AnimCodeFold); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AnimSmoothScroll != nil {
if err := h.Setting.SetAnimSmoothScroll(*req.AnimSmoothScroll); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AnimChrome != nil {
if err := h.Setting.SetAnimChrome(*req.AnimChrome); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.PostLinkNewTab != nil {
if err := h.Setting.SetPostLinkNewTab(*req.PostLinkNewTab); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AttachmentExtLimit != nil {
if err := h.Setting.SetAttachmentExtLimit(*req.AttachmentExtLimit); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AttachmentExts != nil {
if err := h.Setting.SetAttachmentExts(*req.AttachmentExts); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "扩展名无效或数量超限(最多 80 个,仅字母数字)"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AttachmentMaxMB != nil {
if err := h.Setting.SetAttachmentMaxMB(*req.AttachmentMaxMB); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "附件上限须至少 1MB"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.AttachmentMaxCount != nil {
if err := h.Setting.SetAttachmentMaxCount(*req.AttachmentMaxCount); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "每帖附件数须为 1–20"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.ImageMaxMB != nil {
if err := h.Setting.SetImageMaxMB(*req.ImageMaxMB); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "插图上限须至少 1MB"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.BgSiteURL != nil {
if err := h.applyBgURL(service.BgSurfaceSite, *req.BgSiteURL); err != nil {
writeBgSettingError(c, err)
return
}
}
if req.BgAdminURL != nil {
if err := h.applyBgURL(service.BgSurfaceAdmin, *req.BgAdminURL); err != nil {
writeBgSettingError(c, err)
return
}
}
if req.BgSiteMode != nil {
if err := h.Setting.SetBgSiteMode(*req.BgSiteMode); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的背景铺放模式"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
if req.BgAdminMode != nil {
if err := h.Setting.SetBgAdminMode(*req.BgAdminMode); err != nil {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的背景铺放模式"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
var savedAdapter string
if req.TimelineGitImport != nil {
normalized, err := h.Setting.SetTimelineGitAdapterJSON(*req.TimelineGitImport)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
savedAdapter = normalized
}
saved, err := h.Setting.Public()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
payload := settingsPayload(saved)
h.Hub.BroadcastAll(realtime.Envelope{
Type: realtime.EventSettingsChanged,
Data: payload,
})
out := settingsPayload(saved)
out["ok"] = true
if req.TimelineGitImport != nil {
out["timeline_git_import"] = savedAdapter
}
c.JSON(http.StatusOK, out)
}
func writeBgSettingError(c *gin.Context, err error) {
if errors.Is(err, service.ErrInvalidSiteSetting) {
c.JSON(http.StatusBadRequest, gin.H{"error": "背景图地址无效"})
return
}
if errors.Is(err, service.ErrBgFileMissing) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
}
// applyBgURL 写入对应轨的背景 URL,并删除不再被引用的旧文件
func (h *Handlers) applyBgURL(surface, url string) error {
u, ok := service.NormalizeBgURL(url)
if !ok {
return service.ErrInvalidSiteSetting
}
if u != "" && !h.Upload.BackgroundFileExists(u) {
return service.ErrBgFileMissing
}
oldSite, err := h.Setting.BgSiteURL()
if err != nil {
return err
}
oldAdmin, err := h.Setting.BgAdminURL()
if err != nil {
return err
}
if surface == service.BgSurfaceAdmin {
if err := h.Setting.SetBgAdminURL(u); err != nil {
return err
}
h.Upload.RemoveBackgroundIfUnused(oldAdmin, oldSite, u)
return nil
}
if err := h.Setting.SetBgSiteURL(u); err != nil {
return err
}
h.Upload.RemoveBackgroundIfUnused(oldSite, u, oldAdmin)
return nil
}