feat(attach): 附件可配置限额、流式落盘与分阶段上传

管理端可配类型/体积;上传改流式避免整文件进内存;发帖先选再传并拦截未上传提交;提高 Next proxy 体积上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 07:33:16 +08:00
parent 6cc434f103
commit 61bf9fb48c
24 changed files with 2513 additions and 307 deletions

View File

@@ -21,21 +21,26 @@ func (h *Handlers) PublicSettings(c *gin.Context) {
// 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"`
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"`
}
func settingsPayload(saved service.PublicSiteSettings) gin.H {
@@ -55,9 +60,24 @@ func settingsPayload(saved service.PublicSiteSettings) gin.H {
"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,
}
}
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
}
// PUT /api/admin/settings
func (h *Handlers) UpdateSettings(c *gin.Context) {
var req updateSettingsRequest
@@ -65,12 +85,7 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
if 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 {
if !req.hasAny() {
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供任何可更新的设置项"})
return
}
@@ -185,6 +200,52 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
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
}
}
saved, err := h.Setting.Public()
if err != nil {