feat: 就地通过评审,并为已过审用户提供默认可关的免审发布

帖/评管理入口可直接通过;站点默认信任已有 published 内容的用户后续直发。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 05:37:57 +08:00
parent b0170a99fd
commit f3145b24ff
14 changed files with 330 additions and 40 deletions

View File

@@ -71,11 +71,8 @@ func (h *Handlers) CreateComment(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 管理团队成员评论免审;普通用户评论进入待审核队列,审核通过时才发业务通知
status := model.ContentStatusPending
if model.IsStaff(model.Role(claims.Role)) {
status = model.ContentStatusPublished
}
// staff 直发;已过审用户在站点开启免审时可直发;其余进待审队列(通过时才发业务通知)
status := h.resolvePublishStatus(claims.ID, claims.Role)
comment, parent, err := h.Comment.Create(claims.ID, uint(id), req.Content, req.ParentID, status)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

View File

@@ -30,6 +30,23 @@ type Handlers struct {
Chat *service.ChatService
}
// resolvePublishStatus 决定新帖/新评的初始状态:
// staff 直发;若开启「已过审免审」且用户已有 published 内容则直发;否则进 pending。
func (h *Handlers) resolvePublishStatus(userID uint, role string) string {
if model.IsStaff(model.Role(role)) {
return model.ContentStatusPublished
}
trust, err := h.Setting.TrustReviewedPublish()
if err != nil || !trust {
return model.ContentStatusPending
}
ok, err := h.Moderation.UserHasApprovedContent(userID)
if err != nil || !ok {
return model.ContentStatusPending
}
return model.ContentStatusPublished
}
// broadcastFeedChanged 帖子流有公开发布内容时全员广播(三期)
func (h *Handlers) broadcastFeedChanged(kind string, postID, boardID, actorID uint) {
if h.Hub == nil {

View File

@@ -110,10 +110,7 @@ func (h *Handlers) CreatePost(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
status := model.ContentStatusPending
if model.IsStaff(model.Role(claims.Role)) {
status = model.ContentStatusPublished
}
status := h.resolvePublishStatus(claims.ID, claims.Role)
post, err := h.Post.Create(service.CreatePostInput{
UserID: claims.ID,
BoardID: req.BoardID,

View File

@@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
)
// PublicSettings 公开站点设置(无需登录):前端 SSR 据此注入主题 CSS 变量
// PublicSettings 公开站点设置(无需登录):前端 SSR 据此注入主题 CSS 变量等
// GET /api/settings
func (h *Handlers) PublicSettings(c *gin.Context) {
accent, err := h.Setting.AccentColor()
@@ -18,12 +18,23 @@ func (h *Handlers) PublicSettings(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"accent": accent})
trust, err := h.Setting.TrustReviewedPublish()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"accent": accent,
"trust_reviewed_publish": trust,
})
}
// updateSettingsRequest 字段均为可选指针:只更新请求里出现的项,避免外观页覆盖审核策略
type updateSettingsRequest struct {
// Accent 主题色 #RRGGBB;空串恢复默认
Accent string `json:"accent"`
Accent *string `json:"accent"`
// TrustReviewedPublish 已过审用户后续内容免审
TrustReviewedPublish *bool `json:"trust_reviewed_publish"`
}
// UpdateSettings 更新站点设置(管理员)
@@ -34,19 +45,58 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
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": "保存站点设置失败"})
if req.Accent == nil && req.TrustReviewedPublish == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供任何可更新的设置项"})
return
}
// 归一化后的值实时广播:所有在线客户端立即热换肤,无需 F5 或等轮询
saved := strings.ToLower(strings.TrimSpace(req.Accent))
savedAccent := ""
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
}
savedAccent = strings.ToLower(strings.TrimSpace(*req.Accent))
} else {
var err error
savedAccent, err = h.Setting.AccentColor()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
}
trustOn := true
if req.TrustReviewedPublish != nil {
if err := h.Setting.SetTrustReviewedPublish(*req.TrustReviewedPublish); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
trustOn = *req.TrustReviewedPublish
} else {
var err error
trustOn, err = h.Setting.TrustReviewedPublish()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
return
}
}
// 归一化后广播:主题色客户端热换肤;免审开关由下次发帖/发评读库生效
h.Hub.BroadcastAll(realtime.Envelope{
Type: realtime.EventSettingsChanged,
Data: map[string]string{"accent": saved},
Data: gin.H{
"accent": savedAccent,
"trust_reviewed_publish": trustOn,
},
})
c.JSON(http.StatusOK, gin.H{
"ok": true,
"accent": savedAccent,
"trust_reviewed_publish": trustOn,
})
c.JSON(http.StatusOK, gin.H{"ok": true, "accent": saved})
}