feat: 发评限流与品牌 Logo/Favicon/OG 上传

发评挂 RateLimiter comment;设置页支持品牌图上传/清除,并写入 favicon 与 og:image。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 04:06:48 +08:00
parent 4ffcaecba2
commit 118f967540
13 changed files with 255 additions and 58 deletions

View File

@@ -520,6 +520,90 @@ func (d Deps) AdminSettingsBrandPost(c *gin.Context) {
ctx.Redirect("/admin/settings")
}
// AdminSettingsBrandUploadPost 上传 Logo / Favicon / OG 图
func (d Deps) AdminSettingsBrandUploadPost(c *gin.Context) {
ctx := d.ctx(c)
if !ctx.CheckCSRF() {
d.renderAdminSettings(ctx, "无效请求,请重试")
return
}
if d.Store == nil {
d.renderAdminSettings(ctx, "上传存储未就绪")
return
}
kind := strings.TrimSpace(c.PostForm("kind"))
if kind != "logo" && kind != "favicon" && kind != "og_image" {
d.renderAdminSettings(ctx, "请选择 Logo、Favicon 或 OG 图")
return
}
file, err := c.FormFile("file")
if err != nil {
d.renderAdminSettings(ctx, "请选择图片文件")
return
}
const maxBytes = 2 * 1024 * 1024
if file.Size > maxBytes {
d.renderAdminSettings(ctx, "图片不能超过 2MB")
return
}
url, err := services.SaveUploadedImage(d.Store, file, services.UploadCategorySite, kind)
if err != nil {
d.renderAdminSettings(ctx, err.Error())
return
}
prev := d.Settings.SiteBranding()
switch kind {
case "logo":
_ = d.Settings.SetSiteLogo(url)
d.Store.DeleteByURL(prev.Logo)
ctx.SetFlash("Logo 已上传")
case "favicon":
_ = d.Settings.SetSiteFavicon(url)
d.Store.DeleteByURL(prev.Favicon)
ctx.SetFlash("Favicon 已上传")
case "og_image":
_ = d.Settings.SetSiteOGImage(url)
d.Store.DeleteByURL(prev.OGImage)
ctx.SetFlash("默认 OG 图已上传")
}
ctx.Redirect("/admin/settings")
}
// AdminSettingsBrandClearPost 清除品牌图片
func (d Deps) AdminSettingsBrandClearPost(c *gin.Context) {
ctx := d.ctx(c)
if !ctx.CheckCSRF() {
d.renderAdminSettings(ctx, "无效请求,请重试")
return
}
kind := strings.TrimSpace(c.PostForm("kind"))
brand := d.Settings.SiteBranding()
switch kind {
case "logo":
_ = d.Settings.SetSiteLogo("")
if d.Store != nil {
d.Store.DeleteByURL(brand.Logo)
}
ctx.SetFlash("已清除 Logo")
case "favicon":
_ = d.Settings.SetSiteFavicon("")
if d.Store != nil {
d.Store.DeleteByURL(brand.Favicon)
}
ctx.SetFlash("已清除 Favicon")
case "og_image":
_ = d.Settings.SetSiteOGImage("")
if d.Store != nil {
d.Store.DeleteByURL(brand.OGImage)
}
ctx.SetFlash("已清除 OG 图")
default:
d.renderAdminSettings(ctx, "无效的资源类型")
return
}
ctx.Redirect("/admin/settings")
}
// AdminSettingsLimitsPost 限流
func (d Deps) AdminSettingsLimitsPost(c *gin.Context) {
ctx := d.ctx(c)

View File

@@ -108,6 +108,9 @@ type PageChrome struct {
SiteName string
Slogan string
LogoMark string
LogoURL string
FaviconURL string
OGImageURL string
LoggedIn bool
IsAdmin bool
ViewerName string
@@ -170,6 +173,9 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
SiteName: brand.Name,
Slogan: brand.Slogan,
LogoMark: firstRuneOr(brand.LogoMark, "姜"),
LogoURL: strings.TrimSpace(brand.Logo),
FaviconURL: strings.TrimSpace(brand.Favicon),
OGImageURL: strings.TrimSpace(brand.DefaultShareImage()),
LoggedIn: ctx.IsSigned(),
IsAdmin: ctx.IsAdmin(),
ViewerName: name,

View File

@@ -75,6 +75,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
admin.POST("/comments/:id/reject", deps.AdminCommentReject)
admin.GET("/settings", deps.AdminSettingsGet)
admin.POST("/settings/brand", deps.AdminSettingsBrandPost)
admin.POST("/settings/brand/upload", deps.AdminSettingsBrandUploadPost)
admin.POST("/settings/brand/clear", deps.AdminSettingsBrandClearPost)
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
admin.POST("/settings/mail", deps.AdminSettingsMailPost)

View File

@@ -391,6 +391,11 @@ func (d Deps) PostComment(c *gin.Context) {
}
}
isPrivate := c.PostForm("is_private") == "1" || c.PostForm("is_private") == "on"
if d.Limiter != nil && !d.Limiter.Allow("comment", fmt.Sprintf("%d", ctx.UserID())) {
ctx.SetFlash("操作过于频繁,请稍后再试")
ctx.Redirect(fmt.Sprintf("/post/%d#comments", id))
return
}
safe := "<p>" + html.EscapeString(content) + "</p>"
cm, err := d.Comment.Create(services.CommentCreateInput{
PostID: id, UserID: ctx.UserID(), Content: safe,