完善角色与消息权限:收紧板管内容处置,彻底删除仅站长,并统一管理删帖/评弹窗。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,15 +13,17 @@ const (
|
||||
PermAnnouncements = "announcements" // 公告管理(管理员及以上)
|
||||
PermSettings = "settings" // 站点外观设置(超管/站长)
|
||||
PermModeration = "moderation" // 内容审核(任意管理角色,板块范围受限)
|
||||
PermMessages = "messages" // 后台消息管理(站长/超管/站点消息 flag;群管另有业务层放行)
|
||||
)
|
||||
|
||||
// Actor 当前请求操作者的实时权限快照(每次后台请求从 DB 现取,
|
||||
// 不依赖 JWT 内的 role claim,角色/授权变更立即生效)
|
||||
type Actor struct {
|
||||
ID uint
|
||||
Username string
|
||||
Role model.Role
|
||||
BoardIDs []uint // 板块管理员被授权的板块;其他角色为空
|
||||
ID uint
|
||||
Username string
|
||||
Role model.Role
|
||||
BoardIDs []uint // 板块管理员被授权的板块;其他角色为空
|
||||
CanManageMessages bool // 站点级消息管理 flag(站长授予);站长/超管不必依赖此字段
|
||||
}
|
||||
|
||||
// IsStaff 是否管理团队成员
|
||||
@@ -41,10 +43,28 @@ func (a *Actor) HasPerm(p string) bool {
|
||||
return model.RoleLevel(a.Role) >= model.RoleLevel(model.RoleAdmin)
|
||||
case PermModeration:
|
||||
return a.IsStaff()
|
||||
case PermMessages:
|
||||
return a.HasSiteMessagePerm()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasSiteMessagePerm 站点级消息管理:站长/超管固有,或被授予 CanManageMessages
|
||||
func (a *Actor) HasSiteMessagePerm() bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
if a.Role == model.RoleOwner || a.Role == model.RoleSuperAdmin {
|
||||
return true
|
||||
}
|
||||
return a.CanManageMessages
|
||||
}
|
||||
|
||||
// CanPurgeContent 彻底删除帖/评:仅站长
|
||||
func (a *Actor) CanPurgeContent() bool {
|
||||
return a != nil && a.Role == model.RoleOwner
|
||||
}
|
||||
|
||||
// CanModerateBoard 是否可管理指定板块的内容:
|
||||
// 管理员及以上不限板块;板块管理员仅限被授权板块
|
||||
func (a *Actor) CanModerateBoard(boardID uint) bool {
|
||||
@@ -65,6 +85,17 @@ func (a *Actor) CanModerateBoard(boardID uint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CanModerateAuthor 板管不可软删/恢复管理员及以上作者的内容;其他角色不受此限
|
||||
func (a *Actor) CanModerateAuthor(authorRole model.Role) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
if a.Role == model.RoleBoardAdmin {
|
||||
return model.RoleLevel(authorRole) < model.RoleLevel(model.RoleAdmin)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CanAssignRole 操作者能否把他人设置为目标角色(站长不可被授予,仅站长可授予超管)
|
||||
func (a *Actor) CanAssignRole(target model.Role) bool {
|
||||
if a == nil {
|
||||
@@ -82,10 +113,13 @@ func (a *Actor) CanAssignRole(target model.Role) bool {
|
||||
// LoadActor 读取用户实时角色与板块授权
|
||||
func (s *AuthService) LoadActor(id uint) (*Actor, error) {
|
||||
var u model.User
|
||||
if err := s.db.Select("id", "username", "role").First(&u, id).Error; err != nil {
|
||||
if err := s.db.Select("id", "username", "role", "can_manage_messages").First(&u, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actor := &Actor{ID: u.ID, Username: u.Username, Role: u.Role, BoardIDs: []uint{}}
|
||||
actor := &Actor{
|
||||
ID: u.ID, Username: u.Username, Role: u.Role, BoardIDs: []uint{},
|
||||
CanManageMessages: u.CanManageMessages,
|
||||
}
|
||||
if u.Role == model.RoleBoardAdmin {
|
||||
var ids []uint
|
||||
if err := s.db.Model(&model.UserBoard{}).
|
||||
@@ -99,6 +133,15 @@ func (s *AuthService) LoadActor(id uint) (*Actor, error) {
|
||||
return actor, nil
|
||||
}
|
||||
|
||||
// GetUserRole 读取用户角色(内容作者保护用);用户不存在返回 user
|
||||
func (s *AuthService) GetUserRole(id uint) (model.Role, error) {
|
||||
var u model.User
|
||||
if err := s.db.Select("role").First(&u, id).Error; err != nil {
|
||||
return model.RoleUser, err
|
||||
}
|
||||
return u.Role, nil
|
||||
}
|
||||
|
||||
// GetUserBoardIDs 读取用户被授权的板块 ID(供 /me 等场景)
|
||||
func (s *AuthService) GetUserBoardIDs(id uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
|
||||
@@ -15,6 +15,25 @@ var (
|
||||
ErrContentNotFound = errors.New("内容不存在")
|
||||
)
|
||||
|
||||
func (s *ModerationService) authorRole(userID uint) (model.Role, error) {
|
||||
var u model.User
|
||||
if err := s.db.Select("role").First(&u, userID).Error; err != nil {
|
||||
return model.RoleUser, err
|
||||
}
|
||||
return u.Role, nil
|
||||
}
|
||||
|
||||
func (s *ModerationService) assertCanModerateAuthor(actor *Actor, authorID uint) error {
|
||||
role, err := s.authorRole(authorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !actor.CanModerateAuthor(role) {
|
||||
return ErrAuthorProtected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const adminContentPageSize = 15
|
||||
|
||||
// AdminContentStatus 列表筛选:live=未删除全部状态;其余为具体状态或已删除
|
||||
@@ -303,12 +322,15 @@ func (s *ModerationService) loadCommentUnscoped(actor *Actor, id uint) (*model.C
|
||||
return &cm, post.BoardID, nil
|
||||
}
|
||||
|
||||
// SoftDeletePost 软删帖子(未删除时);须填写删除类型与理由,并通知作者
|
||||
// SoftDeletePost 软删帖子(未删除时);须选类型,「其他」才须填理由,并通知作者
|
||||
func (s *ModerationService) SoftDeletePost(actor *Actor, id uint, deleteType, deleteReason string) (authorID uint, err error) {
|
||||
post, err := s.loadPostUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := s.assertCanModerateAuthor(actor, post.UserID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if post.DeletedAt.Valid {
|
||||
return 0, errors.New("帖子已在回收站")
|
||||
}
|
||||
@@ -317,11 +339,15 @@ func (s *ModerationService) SoftDeletePost(actor *Actor, id uint, deleteType, de
|
||||
if !model.ValidPostDeleteType(deleteType) {
|
||||
return 0, errors.New("无效的删除类型")
|
||||
}
|
||||
if deleteReason == "" {
|
||||
return 0, errors.New("请填写删除理由")
|
||||
}
|
||||
if len([]rune(deleteReason)) > 200 {
|
||||
return 0, errors.New("删除理由不能超过 200 字")
|
||||
if deleteType == model.PostDeleteTypeOther {
|
||||
if deleteReason == "" {
|
||||
return 0, errors.New("请填写删除理由")
|
||||
}
|
||||
if len([]rune(deleteReason)) > 200 {
|
||||
return 0, errors.New("删除理由不能超过 200 字")
|
||||
}
|
||||
} else {
|
||||
deleteReason = ""
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"delete_type": deleteType,
|
||||
@@ -343,14 +369,20 @@ func (s *ModerationService) RestorePost(actor *Actor, id uint) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.assertCanModerateAuthor(actor, post.UserID); 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 彻底删除已软删帖子及其评论
|
||||
// PurgePost 彻底删除已软删帖子及其评论(仅站长)
|
||||
func (s *ModerationService) PurgePost(actor *Actor, id uint) error {
|
||||
if !actor.CanPurgeContent() {
|
||||
return ErrPurgeOwnerOnly
|
||||
}
|
||||
post, err := s.loadPostUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -372,6 +404,9 @@ func (s *ModerationService) SoftDeleteComment(actor *Actor, id uint, deleteType,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.assertCanModerateAuthor(actor, cm.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
if cm.DeletedAt.Valid {
|
||||
return errors.New("评论已在回收站")
|
||||
}
|
||||
@@ -414,6 +449,9 @@ func (s *ModerationService) RestoreComment(actor *Actor, id uint) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.assertCanModerateAuthor(actor, cm.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !cm.DeletedAt.Valid {
|
||||
return errors.New("评论未被删除")
|
||||
}
|
||||
@@ -433,8 +471,11 @@ func (s *ModerationService) RestoreComment(actor *Actor, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PurgeComment 彻底删除已软删评论
|
||||
// PurgeComment 彻底删除已软删评论(仅站长)
|
||||
func (s *ModerationService) PurgeComment(actor *Actor, id uint) error {
|
||||
if !actor.CanPurgeContent() {
|
||||
return ErrPurgeOwnerOnly
|
||||
}
|
||||
cm, _, err := s.loadCommentUnscoped(actor, id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -44,23 +44,24 @@ func NewAdminUserService(db *gorm.DB) *AdminUserService {
|
||||
// AdminUserItem 后台用户列表项:email / last_seen / 登录 IP 在 User 模型上 json:"-",
|
||||
// 仅管理员接口通过此 DTO 显式带出
|
||||
type AdminUserItem struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Signature string `json:"signature"`
|
||||
Role string `json:"role"`
|
||||
BoardIDs []uint `json:"board_ids"`
|
||||
Banned bool `json:"banned"`
|
||||
PostCount int64 `json:"post_count"`
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
Points int `json:"points"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at"`
|
||||
Online bool `json:"online"`
|
||||
LastLoginIP string `json:"last_login_ip"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Signature string `json:"signature"`
|
||||
Role string `json:"role"`
|
||||
BoardIDs []uint `json:"board_ids"`
|
||||
Banned bool `json:"banned"`
|
||||
CanManageMessages bool `json:"can_manage_messages"`
|
||||
PostCount int64 `json:"post_count"`
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
Points int `json:"points"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at"`
|
||||
Online bool `json:"online"`
|
||||
LastLoginIP string `json:"last_login_ip"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
}
|
||||
|
||||
// AdminUserSummary 列表顶部汇总
|
||||
@@ -222,18 +223,19 @@ func (s *AdminUserService) toItems(users []model.User) []AdminUserItem {
|
||||
for _, u := range users {
|
||||
ids = append(ids, u.ID)
|
||||
item := AdminUserItem{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Nickname: u.Nickname,
|
||||
Email: u.Email,
|
||||
Avatar: u.Avatar,
|
||||
Signature: u.Signature,
|
||||
Role: string(u.Role),
|
||||
BoardIDs: []uint{},
|
||||
Banned: u.Banned,
|
||||
Points: u.Points,
|
||||
CreatedAt: u.CreatedAt,
|
||||
LastSeenAt: u.LastSeenAt,
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Nickname: u.Nickname,
|
||||
Email: u.Email,
|
||||
Avatar: u.Avatar,
|
||||
Signature: u.Signature,
|
||||
Role: string(u.Role),
|
||||
BoardIDs: []uint{},
|
||||
Banned: u.Banned,
|
||||
CanManageMessages: u.CanManageMessages,
|
||||
Points: u.Points,
|
||||
CreatedAt: u.CreatedAt,
|
||||
LastSeenAt: u.LastSeenAt,
|
||||
}
|
||||
if u.LastSeenAt != nil && now.Sub(*u.LastSeenAt) <= onlineThreshold {
|
||||
item.Online = true
|
||||
@@ -407,6 +409,38 @@ func (s *AdminUserService) SetStaff(operator *Actor, targetID uint, role model.R
|
||||
return s.getItem(s.db, targetID)
|
||||
}
|
||||
|
||||
// SetCanManageMessages 站长授予/撤销站点消息管理权限(不可操作站长账号与自己)
|
||||
func (s *AdminUserService) SetCanManageMessages(operator *Actor, targetID uint, enabled bool) (*AdminUserItem, error) {
|
||||
if operator == nil || operator.Role != model.RoleOwner {
|
||||
return nil, ErrOwnerOnly
|
||||
}
|
||||
if operator.ID == targetID {
|
||||
return nil, ErrAdminSelfAction
|
||||
}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var u model.User
|
||||
if err := tx.First(&u, targetID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Role == model.RoleOwner {
|
||||
return ErrProtectedOwner
|
||||
}
|
||||
// 超管固有全站监管,flag 无意义;强制保持 false 以免误导
|
||||
if u.Role == model.RoleSuperAdmin {
|
||||
enabled = false
|
||||
}
|
||||
if u.CanManageMessages == enabled {
|
||||
return nil
|
||||
}
|
||||
// GORM bool 零值省略:强制 Select 写入
|
||||
return tx.Model(&u).Select("can_manage_messages").Update("can_manage_messages", enabled).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.getItem(s.db, targetID)
|
||||
}
|
||||
|
||||
// SetBanned 封禁/解封用户。站长账号不可封禁;封禁时同事务使登录态立即失效
|
||||
func (s *AdminUserService) SetBanned(operator *Actor, targetID uint, banned bool) (*AdminUserItem, error) {
|
||||
if operator == nil {
|
||||
|
||||
@@ -81,8 +81,9 @@ func fillDaySeries(start, end time.Time, got map[string]int64) []DayCount {
|
||||
return out
|
||||
}
|
||||
|
||||
// Overview 返回近 N 日访问与内容趋势
|
||||
func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
// Overview 返回近 N 日访问与内容趋势。
|
||||
// scoped=true 时按 boardIDs 过滤内容类指标,并隐藏全站 PV/UV/注册。
|
||||
func (s *AnalyticsService) Overview(rangeQ string, boardIDs []uint, scoped bool) (*AnalyticsOverview, error) {
|
||||
days := parseRangeDays(rangeQ)
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
@@ -97,22 +98,38 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
TopViewed: make([]TopViewPost, 0),
|
||||
}
|
||||
|
||||
var stats []model.SiteDailyStats
|
||||
if err := s.db.Where("date >= ? AND date <= ?", start, end).
|
||||
Order("date ASC").Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
if scoped && len(boardIDs) == 0 {
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
out.Visits = append(out.Visits, VisitDay{Date: dateKey(d)})
|
||||
}
|
||||
out.PostsSeries = fillDaySeries(start, end, nil)
|
||||
out.CommentsSeries = fillDaySeries(start, end, nil)
|
||||
out.RegistersSeries = fillDaySeries(start, end, nil)
|
||||
return out, nil
|
||||
}
|
||||
visitMap := map[string]model.SiteDailyStats{}
|
||||
for _, st := range stats {
|
||||
visitMap[dateKey(st.Date)] = st
|
||||
}
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
k := dateKey(d)
|
||||
st := visitMap[k]
|
||||
out.Visits = append(out.Visits, VisitDay{Date: k, PV: st.PV, UV: st.UV})
|
||||
if k == dateKey(today) {
|
||||
out.TodayPV = st.PV
|
||||
out.TodayUV = st.UV
|
||||
|
||||
if !scoped {
|
||||
var stats []model.SiteDailyStats
|
||||
if err := s.db.Where("date >= ? AND date <= ?", start, end).
|
||||
Order("date ASC").Find(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitMap := map[string]model.SiteDailyStats{}
|
||||
for _, st := range stats {
|
||||
visitMap[dateKey(st.Date)] = st
|
||||
}
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
k := dateKey(d)
|
||||
st := visitMap[k]
|
||||
out.Visits = append(out.Visits, VisitDay{Date: k, PV: st.PV, UV: st.UV})
|
||||
if k == dateKey(today) {
|
||||
out.TodayPV = st.PV
|
||||
out.TodayUV = st.UV
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
out.Visits = append(out.Visits, VisitDay{Date: dateKey(d)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,38 +149,56 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
boardFilter := ""
|
||||
var postArgs []interface{}
|
||||
postArgs = append(postArgs, model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
if scoped {
|
||||
boardFilter = " AND board_id IN ?"
|
||||
postArgs = append(postArgs, boardIDs)
|
||||
}
|
||||
postMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM posts
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?`+boardFilter+`
|
||||
GROUP BY day ORDER BY day`,
|
||||
model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
postArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.PostsSeries = fillDaySeries(start, end, postMap)
|
||||
|
||||
var commentArgs []interface{}
|
||||
commentArgs = append(commentArgs, model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
commentBoard := ""
|
||||
if scoped {
|
||||
commentBoard = " AND post_id IN (SELECT id FROM posts WHERE board_id IN ?)"
|
||||
commentArgs = append(commentArgs, boardIDs)
|
||||
}
|
||||
commentMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM comments
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?
|
||||
WHERE deleted_at IS NULL AND status = ? AND created_at >= ? AND created_at < ?`+commentBoard+`
|
||||
GROUP BY day ORDER BY day`,
|
||||
model.ContentStatusPublished, start, end.AddDate(0, 0, 1))
|
||||
commentArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.CommentsSeries = fillDaySeries(start, end, commentMap)
|
||||
|
||||
regMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?
|
||||
GROUP BY day ORDER BY day`,
|
||||
start, end.AddDate(0, 0, 1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !scoped {
|
||||
regMap, err := scanDayCounts(`
|
||||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?
|
||||
GROUP BY day ORDER BY day`,
|
||||
start, end.AddDate(0, 0, 1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.RegistersSeries = fillDaySeries(start, end, regMap)
|
||||
} else {
|
||||
out.RegistersSeries = fillDaySeries(start, end, nil)
|
||||
}
|
||||
out.RegistersSeries = fillDaySeries(start, end, regMap)
|
||||
|
||||
type boardRow struct {
|
||||
ID uint
|
||||
@@ -171,13 +206,18 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
PostCount int64
|
||||
}
|
||||
var boards []boardRow
|
||||
if err := s.db.Raw(`
|
||||
boardSQL := `
|
||||
SELECT b.id, b.name, count(p.id) AS post_count
|
||||
FROM boards b
|
||||
LEFT JOIN posts p ON p.board_id = b.id AND p.deleted_at IS NULL AND p.status = ?
|
||||
WHERE b.deleted_at IS NULL
|
||||
GROUP BY b.id, b.name
|
||||
ORDER BY post_count DESC`, model.ContentStatusPublished).Scan(&boards).Error; err != nil {
|
||||
WHERE b.deleted_at IS NULL`
|
||||
boardArgs := []interface{}{model.ContentStatusPublished}
|
||||
if scoped {
|
||||
boardSQL += ` AND b.id IN ?`
|
||||
boardArgs = append(boardArgs, boardIDs)
|
||||
}
|
||||
boardSQL += ` GROUP BY b.id, b.name ORDER BY post_count DESC`
|
||||
if err := s.db.Raw(boardSQL, boardArgs...).Scan(&boards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range boards {
|
||||
@@ -192,11 +232,14 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
PostType string
|
||||
Cnt int64
|
||||
}
|
||||
var types []typeRow
|
||||
if err := s.db.Model(&model.Post{}).
|
||||
typeQ := s.db.Model(&model.Post{}).
|
||||
Select("post_type, count(*) as cnt").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished).
|
||||
Group("post_type").Scan(&types).Error; err != nil {
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished)
|
||||
if scoped {
|
||||
typeQ = typeQ.Where("board_id IN ?", boardIDs)
|
||||
}
|
||||
var types []typeRow
|
||||
if err := typeQ.Group("post_type").Scan(&types).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range types {
|
||||
@@ -208,10 +251,13 @@ func (s *AnalyticsService) Overview(rangeQ string) (*AnalyticsOverview, error) {
|
||||
})
|
||||
}
|
||||
|
||||
topQ := s.db.Select("id, title, view_count, board_id").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished)
|
||||
if scoped {
|
||||
topQ = topQ.Where("board_id IN ?", boardIDs)
|
||||
}
|
||||
var tops []model.Post
|
||||
if err := s.db.Select("id, title, view_count, board_id").
|
||||
Where("deleted_at IS NULL AND status = ?", model.ContentStatusPublished).
|
||||
Order("view_count DESC").Limit(8).Find(&tops).Error; err != nil {
|
||||
if err := topQ.Order("view_count DESC").Limit(8).Find(&tops).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range tops {
|
||||
|
||||
@@ -35,9 +35,14 @@ var (
|
||||
ErrChatDMDissolve = errors.New("私聊会话不可解散")
|
||||
ErrChatUserGone = errors.New("用户不存在或已封禁")
|
||||
ErrChatHallPinFixed = errors.New("大厅已置顶且不可取消")
|
||||
ErrChatSiteOwnerOnly = errors.New("仅站长可任命群管理员")
|
||||
ErrChatInvalidRole = errors.New("无效的群成员角色")
|
||||
ErrChatMuteDenied = errors.New("无权禁言该成员")
|
||||
ErrChatCannotMuteOwner = errors.New("不能禁言群主或站长")
|
||||
ErrChatAdminAccess = errors.New("无权管理消息")
|
||||
)
|
||||
|
||||
// CanOverseeChat 站长/超管可旁路查看全部群并撤回任意消息
|
||||
// CanOverseeChat 站长/超管可旁路查看全部群(含私聊)并撤回任意消息
|
||||
func CanOverseeChat(role model.Role) bool {
|
||||
return role == model.RoleOwner || role == model.RoleSuperAdmin
|
||||
}
|
||||
@@ -530,7 +535,7 @@ func (s *ChatService) Join(userID, roomID uint) (member *model.ChatRoomMember, j
|
||||
return &m, true, nil
|
||||
}
|
||||
|
||||
// InviteMembers 群主(或监管)拉人入群;返回实际新加入的用户 ID
|
||||
// InviteMembers 群主/群管(或监管)拉人入群;返回实际新加入的用户 ID
|
||||
func (s *ChatService) InviteMembers(operatorID, roomID uint, userIDs []uint, oversee bool) ([]uint, error) {
|
||||
room, err := s.getRoom(roomID)
|
||||
if err != nil {
|
||||
@@ -544,7 +549,7 @@ func (s *ChatService) InviteMembers(operatorID, roomID uint, userIDs []uint, ove
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if op.Role != model.ChatRoleOwner {
|
||||
if op.Role != model.ChatRoleOwner && op.Role != model.ChatRoleAdmin {
|
||||
return nil, ErrChatOwnerOnly
|
||||
}
|
||||
}
|
||||
@@ -702,7 +707,7 @@ func (s *ChatService) ListMembers(userID, roomID uint, oversee bool) ([]model.Ch
|
||||
}
|
||||
var list []model.ChatRoomMember
|
||||
err := s.db.Where("room_id = ?", roomID).
|
||||
Order("CASE WHEN role = 'owner' THEN 0 ELSE 1 END, created_at ASC").
|
||||
Order("CASE WHEN role = 'owner' THEN 0 WHEN role = 'admin' THEN 1 ELSE 2 END, created_at ASC").
|
||||
Preload("User").
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
@@ -711,18 +716,11 @@ func (s *ChatService) ListMembers(userID, roomID uint, oversee bool) ([]model.Ch
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Kick 群主踢人(不能踢群主)
|
||||
func (s *ChatService) Kick(operatorID, roomID, targetID uint) error {
|
||||
// Kick 群主/群管/全站监管踢人(不能踢群主;群管不能踢其他群管)
|
||||
func (s *ChatService) Kick(operatorID, roomID, targetID uint, oversee bool) error {
|
||||
if _, err := s.getRoom(roomID); err != nil {
|
||||
return err
|
||||
}
|
||||
op, err := s.membership(s.db, roomID, operatorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if op.Role != model.ChatRoleOwner {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
target, err := s.membership(s.db, roomID, targetID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -730,9 +728,158 @@ func (s *ChatService) Kick(operatorID, roomID, targetID uint) error {
|
||||
if target.Role == model.ChatRoleOwner {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
if !oversee {
|
||||
op, err := s.membership(s.db, roomID, operatorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if op.Role != model.ChatRoleOwner && op.Role != model.ChatRoleAdmin {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
if target.Role == model.ChatRoleAdmin && op.Role != model.ChatRoleOwner {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
}
|
||||
return s.removeMember(roomID, targetID)
|
||||
}
|
||||
|
||||
// SetMemberMute 禁言/解禁:群主、群管、全站监管;不可禁言群主或站点站长账号
|
||||
func (s *ChatService) SetMemberMute(operatorID, roomID, targetID uint, muted, oversee bool) error {
|
||||
room, err := s.getRoom(roomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if room.RoomType == model.ChatRoomTypeDirect {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
target, err := s.membership(s.db, roomID, targetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Role == model.ChatRoleOwner {
|
||||
return ErrChatCannotMuteOwner
|
||||
}
|
||||
var targetUser model.User
|
||||
if err := s.db.Select("role").First(&targetUser, targetID).Error; err != nil {
|
||||
return ErrChatUserGone
|
||||
}
|
||||
if targetUser.Role == model.RoleOwner {
|
||||
return ErrChatCannotMuteOwner
|
||||
}
|
||||
if !oversee {
|
||||
op, err := s.membership(s.db, roomID, operatorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if op.Role != model.ChatRoleOwner && op.Role != model.ChatRoleAdmin {
|
||||
return ErrChatMuteDenied
|
||||
}
|
||||
if target.Role == model.ChatRoleAdmin && op.Role != model.ChatRoleOwner {
|
||||
return ErrChatMuteDenied
|
||||
}
|
||||
}
|
||||
return s.db.Model(&model.ChatRoomMember{}).
|
||||
Where("room_id = ? AND user_id = ?", roomID, targetID).
|
||||
Select("muted").Update("muted", muted).Error
|
||||
}
|
||||
|
||||
// SetMemberRole 仅站点站长可任命/撤销群管理员(admin <-> member);不可改群主
|
||||
func (s *ChatService) SetMemberRole(operatorSiteRole model.Role, roomID, targetID uint, role string) error {
|
||||
if operatorSiteRole != model.RoleOwner {
|
||||
return ErrChatSiteOwnerOnly
|
||||
}
|
||||
if role != model.ChatRoleAdmin && role != model.ChatRoleMember {
|
||||
return ErrChatInvalidRole
|
||||
}
|
||||
room, err := s.getRoom(roomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if room.RoomType != model.ChatRoomTypeGroup {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
target, err := s.membership(s.db, roomID, targetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Role == model.ChatRoleOwner || room.OwnerID == targetID {
|
||||
return ErrChatOwnerOnly
|
||||
}
|
||||
return s.db.Model(&model.ChatRoomMember{}).
|
||||
Where("room_id = ? AND user_id = ?", roomID, targetID).
|
||||
Update("role", role).Error
|
||||
}
|
||||
|
||||
// CountAdminRooms 用户作为群管理员的群数量
|
||||
func (s *ChatService) CountAdminRooms(userID uint) (int64, error) {
|
||||
var n int64
|
||||
err := s.db.Model(&model.ChatRoomMember{}).
|
||||
Joins("JOIN chat_rooms ON chat_rooms.id = chat_room_members.room_id").
|
||||
Where("chat_room_members.user_id = ? AND chat_room_members.role = ? AND chat_rooms.deleted_at IS NULL AND chat_rooms.room_type = ?",
|
||||
userID, model.ChatRoleAdmin, model.ChatRoomTypeGroup).
|
||||
Count(&n).Error
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ListAdminRoomIDs 用户作为群管理员的群 ID 列表
|
||||
func (s *ChatService) ListAdminRoomIDs(userID uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := s.db.Model(&model.ChatRoomMember{}).
|
||||
Joins("JOIN chat_rooms ON chat_rooms.id = chat_room_members.room_id").
|
||||
Where("chat_room_members.user_id = ? AND chat_room_members.role = ? AND chat_rooms.deleted_at IS NULL AND chat_rooms.room_type = ?",
|
||||
userID, model.ChatRoleAdmin, model.ChatRoomTypeGroup).
|
||||
Pluck("chat_room_members.room_id", &ids).Error
|
||||
return ids, err
|
||||
}
|
||||
|
||||
// CanAccessAdminMessages 可否进入后台消息管理
|
||||
func (s *ChatService) CanAccessAdminMessages(actor *Actor) bool {
|
||||
if actor == nil {
|
||||
return false
|
||||
}
|
||||
if actor.HasSiteMessagePerm() {
|
||||
return true
|
||||
}
|
||||
n, err := s.CountAdminRooms(actor.ID)
|
||||
return err == nil && n > 0
|
||||
}
|
||||
|
||||
// isChatRoomModerator 群主或群管理员
|
||||
func (s *ChatService) isChatRoomModerator(userID, roomID uint) bool {
|
||||
m, err := s.membership(s.db, roomID, userID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return m.Role == model.ChatRoleOwner || m.Role == model.ChatRoleAdmin
|
||||
}
|
||||
|
||||
// hasSiteGroupMessagePerm 站点消息管理 flag(不含超管/站长,调用方通常已判 oversee)
|
||||
func (s *ChatService) hasSiteGroupMessagePerm(userID uint) bool {
|
||||
var u model.User
|
||||
if err := s.db.Select("can_manage_messages").First(&u, userID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return u.CanManageMessages
|
||||
}
|
||||
|
||||
// canModerateRoomMessages 可撤回他人消息 / 看已撤回正文:全站监管、站点消息管(仅群)、群主/群管
|
||||
func (s *ChatService) canModerateRoomMessages(userID, roomID uint, oversee bool) bool {
|
||||
if oversee {
|
||||
return true
|
||||
}
|
||||
if s.isChatRoomModerator(userID, roomID) {
|
||||
return true
|
||||
}
|
||||
if !s.hasSiteGroupMessagePerm(userID) {
|
||||
return false
|
||||
}
|
||||
room, err := s.getRoom(roomID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return room.RoomType == model.ChatRoomTypeGroup
|
||||
}
|
||||
|
||||
// SendMessage 发送群消息:成员且未被禁言;可选引用回复;解析@并落提醒通知。
|
||||
func (s *ChatService) SendMessage(senderID, roomID uint, content string, replyToID uint) (*model.ChatMessage, []uint, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
@@ -907,8 +1054,9 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
|
||||
msgs[i], msgs[j] = msgs[j], msgs[i]
|
||||
}
|
||||
// 已撤回正文:仅监管者(站长/超管)保留,供聊天界面审计展示;普通成员清空
|
||||
if !oversee {
|
||||
// 已撤回正文:监管者 / 站点消息管(群)/ 群主群管保留
|
||||
seeRecalled := oversee || s.canModerateRoomMessages(userID, roomID, false)
|
||||
if !seeRecalled {
|
||||
for i := range msgs {
|
||||
if msgs[i].RecalledAt != nil {
|
||||
msgs[i].Content = ""
|
||||
@@ -918,7 +1066,7 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
return msgs, hasMore, nil
|
||||
}
|
||||
|
||||
// RecallMessage 撤回消息:群主可撤自己的;监管者可撤任意;标记 RecalledAt,正文保留供审计。
|
||||
// RecallMessage 撤回消息:本人;或群主/群管/站点消息管(群)/全站监管可撤他人。
|
||||
// newly=true 表示本次刚打上撤回标记(已撤回的幂等调用为 false,避免重复回退角标)。
|
||||
func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee bool) (*model.ChatMessage, bool, error) {
|
||||
if _, err := s.getRoom(roomID); err != nil {
|
||||
@@ -934,19 +1082,16 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
if msg.RecalledAt != nil {
|
||||
return &msg, false, nil // 幂等
|
||||
}
|
||||
if !oversee {
|
||||
m, err := s.membership(s.db, roomID, operatorID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
// 群主/成员仅可撤回自己的消息;监管者可撤任意
|
||||
if msg.SenderID != operatorID {
|
||||
_ = m // 成员校验已通过
|
||||
return nil, false, ErrChatRecallDenied
|
||||
if msg.SenderID == operatorID {
|
||||
if !oversee {
|
||||
if _, err := s.membership(s.db, roomID, operatorID); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
} else if !s.canModerateRoomMessages(operatorID, roomID, oversee) {
|
||||
return nil, false, ErrChatRecallDenied
|
||||
}
|
||||
now := time.Now()
|
||||
// 只打撤回标记,保留正文供站长审计;前台 ListMessages 对非监管者置空 content
|
||||
if err := s.db.Model(&msg).Updates(map[string]interface{}{
|
||||
"recalled_at": now,
|
||||
"recalled_by": operatorID,
|
||||
@@ -982,20 +1127,44 @@ type AdminChatMessageItem struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// AdminListMessages 按房间类型分页:group=大厅+群聊,direct=私聊;支持正文/房间/参与者搜索
|
||||
func (s *ChatService) AdminListMessages(page int, keyword, roomType string) ([]AdminChatMessageItem, int64, error) {
|
||||
// AdminListMessages 按房间类型分页;scope 限制可见房间;非全站监管脱敏已撤回正文。
|
||||
// actor 为站长/超管:全站;HasSiteMessagePerm:全部群聊(无私聊);否则仅群管房间。
|
||||
func (s *ChatService) AdminListMessages(actor *Actor, page int, keyword, roomType string) ([]AdminChatMessageItem, int64, error) {
|
||||
if actor == nil || !s.CanAccessAdminMessages(actor) {
|
||||
return nil, 0, ErrChatAdminAccess
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
fullOversee := CanOverseeChat(actor.Role)
|
||||
siteGroup := actor.HasSiteMessagePerm() && !fullOversee
|
||||
if roomType != model.ChatRoomTypeGroup && roomType != model.ChatRoomTypeDirect {
|
||||
return nil, 0, errors.New("房间类型无效")
|
||||
}
|
||||
// 非全站监管不可看私聊
|
||||
if roomType == model.ChatRoomTypeDirect && !fullOversee {
|
||||
return []AdminChatMessageItem{}, 0, nil
|
||||
}
|
||||
const pageSize = 15
|
||||
q := s.db.Model(&model.ChatMessage{}).
|
||||
Joins("JOIN chat_rooms ON chat_rooms.id = chat_messages.room_id").
|
||||
Joins("LEFT JOIN users ON users.id = chat_messages.sender_id").
|
||||
Where("chat_rooms.deleted_at IS NULL AND chat_messages.deleted_at IS NULL").
|
||||
Where("chat_rooms.room_type = ?", roomType)
|
||||
|
||||
if !fullOversee && !siteGroup {
|
||||
ids, err := s.ListAdminRoomIDs(actor.ID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return []AdminChatMessageItem{}, 0, nil
|
||||
}
|
||||
q = q.Where("chat_messages.room_id IN ?", ids)
|
||||
} else if siteGroup {
|
||||
q = q.Where("chat_rooms.room_type = ?", model.ChatRoomTypeGroup)
|
||||
}
|
||||
|
||||
if kw := strings.TrimSpace(keyword); kw != "" {
|
||||
like := "%" + kw + "%"
|
||||
q = q.Where(
|
||||
@@ -1037,7 +1206,6 @@ func (s *ChatService) AdminListMessages(page int, keyword, roomType string) ([]A
|
||||
roomMap[r.ID] = r
|
||||
}
|
||||
}
|
||||
// 私聊:预加载双方,供前端叠放头像与「甲 · 乙」会话条
|
||||
participantsByRoom := map[uint][]AdminChatParticipant{}
|
||||
if roomType == model.ChatRoomTypeDirect && len(roomIDs) > 0 {
|
||||
var members []model.ChatRoomMember
|
||||
@@ -1073,9 +1241,14 @@ func (s *ChatService) AdminListMessages(page int, keyword, roomType string) ([]A
|
||||
name = "私聊"
|
||||
}
|
||||
}
|
||||
content := m.Content
|
||||
// 后台列表:仅全站监管可见已撤回正文;群管/消息管理员在前台会话内可看
|
||||
if !fullOversee && m.RecalledAt != nil {
|
||||
content = ""
|
||||
}
|
||||
item := AdminChatMessageItem{
|
||||
ID: m.ID, RoomID: m.RoomID, RoomName: name, RoomType: r.RoomType, IsDefault: r.IsDefault,
|
||||
SenderID: m.SenderID, Content: m.Content, RecalledAt: m.RecalledAt, CreatedAt: m.CreatedAt,
|
||||
SenderID: m.SenderID, Content: content, RecalledAt: m.RecalledAt, CreatedAt: m.CreatedAt,
|
||||
Participants: parts,
|
||||
}
|
||||
if m.Sender.ID > 0 {
|
||||
|
||||
@@ -435,6 +435,13 @@ func (s *CommentService) Delete(actor *Actor, commentID, userID uint, opts Delet
|
||||
if actor == nil || !actor.CanModerateBoard(post.BoardID) {
|
||||
return ErrCommentForbidden
|
||||
}
|
||||
var author model.User
|
||||
if err := s.db.Select("role").First(&author, comment.UserID).Error; err != nil {
|
||||
return ErrCommentNotFound
|
||||
}
|
||||
if !actor.CanModerateAuthor(author.Role) {
|
||||
return ErrAuthorProtected
|
||||
}
|
||||
deleteType = strings.TrimSpace(opts.DeleteType)
|
||||
if !model.ValidPostDeleteType(deleteType) {
|
||||
return ErrCommentInvalidType
|
||||
@@ -490,6 +497,13 @@ func (s *CommentService) Restore(actor *Actor, commentID, userID uint) error {
|
||||
return ErrCommentNotFound
|
||||
}
|
||||
if actor != nil && actor.CanModerateBoard(post.BoardID) {
|
||||
var author model.User
|
||||
if err := s.db.Select("role").First(&author, comment.UserID).Error; err != nil {
|
||||
return ErrCommentNotFound
|
||||
}
|
||||
if !actor.CanModerateAuthor(author.Role) {
|
||||
return ErrAuthorProtected
|
||||
}
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
@@ -546,6 +560,9 @@ func (s *CommentService) collectDescendantIDs(comment model.Comment) []uint {
|
||||
|
||||
// Purge 硬删评论及子树(仅板块审核权);不占位;对尚未软删的已发布条递减 comment_count。
|
||||
func (s *CommentService) Purge(actor *Actor, commentID, userID uint) error {
|
||||
if actor == nil || !actor.CanPurgeContent() {
|
||||
return ErrPurgeOwnerOnly
|
||||
}
|
||||
var comment model.Comment
|
||||
if err := s.db.Unscoped().First(&comment, commentID).Error; err != nil {
|
||||
return ErrCommentNotFound
|
||||
@@ -554,7 +571,7 @@ func (s *CommentService) Purge(actor *Actor, commentID, userID uint) error {
|
||||
if err := s.db.Select("id", "board_id").First(&post, comment.PostID).Error; err != nil {
|
||||
return ErrCommentNotFound
|
||||
}
|
||||
if actor == nil || !actor.CanModerateBoard(post.BoardID) {
|
||||
if !actor.CanModerateBoard(post.BoardID) {
|
||||
return ErrCommentForbidden
|
||||
}
|
||||
_ = userID
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
var (
|
||||
ErrModerationForbidden = errors.New("无权审核该板块的内容")
|
||||
ErrNotPending = errors.New("该内容不在待审核状态")
|
||||
ErrAuthorProtected = errors.New("无权处置该作者的内容")
|
||||
ErrPurgeOwnerOnly = errors.New("仅站长可彻底删除")
|
||||
)
|
||||
|
||||
// ModerationService 帖子/评论审核队列
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
var (
|
||||
ErrPostNotFound = errors.New("帖子不存在")
|
||||
ErrPostForbidden = errors.New("无权限操作此帖子")
|
||||
ErrPostDeleteMeta = errors.New("管理删除须填写类型与理由")
|
||||
ErrPostDeleteMeta = errors.New("选「其他」时须填写删除理由")
|
||||
ErrPostInvalidType = errors.New("无效的删除类型")
|
||||
)
|
||||
|
||||
@@ -1083,16 +1083,27 @@ func (s *PostService) Delete(actor *Actor, postID, userID uint, opts DeletePostO
|
||||
if post.UserID == userID {
|
||||
staffDeleted = false
|
||||
} else if actor != nil && actor.CanModerateBoard(post.BoardID) {
|
||||
var author model.User
|
||||
if err := s.db.Select("role").First(&author, post.UserID).Error; err != nil {
|
||||
return false, authorID, ErrPostNotFound
|
||||
}
|
||||
if !actor.CanModerateAuthor(author.Role) {
|
||||
return false, authorID, ErrAuthorProtected
|
||||
}
|
||||
deleteType = strings.TrimSpace(opts.DeleteType)
|
||||
deleteReason = strings.TrimSpace(opts.DeleteReason)
|
||||
if !model.ValidPostDeleteType(deleteType) {
|
||||
return false, authorID, ErrPostInvalidType
|
||||
}
|
||||
if deleteReason == "" {
|
||||
return false, authorID, ErrPostDeleteMeta
|
||||
}
|
||||
if len([]rune(deleteReason)) > 200 {
|
||||
return false, authorID, errors.New("删除理由不能超过 200 字")
|
||||
if deleteType == model.PostDeleteTypeOther {
|
||||
if deleteReason == "" {
|
||||
return false, authorID, ErrPostDeleteMeta
|
||||
}
|
||||
if len([]rune(deleteReason)) > 200 {
|
||||
return false, authorID, errors.New("删除理由不能超过 200 字")
|
||||
}
|
||||
} else {
|
||||
deleteReason = ""
|
||||
}
|
||||
staffDeleted = true
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user