Files
jiang13-bbs/backend/service/board.go
freefire 833bcd33a1 chore: 去掉未上线前的旧兼容垫片,板块流收口到首页
鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:04:47 +08:00

225 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"time"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
)
// BoardService 板块服务
type BoardService struct {
db *gorm.DB
}
func NewBoardService(db *gorm.DB) *BoardService {
return &BoardService{db: db}
}
// BoardWithCount 板块及其已发布帖子计数(Board 字段被 encoding/json 展平)
type BoardWithCount struct {
model.Board
PostCount int64 `json:"post_count"`
}
// BoardModerator 板块管理员公开资料
type BoardModerator struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}
// BoardSidebarStats 板级统计(不含在线)
type BoardSidebarStats struct {
PostCount int64 `json:"post_count"`
TodayPosts int64 `json:"today_posts"`
CommentCount int64 `json:"comment_count"`
TodayComments int64 `json:"today_comments"`
}
// BoardSidebarData 首页选中板块后的右栏聚合
type BoardSidebarData struct {
Board BoardWithCount `json:"board"`
Moderators []BoardModerator `json:"moderators"`
Stats BoardSidebarStats `json:"stats"`
Hot []PostListItem `json:"hot"`
ActiveUsers []ActiveUser `json:"active_users"`
Checkin *CheckinStatus `json:"checkin,omitempty"`
}
// List 获取所有板块(按 sort_order 排序),附带各板块已发布帖子数(板块目录页展示用)
func (s *BoardService) List() ([]BoardWithCount, error) {
var boards []model.Board
if err := s.db.Order("sort_order ASC, id ASC").Find(&boards).Error; err != nil {
return nil, err
}
type countRow struct {
BoardID uint
Cnt int64
}
var counts []countRow
if err := s.db.Model(&model.Post{}).
Select("board_id, COUNT(*) AS cnt").
Where("status = ?", model.ContentStatusPublished).
Group("board_id").Scan(&counts).Error; err != nil {
return nil, err
}
countMap := make(map[uint]int64, len(counts))
for _, c := range counts {
countMap[c.BoardID] = c.Cnt
}
items := make([]BoardWithCount, 0, len(boards))
for _, b := range boards {
items = append(items, BoardWithCount{Board: b, PostCount: countMap[b.ID]})
}
return items, nil
}
// Get 根据 ID 获取板块
func (s *BoardService) Get(id uint) (*model.Board, error) {
var board model.Board
if err := s.db.First(&board, id).Error; err != nil {
return nil, err
}
return &board, nil
}
// Sidebar 首页板块右栏:管理员 + 概况统计 + 本板热门/活跃
func (s *BoardService) Sidebar(boardID uint) (*BoardSidebarData, error) {
board, err := s.Get(boardID)
if err != nil {
return nil, err // 含 gorm.ErrRecordNotFound
}
data := &BoardSidebarData{
Board: BoardWithCount{Board: *board},
Moderators: []BoardModerator{},
Hot: []PostListItem{},
ActiveUsers: []ActiveUser{},
}
now := time.Now()
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
weekStart := startOfWeek(now)
// 帖子总数 / 今日新帖
if err := s.db.Model(&model.Post{}).
Where("board_id = ? AND status = ?", boardID, model.ContentStatusPublished).
Count(&data.Stats.PostCount).Error; err != nil {
return nil, err
}
data.Board.PostCount = data.Stats.PostCount
if err := s.db.Model(&model.Post{}).
Where("board_id = ? AND status = ? AND created_at >= ?", boardID, model.ContentStatusPublished, dayStart).
Count(&data.Stats.TodayPosts).Error; err != nil {
return nil, err
}
// 评论总数 / 今日评论(经 posts.board_id)
if err := s.db.Model(&model.Comment{}).
Joins("JOIN posts ON posts.id = comments.post_id AND posts.deleted_at IS NULL").
Where("posts.board_id = ? AND posts.status = ? AND comments.status = ?",
boardID, model.ContentStatusPublished, model.ContentStatusPublished).
Count(&data.Stats.CommentCount).Error; err != nil {
return nil, err
}
if err := s.db.Model(&model.Comment{}).
Joins("JOIN posts ON posts.id = comments.post_id AND posts.deleted_at IS NULL").
Where("posts.board_id = ? AND posts.status = ? AND comments.status = ? AND comments.created_at >= ?",
boardID, model.ContentStatusPublished, model.ContentStatusPublished, dayStart).
Count(&data.Stats.TodayComments).Error; err != nil {
return nil, err
}
// 板块管理员:仅 board_admin 且授权本板、未封禁
var mods []model.User
if err := s.db.Table("users").
Select("users.id, users.username, users.nickname, users.avatar").
Joins("JOIN user_boards ON user_boards.user_id = users.id AND user_boards.board_id = ?", boardID).
Where("users.role = ? AND users.banned = ? AND users.deleted_at IS NULL", model.RoleBoardAdmin, false).
Order("users.id ASC").
Find(&mods).Error; err != nil {
return nil, err
}
for _, u := range mods {
data.Moderators = append(data.Moderators, BoardModerator{
ID: u.ID,
Username: u.Username,
Nickname: u.Nickname,
Avatar: u.Avatar,
})
}
// 本板热门
var hotPosts []model.Post
if err := s.db.Where("board_id = ? AND status = ? AND pinned = 0", boardID, model.ContentStatusPublished).
Order("(like_count + comment_count * 2) DESC, created_at DESC").
Limit(5).Preload("Board").Preload("User").Find(&hotPosts).Error; err != nil {
return nil, err
}
data.Hot = toPostListItems(hotPosts)
// 本板本周活跃
type activityRow struct {
UserID uint
PostCount int64
CommentCount int64
}
var rows []activityRow
activitySQL := `
SELECT a.user_id AS user_id,
SUM(a.is_post) AS post_count,
SUM(1 - a.is_post) AS comment_count
FROM (
SELECT user_id, 1 AS is_post FROM posts
WHERE status = ? AND board_id = ? AND created_at >= ? AND deleted_at IS NULL
UNION ALL
SELECT c.user_id, 0 AS is_post FROM comments c
JOIN posts p ON p.id = c.post_id AND p.deleted_at IS NULL
WHERE c.status = ? AND c.created_at >= ? AND c.deleted_at IS NULL
AND p.board_id = ? AND p.status = ?
) AS a
JOIN users AS u ON u.id = a.user_id
WHERE u.banned = ? AND u.deleted_at IS NULL
GROUP BY a.user_id
ORDER BY COUNT(*) DESC, a.user_id ASC
LIMIT 5`
if err := s.db.Raw(activitySQL,
model.ContentStatusPublished, boardID, weekStart,
model.ContentStatusPublished, weekStart, boardID, model.ContentStatusPublished,
false).Scan(&rows).Error; err != nil {
return nil, err
}
if len(rows) > 0 {
ids := make([]uint, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.UserID)
}
var users []model.User
if err := s.db.Where("id IN ?", ids).Find(&users).Error; err != nil {
return nil, err
}
userMap := make(map[uint]model.User, len(users))
for _, u := range users {
userMap[u.ID] = u
}
for _, r := range rows {
u, ok := userMap[r.UserID]
if !ok {
continue
}
data.ActiveUsers = append(data.ActiveUsers, ActiveUser{
ID: u.ID,
Username: u.Username,
Nickname: u.Nickname,
Avatar: u.Avatar,
PostCount: r.PostCount,
CommentCount: r.CommentCount,
})
}
}
return data, nil
}