feat: 完成社区首页功能迭代,新增首页聚合与工作台组件

本次提交实现了完整的社区首页功能:
1. 新增首页聚合接口与服务,包含社区统计、热门帖、活跃用户与板块数据
2. 实现帖子推荐排序、SSR缓存与点赞态同步逻辑
3. 重构导航与布局系统,新增工作台侧边栏与移动端适配
4. 优化主题色与UI样式,统一设计系统风格
5. 修复登录注册页自动跳转与组件状态同步问题
This commit is contained in:
2026-09-12 23:36:18 +08:00
parent 055688a6cf
commit bee06fa962
24 changed files with 1446 additions and 359 deletions

167
backend/service/overview.go Normal file
View File

@@ -0,0 +1,167 @@
package service
import (
"time"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
)
// OverviewService 首页聚合服务:一次请求返回统计、热门榜、活跃用户、板块计数
type OverviewService struct {
db *gorm.DB
}
func NewOverviewService(db *gorm.DB) *OverviewService {
return &OverviewService{db: db}
}
// OverviewStats 社区整体统计
type OverviewStats struct {
Posts int64 `json:"posts"`
Users int64 `json:"users"`
Comments int64 `json:"comments"`
TodayPosts int64 `json:"today_posts"`
}
// ActiveUser 近 14 天活跃用户
type ActiveUser struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
ActivityCount int64 `json:"activity_count"`
}
// BoardCount 板块及其已发布帖子计数(Board 字段被 encoding/json 展平)
type BoardCount struct {
model.Board
PostCount int64 `json:"post_count"`
}
// OverviewData 首页聚合数据
type OverviewData struct {
Stats OverviewStats `json:"stats"`
Hot []PostListItem `json:"hot"`
ActiveUsers []ActiveUser `json:"active_users"`
Boards []BoardCount `json:"boards"`
}
const activeUserWindow = 14 * 24 * time.Hour
// Get 聚合首页数据。hot 的 liked 态由 handler 层按当前用户填充。
func (s *OverviewService) Get() (*OverviewData, error) {
data := &OverviewData{
Hot: []PostListItem{},
ActiveUsers: []ActiveUser{},
Boards: []BoardCount{},
}
now := time.Now()
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
since := now.Add(-activeUserWindow)
// 统计
postQ := s.db.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished)
if err := postQ.Count(&data.Stats.Posts).Error; err != nil {
return nil, err
}
if err := s.db.Model(&model.User{}).Count(&data.Stats.Users).Error; err != nil {
return nil, err
}
if err := s.db.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&data.Stats.Comments).Error; err != nil {
return nil, err
}
if err := s.db.Model(&model.Post{}).
Where("status = ? AND created_at >= ?", model.ContentStatusPublished, dayStart).
Count(&data.Stats.TodayPosts).Error; err != nil {
return nil, err
}
// 热门榜:非置顶,按 赞 + 评论×2
var hotPosts []model.Post
if err := s.db.Where("status = ? AND pinned = 0", 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)
// 活跃用户:近 14 天发帖+评论合并计数,JOIN 用户过滤封禁/软删
type activityRow struct {
UserID uint
ActivityCount int64
}
var rows []activityRow
activitySQL := `
SELECT a.user_id AS user_id, COUNT(*) AS activity_count
FROM (
SELECT user_id FROM posts WHERE status = ? AND created_at >= ? AND deleted_at IS NULL
UNION ALL
SELECT user_id FROM comments WHERE status = ? AND created_at >= ? AND deleted_at IS NULL
) 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 activity_count DESC, a.user_id ASC
LIMIT 5`
if err := s.db.Raw(activitySQL,
model.ContentStatusPublished, since,
model.ContentStatusPublished, since,
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,
ActivityCount: r.ActivityCount,
})
}
}
// 板块 + 各自帖子计数(一条 GROUP BY)
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
}
for _, b := range boards {
data.Boards = append(data.Boards, BoardCount{Board: b, PostCount: countMap[b.ID]})
}
return data, nil
}