补齐论坛核心能力:讨论锁定、发帖本地草稿、标签精确筛选,以及私信与通知分流。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -194,6 +194,11 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
|
||||
return nil, errors.New("账号已被禁言")
|
||||
}
|
||||
|
||||
// 讨论锁定:管理员亦不可强评(避免结贴后仍被顶楼)
|
||||
if post.CommentsLocked {
|
||||
return nil, ErrPostCommentsLocked
|
||||
}
|
||||
|
||||
// 未公开帖仅作者/管理员可评论
|
||||
if post.Status != model.ContentStatusPublished && post.Status != "" {
|
||||
if user.Role != model.RoleAdmin && post.UserID != in.UserID {
|
||||
|
||||
@@ -25,6 +25,7 @@ var (
|
||||
ErrPermissionDenied = errors.New("无权操作")
|
||||
ErrBoardNotFound = errors.New("板块不存在")
|
||||
ErrPostEditLocked = errors.New("帖子已被管理员锁定,无法编辑")
|
||||
ErrPostCommentsLocked = errors.New("该帖子已锁定讨论,无法评论")
|
||||
ErrPostEditExpired = errors.New("已超过可编辑时限")
|
||||
ErrRevisionNotFound = errors.New("历史版本不存在")
|
||||
ErrInvalidSetting = errors.New("无效的设置值")
|
||||
|
||||
@@ -130,6 +130,59 @@ func (s *MessageService) UnreadCount(userID uint) (int64, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// UnreadCounts 未读总数,以及私信 / 系统通知分项
|
||||
func (s *MessageService) UnreadCounts(userID uint) (total, dm, notify int64, err error) {
|
||||
err = model.DB.Model(&model.PrivateMessage{}).
|
||||
Where("to_user_id = ? AND is_read = ?", userID, false).
|
||||
Count(&total).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
err = model.DB.Model(&model.PrivateMessage{}).
|
||||
Where("to_user_id = ? AND is_read = ? AND from_user_id = 0", userID, false).
|
||||
Count(¬ify).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
dm = total - notify
|
||||
if dm < 0 {
|
||||
dm = 0
|
||||
}
|
||||
return total, dm, notify, nil
|
||||
}
|
||||
|
||||
// ListNotifications 系统通知列表(按时间倒序,非聊天气泡)
|
||||
func (s *MessageService) ListNotifications(userID uint, page, size int, kind string) ([]model.PrivateMessage, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
size = s.settings.NormalizePageSize(size)
|
||||
db := model.DB.Model(&model.PrivateMessage{}).
|
||||
Where("from_user_id = 0 AND to_user_id = ?", userID)
|
||||
kind = strings.TrimSpace(kind)
|
||||
if kind != "" && kind != "all" {
|
||||
db = db.Where("kind = ?", kind)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var list []model.PrivateMessage
|
||||
err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&list).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if list == nil {
|
||||
list = []model.PrivateMessage{}
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// MarkNotificationsRead 将系统通知全部标为已读
|
||||
func (s *MessageService) MarkNotificationsRead(userID uint) error {
|
||||
return s.MarkConversationRead(userID, 0)
|
||||
}
|
||||
|
||||
// MessageConversation 按对方聚合的会话摘要
|
||||
type MessageConversation struct {
|
||||
PeerUserID uint `json:"peer_user_id"` // 0 = 系统通知
|
||||
|
||||
@@ -34,6 +34,7 @@ type PostListQuery struct {
|
||||
Page int
|
||||
Size int
|
||||
Keyword string
|
||||
Tag string // 精确标签筛选(整枚匹配,不走 keyword LIKE)
|
||||
Sort string // latest | reply | hot
|
||||
ViewerID uint // 当前查看者(用于 pending 仅作者可见)
|
||||
ViewerIsAdmin bool
|
||||
@@ -271,6 +272,12 @@ func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) {
|
||||
kw := "%" + q.Keyword + "%"
|
||||
db = db.Where("title LIKE ? OR content_plain LIKE ? OR tags LIKE ?", kw, kw, kw)
|
||||
}
|
||||
if tag := strings.TrimSpace(q.Tag); tag != "" {
|
||||
// 整枚标签匹配:逗号/中文逗号分隔,忽略标签两侧空格,大小写不敏感
|
||||
escaped := escapeLikePattern(strings.ToLower(tag))
|
||||
normalized := "LOWER(',' || REPLACE(REPLACE(REPLACE(IFNULL(tags,''), ',', ','), ', ', ','), ' ,', ',') || ',')"
|
||||
db = db.Where(normalized+" LIKE ? ESCAPE '\\'", "%,"+escaped+",%")
|
||||
}
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
var posts []model.Post
|
||||
@@ -310,6 +317,14 @@ func normalizePostSort(sort string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// escapeLikePattern 转义 LIKE 通配符,配合 ESCAPE '\'
|
||||
func escapeLikePattern(s string) string {
|
||||
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||
s = strings.ReplaceAll(s, `%`, `\%`)
|
||||
s = strings.ReplaceAll(s, `_`, `\_`)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PostService) FindByID(id uint) (*model.Post, error) {
|
||||
var post model.Post
|
||||
err := model.DB.Preload("User").Preload("Board").First(&post, id).Error
|
||||
@@ -535,6 +550,18 @@ func (s *PostService) SetEditLocked(postID uint, locked bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCommentsLocked 锁定/解锁讨论(禁止新评论)
|
||||
func (s *PostService) SetCommentsLocked(postID uint, locked bool) error {
|
||||
res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("comments_locked", locked)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrPostNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostService) ListRevisions(postID uint) ([]model.PostRevision, error) {
|
||||
var revs []model.PostRevision
|
||||
err := model.DB.Preload("Editor").Where("post_id = ?", postID).
|
||||
|
||||
Reference in New Issue
Block a user