feat(admin): 完善站点设置、内容回收站与后台菜单分组
新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,6 +96,13 @@ type LoginRequest struct {
|
||||
|
||||
// Register 用户注册:成功后直接签发登录态(注册即登录,免去手动再登一次)
|
||||
func (h *Handlers) Register(c *gin.Context) {
|
||||
if allow, err := h.Setting.AllowRegister(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册暂时不可用"})
|
||||
return
|
||||
} else if !allow {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "站点已关闭注册"})
|
||||
return
|
||||
}
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -87,16 +87,91 @@ func (h *Handlers) AdminRejectComment(c *gin.Context) {
|
||||
h.execModeration(c, h.Moderation.RejectComment)
|
||||
}
|
||||
|
||||
// writeModerationError 映射审核业务错误为 HTTP 响应
|
||||
// ===== 内容库(帖子/评论 + 已删除回收站) =====
|
||||
|
||||
func (h *Handlers) AdminContentCounts(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, h.Moderation.AdminContentCounts(middleware.CurrentActor(c)))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminListPosts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
items, total, err := h.Moderation.ListAdminPosts(
|
||||
middleware.CurrentActor(c), c.Query("status"), c.Query("q"), page,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取帖子列表失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"posts": nonNilSlice(items), "total": total, "page": page})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminListComments(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
items, total, err := h.Moderation.ListAdminComments(
|
||||
middleware.CurrentActor(c), c.Query("status"), c.Query("q"), page,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论列表失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"comments": nonNilSlice(items), "total": total, "page": page})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminSoftDeletePost(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.SoftDeletePost)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminRestorePost(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.RestorePost)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminPurgePost(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.PurgePost)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminSoftDeleteComment(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.SoftDeleteComment)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminRestoreComment(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.RestoreComment)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminPurgeComment(c *gin.Context) {
|
||||
h.execContentAction(c, h.Moderation.PurgeComment)
|
||||
}
|
||||
|
||||
func (h *Handlers) execContentAction(c *gin.Context, fn func(*service.Actor, uint) error) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
|
||||
return
|
||||
}
|
||||
if err := fn(middleware.CurrentActor(c), uint(id)); err != nil {
|
||||
h.writeModerationError(c, err)
|
||||
return
|
||||
}
|
||||
h.broadcastModerationChanged()
|
||||
c.JSON(http.StatusOK, gin.H{"message": "操作成功"})
|
||||
}
|
||||
|
||||
// writeModerationError 映射审核/内容库业务错误为 HTTP 响应
|
||||
func (h *Handlers) writeModerationError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
case errors.Is(err, gorm.ErrRecordNotFound), errors.Is(err, service.ErrContentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "内容不存在"})
|
||||
case errors.Is(err, service.ErrModerationForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, service.ErrNotPending):
|
||||
case errors.Is(err, service.ErrNotPending),
|
||||
errors.Is(err, service.ErrContentNotDeleted):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
default:
|
||||
msg := err.Error()
|
||||
if msg == "帖子已在回收站" || msg == "帖子未被删除" ||
|
||||
msg == "评论已在回收站" || msg == "评论未被删除" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,41 +3,32 @@ package handler
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/realtime"
|
||||
"github.com/freefire/jiang13-bbs/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PublicSettings 公开站点设置(无需登录):前端 SSR 据此注入主题 CSS 变量等
|
||||
// GET /api/settings
|
||||
func (h *Handlers) PublicSettings(c *gin.Context) {
|
||||
accent, err := h.Setting.AccentColor()
|
||||
s, err := h.Setting.Public()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
|
||||
return
|
||||
}
|
||||
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,
|
||||
})
|
||||
c.JSON(http.StatusOK, s)
|
||||
}
|
||||
|
||||
// updateSettingsRequest 字段均为可选指针:只更新请求里出现的项,避免外观页覆盖审核策略
|
||||
// updateSettingsRequest 字段均为可选指针:只更新请求里出现的项,避免外观页覆盖其它设置
|
||||
type updateSettingsRequest struct {
|
||||
// Accent 主题色 #RRGGBB;空串恢复默认
|
||||
Accent *string `json:"accent"`
|
||||
// TrustReviewedPublish 已过审用户后续内容免审
|
||||
TrustReviewedPublish *bool `json:"trust_reviewed_publish"`
|
||||
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"`
|
||||
PostCooldownHours *int `json:"post_cooldown_hours"`
|
||||
}
|
||||
|
||||
// UpdateSettings 更新站点设置(管理员)
|
||||
// PUT /api/admin/settings
|
||||
func (h *Handlers) UpdateSettings(c *gin.Context) {
|
||||
var req updateSettingsRequest
|
||||
@@ -45,12 +36,12 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
|
||||
return
|
||||
}
|
||||
if req.Accent == nil && req.TrustReviewedPublish == nil {
|
||||
if req.Accent == nil && req.TrustReviewedPublish == nil && req.SiteName == nil &&
|
||||
req.SiteDescription == nil && req.AllowRegister == nil && req.PostCooldownHours == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供任何可更新的设置项"})
|
||||
return
|
||||
}
|
||||
|
||||
savedAccent := ""
|
||||
if req.Accent != nil {
|
||||
if err := h.Setting.SetAccent(*req.Accent); err != nil {
|
||||
if errors.Is(err, service.ErrInvalidAccent) {
|
||||
@@ -60,43 +51,74 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
|
||||
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": "获取站点设置失败"})
|
||||
}
|
||||
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.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
|
||||
}
|
||||
}
|
||||
|
||||
// 归一化后广播:主题色客户端热换肤;免审开关由下次发帖/发评读库生效
|
||||
saved, err := h.Setting.Public()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取站点设置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
h.Hub.BroadcastAll(realtime.Envelope{
|
||||
Type: realtime.EventSettingsChanged,
|
||||
Data: gin.H{
|
||||
"accent": savedAccent,
|
||||
"trust_reviewed_publish": trustOn,
|
||||
"accent": saved.Accent,
|
||||
"trust_reviewed_publish": saved.TrustReviewedPublish,
|
||||
"site_name": saved.SiteName,
|
||||
"site_description": saved.SiteDescription,
|
||||
"allow_register": saved.AllowRegister,
|
||||
"post_cooldown_hours": saved.PostCooldownHours,
|
||||
},
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true,
|
||||
"accent": savedAccent,
|
||||
"trust_reviewed_publish": trustOn,
|
||||
"accent": saved.Accent,
|
||||
"trust_reviewed_publish": saved.TrustReviewedPublish,
|
||||
"site_name": saved.SiteName,
|
||||
"site_description": saved.SiteDescription,
|
||||
"allow_register": saved.AllowRegister,
|
||||
"post_cooldown_hours": saved.PostCooldownHours,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
// 服务
|
||||
authSvc := service.NewAuthService(model.DB, cfg.JWTSecret)
|
||||
boardSvc := service.NewBoardService(model.DB)
|
||||
postSvc := service.NewPostService(model.DB)
|
||||
settingSvc := service.NewSettingService(model.DB)
|
||||
postSvc := service.NewPostService(model.DB).WithSetting(settingSvc)
|
||||
commentSvc := service.NewCommentService(model.DB)
|
||||
likeSvc := service.NewLikeService(model.DB)
|
||||
notifSvc := service.NewNotificationService(model.DB)
|
||||
@@ -51,7 +52,6 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
uploadSvc := service.NewUploadService(model.DB, filepath.Join(cfg.DataDir, "uploads"))
|
||||
postFileSvc := service.NewPostFileService(model.DB, filepath.Join(cfg.DataDir, "private"))
|
||||
pointsSvc := service.NewPointsService(model.DB)
|
||||
settingSvc := service.NewSettingService(model.DB)
|
||||
adminUserSvc := service.NewAdminUserService(model.DB)
|
||||
moderationSvc := service.NewModerationService(model.DB, notifSvc)
|
||||
chatSvc := service.NewChatService(model.DB, notifSvc)
|
||||
@@ -205,6 +205,17 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||||
staffAPI.PUT("/moderation/comments/:id/approve", h.AdminApproveComment)
|
||||
staffAPI.PUT("/moderation/comments/:id/reject", h.AdminRejectComment)
|
||||
|
||||
// 内容库:帖子/评论全量 + 回收站(恢复 / 彻底删除)
|
||||
staffAPI.GET("/content/counts", h.AdminContentCounts)
|
||||
staffAPI.GET("/content/posts", h.AdminListPosts)
|
||||
staffAPI.GET("/content/comments", h.AdminListComments)
|
||||
staffAPI.DELETE("/content/posts/:id", h.AdminSoftDeletePost)
|
||||
staffAPI.PUT("/content/posts/:id/restore", h.AdminRestorePost)
|
||||
staffAPI.DELETE("/content/posts/:id/purge", h.AdminPurgePost)
|
||||
staffAPI.DELETE("/content/comments/:id", h.AdminSoftDeleteComment)
|
||||
staffAPI.PUT("/content/comments/:id/restore", h.AdminRestoreComment)
|
||||
staffAPI.DELETE("/content/comments/:id/purge", h.AdminPurgeComment)
|
||||
|
||||
// 积分与类型帖概览(管理员及以上)
|
||||
staffAPI.GET("/economy", authMW.RequirePerm(service.PermAnnouncements), h.AdminPointsStats)
|
||||
|
||||
|
||||
395
backend/service/admin_content.go
Normal file
395
backend/service/admin_content.go
Normal file
@@ -0,0 +1,395 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 内容库管理错误
|
||||
var (
|
||||
ErrContentNotDeleted = errors.New("仅已删除内容可彻底清除")
|
||||
ErrContentNotFound = errors.New("内容不存在")
|
||||
)
|
||||
|
||||
const adminContentPageSize = 15
|
||||
|
||||
// AdminContentStatus 列表筛选:live=未删除全部状态;其余为具体状态或已删除
|
||||
type AdminContentStatus string
|
||||
|
||||
const (
|
||||
AdminContentLive AdminContentStatus = "live"
|
||||
AdminContentPending AdminContentStatus = "pending"
|
||||
AdminContentRejected AdminContentStatus = "rejected"
|
||||
AdminContentPublished AdminContentStatus = "published"
|
||||
AdminContentDeleted AdminContentStatus = "deleted"
|
||||
)
|
||||
|
||||
// AdminContentCounts 当前操作者可见范围内的数量(用于页内 Tab)
|
||||
type AdminContentCounts struct {
|
||||
PostsLive int64 `json:"posts_live"`
|
||||
PostsPending int64 `json:"posts_pending"`
|
||||
PostsRejected int64 `json:"posts_rejected"`
|
||||
PostsDeleted int64 `json:"posts_deleted"`
|
||||
CommentsLive int64 `json:"comments_live"`
|
||||
CommentsPending int64 `json:"comments_pending"`
|
||||
CommentsRejected int64 `json:"comments_rejected"`
|
||||
CommentsDeleted int64 `json:"comments_deleted"`
|
||||
}
|
||||
|
||||
// AdminContentPost 后台帖子列表项
|
||||
type AdminContentPost struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Deleted bool `json:"deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
BoardID uint `json:"board_id"`
|
||||
Board model.Board `json:"board"`
|
||||
User model.User `json:"user"`
|
||||
}
|
||||
|
||||
// AdminContentComment 后台评论列表项
|
||||
type AdminContentComment struct {
|
||||
ID uint `json:"id"`
|
||||
PostID uint `json:"post_id"`
|
||||
PostTitle string `json:"post_title"`
|
||||
BoardID uint `json:"board_id"`
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"`
|
||||
Deleted bool `json:"deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Board model.Board `json:"board"`
|
||||
User model.User `json:"user"`
|
||||
}
|
||||
|
||||
func normalizeAdminStatus(raw string) AdminContentStatus {
|
||||
switch AdminContentStatus(raw) {
|
||||
case AdminContentPending, AdminContentRejected, AdminContentPublished, AdminContentDeleted:
|
||||
return AdminContentStatus(raw)
|
||||
default:
|
||||
return AdminContentLive
|
||||
}
|
||||
}
|
||||
|
||||
func applyPostStatusFilter(q *gorm.DB, st AdminContentStatus) *gorm.DB {
|
||||
switch st {
|
||||
case AdminContentDeleted:
|
||||
return q.Where("deleted_at IS NOT NULL")
|
||||
case AdminContentPending, AdminContentRejected, AdminContentPublished:
|
||||
return q.Where("deleted_at IS NULL AND status = ?", string(st))
|
||||
default:
|
||||
return q.Where("deleted_at IS NULL")
|
||||
}
|
||||
}
|
||||
|
||||
func applyCommentStatusFilter(q *gorm.DB, st AdminContentStatus) *gorm.DB {
|
||||
switch st {
|
||||
case AdminContentDeleted:
|
||||
return q.Where("comments.deleted_at IS NOT NULL")
|
||||
case AdminContentPending, AdminContentRejected, AdminContentPublished:
|
||||
return q.Where("comments.deleted_at IS NULL AND comments.status = ?", string(st))
|
||||
default:
|
||||
return q.Where("comments.deleted_at IS NULL")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ModerationService) scopedPosts(actor *Actor) *gorm.DB {
|
||||
q := s.db.Unscoped().Model(&model.Post{})
|
||||
if actor != nil && actor.Role == model.RoleBoardAdmin {
|
||||
if len(actor.BoardIDs) == 0 {
|
||||
return q.Where("1 = 0")
|
||||
}
|
||||
q = q.Where("board_id IN ?", actor.BoardIDs)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (s *ModerationService) scopedComments(actor *Actor) *gorm.DB {
|
||||
q := s.db.Unscoped().Table("comments").
|
||||
Joins("JOIN posts ON posts.id = comments.post_id")
|
||||
if actor != nil && actor.Role == model.RoleBoardAdmin {
|
||||
if len(actor.BoardIDs) == 0 {
|
||||
return q.Where("1 = 0")
|
||||
}
|
||||
q = q.Where("posts.board_id IN ?", actor.BoardIDs)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// AdminContentCounts 各筛选桶数量(每次独立起查询,避免 Where 叠加)
|
||||
func (s *ModerationService) AdminContentCounts(actor *Actor) AdminContentCounts {
|
||||
var c AdminContentCounts
|
||||
s.scopedPosts(actor).Where("deleted_at IS NULL").Count(&c.PostsLive)
|
||||
s.scopedPosts(actor).Where("deleted_at IS NULL AND status = ?", model.ContentStatusPending).Count(&c.PostsPending)
|
||||
s.scopedPosts(actor).Where("deleted_at IS NULL AND status = ?", model.ContentStatusRejected).Count(&c.PostsRejected)
|
||||
s.scopedPosts(actor).Where("deleted_at IS NOT NULL").Count(&c.PostsDeleted)
|
||||
|
||||
s.scopedComments(actor).Where("comments.deleted_at IS NULL").Count(&c.CommentsLive)
|
||||
s.scopedComments(actor).Where("comments.deleted_at IS NULL AND comments.status = ?", model.ContentStatusPending).Count(&c.CommentsPending)
|
||||
s.scopedComments(actor).Where("comments.deleted_at IS NULL AND comments.status = ?", model.ContentStatusRejected).Count(&c.CommentsRejected)
|
||||
s.scopedComments(actor).Where("comments.deleted_at IS NOT NULL").Count(&c.CommentsDeleted)
|
||||
return c
|
||||
}
|
||||
|
||||
// ListAdminPosts 后台帖子库(含已删除)
|
||||
func (s *ModerationService) ListAdminPosts(actor *Actor, status, keyword string, page int) ([]AdminContentPost, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
st := normalizeAdminStatus(status)
|
||||
q := applyPostStatusFilter(s.scopedPosts(actor), st)
|
||||
if kw := strings.TrimSpace(keyword); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
q = q.Where("title ILIKE ?", like)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var posts []model.Post
|
||||
if err := q.Order("id DESC").
|
||||
Offset((page - 1) * adminContentPageSize).Limit(adminContentPageSize).
|
||||
Preload("Board").Preload("User").
|
||||
Find(&posts).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]AdminContentPost, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
it := AdminContentPost{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
Status: p.Status,
|
||||
CreatedAt: p.CreatedAt,
|
||||
CommentCount: p.CommentCount,
|
||||
BoardID: p.BoardID,
|
||||
Board: p.Board,
|
||||
User: p.User,
|
||||
}
|
||||
if p.DeletedAt.Valid {
|
||||
it.Deleted = true
|
||||
t := p.DeletedAt.Time
|
||||
it.DeletedAt = &t
|
||||
}
|
||||
items = append(items, it)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// ListAdminComments 后台评论库(含已删除;所属帖即使已删仍可见标题)
|
||||
func (s *ModerationService) ListAdminComments(actor *Actor, status, keyword string, page int) ([]AdminContentComment, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
st := normalizeAdminStatus(status)
|
||||
q := applyCommentStatusFilter(s.scopedComments(actor), st)
|
||||
if kw := strings.TrimSpace(keyword); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
q = q.Where("comments.content ILIKE ?", like)
|
||||
}
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
type row struct {
|
||||
ID uint
|
||||
PostID uint
|
||||
Content string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
BoardID uint
|
||||
PostTitle string
|
||||
UserID uint
|
||||
}
|
||||
var rows []row
|
||||
if err := q.Select("comments.id, comments.post_id, comments.content, comments.status, comments.created_at, comments.deleted_at, posts.board_id, posts.title AS post_title, comments.user_id").
|
||||
Order("comments.id DESC").
|
||||
Offset((page - 1) * adminContentPageSize).Limit(adminContentPageSize).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
boardIDs := make([]uint, 0)
|
||||
userIDs := make([]uint, 0)
|
||||
seenB := map[uint]struct{}{}
|
||||
seenU := map[uint]struct{}{}
|
||||
for _, r := range rows {
|
||||
if _, ok := seenB[r.BoardID]; !ok {
|
||||
seenB[r.BoardID] = struct{}{}
|
||||
boardIDs = append(boardIDs, r.BoardID)
|
||||
}
|
||||
if _, ok := seenU[r.UserID]; !ok {
|
||||
seenU[r.UserID] = struct{}{}
|
||||
userIDs = append(userIDs, r.UserID)
|
||||
}
|
||||
}
|
||||
boards := map[uint]model.Board{}
|
||||
if len(boardIDs) > 0 {
|
||||
var list []model.Board
|
||||
s.db.Where("id IN ?", boardIDs).Find(&list)
|
||||
for _, b := range list {
|
||||
boards[b.ID] = b
|
||||
}
|
||||
}
|
||||
users := map[uint]model.User{}
|
||||
if len(userIDs) > 0 {
|
||||
var list []model.User
|
||||
s.db.Where("id IN ?", userIDs).Find(&list)
|
||||
for _, u := range list {
|
||||
users[u.ID] = u
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]AdminContentComment, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
it := AdminContentComment{
|
||||
ID: r.ID,
|
||||
PostID: r.PostID,
|
||||
PostTitle: r.PostTitle,
|
||||
BoardID: r.BoardID,
|
||||
Content: r.Content,
|
||||
Status: r.Status,
|
||||
CreatedAt: r.CreatedAt,
|
||||
Board: boards[r.BoardID],
|
||||
User: users[r.UserID],
|
||||
}
|
||||
if r.DeletedAt.Valid {
|
||||
it.Deleted = true
|
||||
t := r.DeletedAt.Time
|
||||
it.DeletedAt = &t
|
||||
}
|
||||
items = append(items, it)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *ModerationService) loadPostUnscoped(actor *Actor, id uint) (*model.Post, error) {
|
||||
var post model.Post
|
||||
if err := s.db.Unscoped().First(&post, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrContentNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !actor.CanModerateBoard(post.BoardID) {
|
||||
return nil, ErrModerationForbidden
|
||||
}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (s *ModerationService) loadCommentUnscoped(actor *Actor, id uint) (*model.Comment, uint, error) {
|
||||
var cm model.Comment
|
||||
if err := s.db.Unscoped().First(&cm, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, 0, ErrContentNotFound
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
var post model.Post
|
||||
if err := s.db.Unscoped().Select("id", "board_id").First(&post, cm.PostID).Error; err != nil {
|
||||
return nil, 0, ErrContentNotFound
|
||||
}
|
||||
if !actor.CanModerateBoard(post.BoardID) {
|
||||
return nil, 0, ErrModerationForbidden
|
||||
}
|
||||
return &cm, post.BoardID, nil
|
||||
}
|
||||
|
||||
// SoftDeletePost 软删帖子(未删除时)
|
||||
func (s *ModerationService) SoftDeletePost(actor *Actor, id uint) error {
|
||||
post, err := s.loadPostUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if post.DeletedAt.Valid {
|
||||
return errors.New("帖子已在回收站")
|
||||
}
|
||||
return s.db.Delete(post).Error
|
||||
}
|
||||
|
||||
// RestorePost 从回收站恢复帖子
|
||||
func (s *ModerationService) RestorePost(actor *Actor, id uint) error {
|
||||
post, err := s.loadPostUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !post.DeletedAt.Valid {
|
||||
return errors.New("帖子未被删除")
|
||||
}
|
||||
return s.db.Unscoped().Model(&model.Post{}).Where("id = ?", id).Update("deleted_at", nil).Error
|
||||
}
|
||||
|
||||
// PurgePost 彻底删除已软删帖子及其评论
|
||||
func (s *ModerationService) PurgePost(actor *Actor, id uint) error {
|
||||
post, err := s.loadPostUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !post.DeletedAt.Valid {
|
||||
return ErrContentNotDeleted
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Where("post_id = ?", id).Delete(&model.Comment{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Delete(&model.Post{}, id).Error
|
||||
})
|
||||
}
|
||||
|
||||
// SoftDeleteComment 软删单条评论(不级联子树,后台点对点清理)
|
||||
func (s *ModerationService) SoftDeleteComment(actor *Actor, id uint) error {
|
||||
cm, _, err := s.loadCommentUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cm.DeletedAt.Valid {
|
||||
return errors.New("评论已在回收站")
|
||||
}
|
||||
if err := s.db.Delete(cm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cm.Status == model.ContentStatusPublished {
|
||||
s.db.Model(&model.Post{}).Where("id = ?", cm.PostID).
|
||||
UpdateColumn("comment_count", gorm.Expr("GREATEST(comment_count - 1, 0)"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreComment 恢复单条评论
|
||||
func (s *ModerationService) RestoreComment(actor *Actor, id uint) error {
|
||||
cm, _, err := s.loadCommentUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cm.DeletedAt.Valid {
|
||||
return errors.New("评论未被删除")
|
||||
}
|
||||
if err := s.db.Unscoped().Model(&model.Comment{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cm.Status == model.ContentStatusPublished {
|
||||
s.db.Model(&model.Post{}).Where("id = ?", cm.PostID).
|
||||
UpdateColumn("comment_count", gorm.Expr("comment_count + 1"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PurgeComment 彻底删除已软删评论
|
||||
func (s *ModerationService) PurgeComment(actor *Actor, id uint) error {
|
||||
cm, _, err := s.loadCommentUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cm.DeletedAt.Valid {
|
||||
return ErrContentNotDeleted
|
||||
}
|
||||
return s.db.Unscoped().Delete(&model.Comment{}, id).Error
|
||||
}
|
||||
@@ -59,12 +59,17 @@ type AdminUserItem struct {
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
}
|
||||
|
||||
// AdminUserSummary 列表顶部汇总(总数/管理团队/封禁/今日新增)
|
||||
// AdminUserSummary 列表顶部汇总
|
||||
// - online / in_cooldown / active_7d / failed_logins_24h:用户页独有运营指标
|
||||
// - total / admins / banned:筛选 Tab 角标(不在顶部卡片展示,避免与仪表盘重复)
|
||||
type AdminUserSummary struct {
|
||||
Total int64 `json:"total"`
|
||||
Admins int64 `json:"admins"`
|
||||
Banned int64 `json:"banned"`
|
||||
TodayNew int64 `json:"today_new"`
|
||||
Online int64 `json:"online"`
|
||||
InCooldown int64 `json:"in_cooldown"`
|
||||
Active7d int64 `json:"active_7d"`
|
||||
FailedLogins24h int64 `json:"failed_logins_24h"`
|
||||
Total int64 `json:"total"`
|
||||
Admins int64 `json:"admins"`
|
||||
Banned int64 `json:"banned"`
|
||||
}
|
||||
|
||||
// staffRoles 管理团队角色集合
|
||||
@@ -131,14 +136,31 @@ func (s *AdminUserService) List(q AdminUserListQuery) (*AdminUserListResult, err
|
||||
|
||||
items := s.toItems(users)
|
||||
|
||||
// 汇总数据(不受筛选条件影响,始终反映全站)
|
||||
// 汇总:用户页独有指标(避免与仪表盘站点总量重复)
|
||||
now := time.Now()
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
onlineSince := now.Add(-onlineThreshold)
|
||||
hours, _ := (&SettingService{db: s.db}).PostCooldownHours()
|
||||
activeSince := now.Add(-7 * 24 * time.Hour)
|
||||
failSince := now.Add(-24 * time.Hour)
|
||||
summary := AdminUserSummary{}
|
||||
s.db.Model(&model.User{}).
|
||||
Where("banned = ? AND last_seen_at IS NOT NULL AND last_seen_at >= ?", false, onlineSince).
|
||||
Count(&summary.Online)
|
||||
if hours > 0 {
|
||||
cooldownSince := now.Add(-time.Duration(hours) * time.Hour)
|
||||
s.db.Model(&model.User{}).
|
||||
Where("banned = ? AND role = ? AND created_at >= ?", false, model.RoleUser, cooldownSince).
|
||||
Count(&summary.InCooldown)
|
||||
}
|
||||
s.db.Model(&model.User{}).
|
||||
Where("last_seen_at IS NOT NULL AND last_seen_at >= ?", activeSince).
|
||||
Count(&summary.Active7d)
|
||||
s.db.Model(&model.LoginLog{}).
|
||||
Where("success = ? AND created_at >= ?", false, failSince).
|
||||
Count(&summary.FailedLogins24h)
|
||||
s.db.Model(&model.User{}).Count(&summary.Total)
|
||||
s.db.Model(&model.User{}).Where("role IN ?", staffRoles).Count(&summary.Admins)
|
||||
s.db.Model(&model.User{}).Where("banned = ?", true).Count(&summary.Banned)
|
||||
s.db.Model(&model.User{}).Where("created_at >= ?", dayStart).Count(&summary.TodayNew)
|
||||
|
||||
return &AdminUserListResult{
|
||||
Users: items,
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,13 +18,20 @@ var (
|
||||
|
||||
// PostService 帖子服务
|
||||
type PostService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
setting *SettingService
|
||||
}
|
||||
|
||||
func NewPostService(db *gorm.DB) *PostService {
|
||||
return &PostService{db: db}
|
||||
}
|
||||
|
||||
// WithSetting 注入站点设置(发帖冷静期可读配置);可链式调用
|
||||
func (s *PostService) WithSetting(setting *SettingService) *PostService {
|
||||
s.setting = setting
|
||||
return s
|
||||
}
|
||||
|
||||
// PostListQuery 帖子列表查询参数
|
||||
type PostListQuery struct {
|
||||
BoardID uint
|
||||
@@ -925,14 +933,23 @@ func (s *PostService) EnsurePostVisible(postID, viewerID uint, loadActor func()
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkNewUserCooldown 新用户发帖 24h 冷静期
|
||||
// checkNewUserCooldown 新用户发帖冷静期(小时数取自站点设置,0=关闭)
|
||||
func (s *PostService) checkNewUserCooldown(userID uint) error {
|
||||
hours := DefaultCooldownHours
|
||||
if s.setting != nil {
|
||||
if h, err := s.setting.PostCooldownHours(); err == nil {
|
||||
hours = h
|
||||
}
|
||||
}
|
||||
if hours <= 0 {
|
||||
return nil
|
||||
}
|
||||
var user model.User
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Since(user.CreatedAt) < 24*time.Hour {
|
||||
return errors.New("新用户注册 24 小时后才能发帖")
|
||||
if time.Since(user.CreatedAt) < time.Duration(hours)*time.Hour {
|
||||
return fmt.Errorf("新用户注册 %d 小时后才能发帖", hours)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"gorm.io/gorm"
|
||||
@@ -15,14 +17,40 @@ const (
|
||||
SettingKeyAccent = "theme_accent"
|
||||
// SettingKeyTrustReviewedPublish 已过审用户后续发帖/评论免审;缺行视为开启(默认开)
|
||||
SettingKeyTrustReviewedPublish = "trust_reviewed_publish"
|
||||
SettingKeySiteName = "site_name"
|
||||
SettingKeySiteDescription = "site_description"
|
||||
SettingKeyAllowRegister = "allow_register"
|
||||
SettingKeyPostCooldownHours = "post_cooldown_hours"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSiteName = "姜十三论坛"
|
||||
DefaultSiteDescription = "姜十三论坛 - 技术分享与讨论社区"
|
||||
DefaultCooldownHours = 24
|
||||
MaxSiteNameRunes = 32
|
||||
MaxSiteDescRunes = 160
|
||||
MaxCooldownHours = 168
|
||||
)
|
||||
|
||||
// ErrInvalidAccent 主题色格式非法
|
||||
var ErrInvalidAccent = errors.New("主题色格式不正确,应为 #RRGGBB")
|
||||
|
||||
// ErrInvalidSiteSetting 站点设置字段校验失败
|
||||
var ErrInvalidSiteSetting = errors.New("站点设置参数无效")
|
||||
|
||||
var accentHexRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
||||
|
||||
// SettingService 站点级键值设置(主题色等外观配置)
|
||||
// PublicSiteSettings 对前台公开的站点配置(SEO / 注册入口 / 发帖冷静期提示)
|
||||
type PublicSiteSettings 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"`
|
||||
PostCooldownHours int `json:"post_cooldown_hours"`
|
||||
}
|
||||
|
||||
// SettingService 站点级键值设置
|
||||
type SettingService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
@@ -31,59 +59,195 @@ func NewSettingService(db *gorm.DB) *SettingService {
|
||||
return &SettingService{db: db}
|
||||
}
|
||||
|
||||
// AccentColor 返回配置的主题色(小写 #rrggbb);未配置时返回空串,表示走前端默认色
|
||||
func (s *SettingService) AccentColor() (string, error) {
|
||||
func (s *SettingService) getValue(key string) (string, bool, error) {
|
||||
var st model.SiteSetting
|
||||
err := s.db.Where("key = ?", SettingKeyAccent).First(&st).Error
|
||||
err := s.db.Where("key = ?", key).First(&st).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return st.Value, true, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) putValue(key, value string) error {
|
||||
st := model.SiteSetting{Key: key, Value: value}
|
||||
return s.db.Save(&st).Error
|
||||
}
|
||||
|
||||
func (s *SettingService) deleteKey(key string) error {
|
||||
return s.db.Where("key = ?", key).Delete(&model.SiteSetting{}).Error
|
||||
}
|
||||
|
||||
// 缺行或空值视为 on;仅显式 false/0/off/no 为关。不用 GORM bool default:true,避免零值省略。
|
||||
func parseBoolDefaultTrue(v string, found bool) bool {
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
switch strings.TrimSpace(strings.ToLower(v)) {
|
||||
case "false", "0", "off", "no":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingService) setBoolDefaultTrue(key string, on bool) error {
|
||||
if on {
|
||||
return s.deleteKey(key)
|
||||
}
|
||||
return s.putValue(key, "false")
|
||||
}
|
||||
|
||||
// Public 返回解析后的公开站点设置(缺省已填默认值)
|
||||
func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
out := PublicSiteSettings{
|
||||
SiteName: DefaultSiteName,
|
||||
SiteDescription: DefaultSiteDescription,
|
||||
TrustReviewedPublish: true,
|
||||
AllowRegister: true,
|
||||
PostCooldownHours: DefaultCooldownHours,
|
||||
}
|
||||
accent, err := s.AccentColor()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Accent = accent
|
||||
|
||||
trust, err := s.TrustReviewedPublish()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.TrustReviewedPublish = trust
|
||||
|
||||
name, found, err := s.getValue(SettingKeySiteName)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if found {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
out.SiteName = name
|
||||
}
|
||||
}
|
||||
|
||||
desc, found, err := s.getValue(SettingKeySiteDescription)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if found {
|
||||
out.SiteDescription = strings.TrimSpace(desc)
|
||||
}
|
||||
|
||||
reg, found, err := s.getValue(SettingKeyAllowRegister)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.AllowRegister = parseBoolDefaultTrue(reg, found)
|
||||
|
||||
hours, err := s.PostCooldownHours()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.PostCooldownHours = hours
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AccentColor 返回配置的主题色(小写 #rrggbb);未配置时返回空串,表示走前端默认色
|
||||
func (s *SettingService) AccentColor() (string, error) {
|
||||
v, found, err := s.getValue(SettingKeyAccent)
|
||||
if err != nil || !found {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToLower(st.Value), nil
|
||||
return strings.ToLower(v), nil
|
||||
}
|
||||
|
||||
// SetAccent 校验并保存主题色;hex 传空串表示恢复默认(删除该键)
|
||||
func (s *SettingService) SetAccent(hex string) error {
|
||||
hex = strings.TrimSpace(strings.ToLower(hex))
|
||||
if hex == "" {
|
||||
return s.db.Where("key = ?", SettingKeyAccent).Delete(&model.SiteSetting{}).Error
|
||||
return s.deleteKey(SettingKeyAccent)
|
||||
}
|
||||
if !accentHexRe.MatchString(hex) {
|
||||
return ErrInvalidAccent
|
||||
}
|
||||
// UPSERT:键冲突时更新值与时间戳
|
||||
st := model.SiteSetting{Key: SettingKeyAccent, Value: hex}
|
||||
return s.db.Save(&st).Error
|
||||
return s.putValue(SettingKeyAccent, hex)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
v, found, err := s.getValue(SettingKeyTrustReviewedPublish)
|
||||
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
|
||||
}
|
||||
return parseBoolDefaultTrue(v, found), 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
|
||||
return s.setBoolDefaultTrue(SettingKeyTrustReviewedPublish, on)
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSiteName(name string) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == DefaultSiteName {
|
||||
return s.deleteKey(SettingKeySiteName)
|
||||
}
|
||||
if utf8.RuneCountInString(name) > MaxSiteNameRunes {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
return s.putValue(SettingKeySiteName, name)
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSiteDescription(desc string) error {
|
||||
desc = strings.TrimSpace(desc)
|
||||
if utf8.RuneCountInString(desc) > MaxSiteDescRunes {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if desc == "" || desc == DefaultSiteDescription {
|
||||
return s.deleteKey(SettingKeySiteDescription)
|
||||
}
|
||||
return s.putValue(SettingKeySiteDescription, desc)
|
||||
}
|
||||
|
||||
func (s *SettingService) AllowRegister() (bool, error) {
|
||||
v, found, err := s.getValue(SettingKeyAllowRegister)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return parseBoolDefaultTrue(v, found), nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAllowRegister(on bool) error {
|
||||
return s.setBoolDefaultTrue(SettingKeyAllowRegister, on)
|
||||
}
|
||||
|
||||
// PostCooldownHours 新用户发帖冷静期(小时)。缺行=24;0 表示关闭。
|
||||
func (s *SettingService) PostCooldownHours() (int, error) {
|
||||
v, found, err := s.getValue(SettingKeyPostCooldownHours)
|
||||
if err != nil {
|
||||
return DefaultCooldownHours, err
|
||||
}
|
||||
if !found {
|
||||
return DefaultCooldownHours, nil
|
||||
}
|
||||
n, convErr := strconv.Atoi(strings.TrimSpace(v))
|
||||
if convErr != nil || n < 0 {
|
||||
return DefaultCooldownHours, nil
|
||||
}
|
||||
if n > MaxCooldownHours {
|
||||
return MaxCooldownHours, nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetPostCooldownHours(hours int) error {
|
||||
if hours < 0 || hours > MaxCooldownHours {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if hours == DefaultCooldownHours {
|
||||
return s.deleteKey(SettingKeyPostCooldownHours)
|
||||
}
|
||||
return s.putValue(SettingKeyPostCooldownHours, strconv.Itoa(hours))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user