feat: 实时消息与管理端运营能力

新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 22:58:16 +08:00
parent 29db9ae9b5
commit 42c395a864
55 changed files with 7607 additions and 346 deletions

View File

@@ -29,9 +29,11 @@ type PostListQuery struct {
BoardID uint
Page int
Size int
Sort string // latest | hot
Sort string // latest | hot | new
Keyword string // 搜索关键词
Recommended bool // 仅精华帖
ViewerID uint // 当前查看者(0=游客)
Actor *Actor // 查看者权限快照;nil 仅 published
}
// toPostListItems 将 Post 模型批量转为不含正文的列表项
@@ -42,7 +44,7 @@ func toPostListItems(posts []model.Post) []PostListItem {
ID: p.ID, BoardID: p.BoardID, UserID: p.UserID,
Title: p.Title, Tags: p.Tags, PostType: p.PostType,
Pinned: p.Pinned, Recommended: p.Recommended, LikeCount: p.LikeCount, ViewCount: p.ViewCount,
CommentCount: p.CommentCount, CreatedAt: p.CreatedAt,
CommentCount: p.CommentCount, Status: p.Status, CreatedAt: p.CreatedAt,
Board: p.Board, User: p.User,
})
}
@@ -76,6 +78,7 @@ type PostListItem struct {
LikeCount int `json:"like_count"`
ViewCount int `json:"view_count"`
CommentCount int `json:"comment_count"`
Status string `json:"status"`
Liked bool `json:"liked"`
CreatedAt time.Time `json:"created_at"`
LastReply *LastReplyInfo `json:"last_reply,omitempty"`
@@ -145,6 +148,26 @@ func (s *PostService) fillLastReply(items []PostListItem) {
}
}
// applyListVisibility 公开流可见性:published,或 pending 且(作者本人 / 该板可审者)
func applyListVisibility(db *gorm.DB, viewerID uint, actor *Actor) *gorm.DB {
if viewerID == 0 || actor == nil {
return db.Where("status = ?", model.ContentStatusPublished)
}
if model.RoleLevel(actor.Role) >= model.RoleLevel(model.RoleAdmin) {
return db.Where("status IN ?", []string{model.ContentStatusPublished, model.ContentStatusPending})
}
if actor.Role == model.RoleBoardAdmin && len(actor.BoardIDs) > 0 {
return db.Where(
"status = ? OR (status = ? AND (user_id = ? OR board_id IN ?))",
model.ContentStatusPublished, model.ContentStatusPending, viewerID, actor.BoardIDs,
)
}
return db.Where(
"status = ? OR (status = ? AND user_id = ?)",
model.ContentStatusPublished, model.ContentStatusPending, viewerID,
)
}
// List 获取帖子列表
func (s *PostService) List(q PostListQuery) ([]PostListItem, int64, error) {
if q.Page < 1 {
@@ -154,7 +177,7 @@ func (s *PostService) List(q PostListQuery) ([]PostListItem, int64, error) {
q.Size = 20
}
query := s.db.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished)
query := applyListVisibility(s.db.Model(&model.Post{}), q.ViewerID, q.Actor)
if q.BoardID > 0 {
query = query.Where("board_id = ?", q.BoardID)
}
@@ -196,15 +219,26 @@ func (s *PostService) List(q PostListQuery) ([]PostListItem, int64, error) {
return items, total, nil
}
// ListByUser 获取指定用户发布的帖子(公开已发布)
func (s *PostService) ListByUser(userID uint, page, size int) ([]PostListItem, int64, error) {
// ListByUser 获取指定用户发布的帖子。
// viewer 为本人或全站可审者时并入 pending;板块管理员仅并入其授权板的 pending。
func (s *PostService) ListByUser(userID, viewerID uint, actor *Actor, page, size int) ([]PostListItem, int64, error) {
if page < 1 {
page = 1
}
if size < 1 || size > 50 {
size = 20
}
query := s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished)
query := s.db.Model(&model.Post{}).Where("user_id = ?", userID)
if viewerID == userID || (actor != nil && model.RoleLevel(actor.Role) >= model.RoleLevel(model.RoleAdmin)) {
query = query.Where("status IN ?", []string{model.ContentStatusPublished, model.ContentStatusPending})
} else if actor != nil && actor.Role == model.RoleBoardAdmin && len(actor.BoardIDs) > 0 {
query = query.Where(
"status = ? OR (status = ? AND board_id IN ?)",
model.ContentStatusPublished, model.ContentStatusPending, actor.BoardIDs,
)
} else {
query = query.Where("status = ?", model.ContentStatusPublished)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
@@ -229,6 +263,15 @@ func (s *PostService) GetAuthorID(postID uint) (uint, error) {
return post.UserID, nil
}
// GetBoardID 获取帖子所属板块 ID(不增加浏览量)
func (s *PostService) GetBoardID(postID uint) (uint, error) {
var post model.Post
if err := s.db.Select("board_id").First(&post, postID).Error; err != nil {
return 0, err
}
return post.BoardID, nil
}
// CountByUser 统计用户发帖数
func (s *PostService) CountByUser(userID uint) (int64, error) {
var total int64