feat: 就地通过评审,并为已过审用户提供默认可关的免审发布
帖/评管理入口可直接通过;站点默认信任已有 published 内容的用户后续直发。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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()})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -280,6 +280,26 @@ func (s *ModerationService) RejectComment(actor *Actor, id uint) error {
|
||||
})
|
||||
}
|
||||
|
||||
// UserHasApprovedContent 用户是否已有至少一篇已发布帖子或一条已发布评论(即曾通过审核或免审直发)
|
||||
func (s *ModerationService) UserHasApprovedContent(userID uint) (bool, error) {
|
||||
var n int64
|
||||
if err := s.db.Model(&model.Post{}).
|
||||
Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished).
|
||||
Limit(1).Count(&n).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if n > 0 {
|
||||
return true, nil
|
||||
}
|
||||
n = 0
|
||||
if err := s.db.Model(&model.Comment{}).
|
||||
Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished).
|
||||
Limit(1).Count(&n).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
func keys(m map[uint]bool) []uint {
|
||||
out := make([]uint, 0, len(m))
|
||||
for k := range m {
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
const (
|
||||
// SettingKeyAccent 站点主题色(浅色基准,#rrggbb);为空表示使用内置默认色
|
||||
SettingKeyAccent = "theme_accent"
|
||||
// SettingKeyTrustReviewedPublish 已过审用户后续发帖/评论免审;缺行视为开启(默认开)
|
||||
SettingKeyTrustReviewedPublish = "trust_reviewed_publish"
|
||||
)
|
||||
|
||||
// ErrInvalidAccent 主题色格式非法
|
||||
@@ -55,3 +57,33 @@ func (s *SettingService) SetAccent(hex string) error {
|
||||
st := model.SiteSetting{Key: SettingKeyAccent, Value: hex}
|
||||
return s.db.Save(&st).Error
|
||||
}
|
||||
|
||||
// TrustReviewedPublish 是否对「已有过审内容」的用户免审。
|
||||
// KV 缺行 / 空值视为开启(默认开);仅显式写入 false/0/off/no 为关闭。
|
||||
// 不用 GORM bool default:true,避免零值省略陷阱。
|
||||
func (s *SettingService) TrustReviewedPublish() (bool, error) {
|
||||
var st model.SiteSetting
|
||||
err := s.db.Where("key = ?", SettingKeyTrustReviewedPublish).First(&st).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
v := strings.TrimSpace(strings.ToLower(st.Value))
|
||||
switch v {
|
||||
case "false", "0", "off", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetTrustReviewedPublish 持久化免审开关;开启时删除键以保持「缺行=默认开」语义
|
||||
func (s *SettingService) SetTrustReviewedPublish(on bool) error {
|
||||
if on {
|
||||
return s.db.Where("key = ?", SettingKeyTrustReviewedPublish).Delete(&model.SiteSetting{}).Error
|
||||
}
|
||||
st := model.SiteSetting{Key: SettingKeyTrustReviewedPublish, Value: "false"}
|
||||
return s.db.Save(&st).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user