package service import ( "strconv" "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"` TodayUsers int64 `json:"today_users"` TodayComments int64 `json:"today_comments"` Online int64 `json:"online"` // 近 5 分钟活跃用户 PeakOnline int64 `json:"peak_online"` // 历史在线峰值 } // ActiveUser 本周活跃用户(发帖数 / 回复数分列) type ActiveUser struct { ID uint `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` PostCount int64 `json:"post_count"` CommentCount int64 `json:"comment_count"` } // BoardCount 板块及其已发布帖子计数(Board 字段被 encoding/json 展平) type BoardCount struct { model.Board PostCount int64 `json:"post_count"` } // AnnouncementItem 首页"站点公告"条目(标签/日期/标题) type AnnouncementItem struct { ID uint `json:"id"` Title string `json:"title"` Tag string `json:"tag"` TagColor string `json:"tag_color"` CreatedAt time.Time `json:"created_at"` } // NewUserItem 最新注册成员 type NewUserItem struct { ID uint `json:"id"` Username string `json:"username"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` CreatedAt time.Time `json:"created_at"` } // OverviewData 首页聚合数据 type OverviewData struct { Stats OverviewStats `json:"stats"` Hot []PostListItem `json:"hot"` ActiveUsers []ActiveUser `json:"active_users"` Boards []BoardCount `json:"boards"` Announcements []AnnouncementItem `json:"announcements"` NewUsers []NewUserItem `json:"new_users"` Checkin *CheckinStatus `json:"checkin,omitempty"` } const ( // onlineWindow 判定"在线"的最近活跃窗口 onlineWindow = 5 * time.Minute // peakOnlineKey 历史在线峰值在 site_settings 中的键 peakOnlineKey = "peak_online" ) // startOfWeek 返回本周一 00:00(本地时区) func startOfWeek(t time.Time) time.Time { wd := int(t.Weekday()) if wd == 0 { wd = 7 // 周日归到本周末尾 } d := t.AddDate(0, 0, -(wd - 1)) return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, t.Location()) } // Get 聚合首页数据。hot 的 liked 态由 handler 层按当前用户填充。 func (s *OverviewService) Get() (*OverviewData, error) { data := &OverviewData{ Hot: []PostListItem{}, ActiveUsers: []ActiveUser{}, Boards: []BoardCount{}, Announcements: []AnnouncementItem{}, NewUsers: []NewUserItem{}, } now := time.Now() dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) weekStart := startOfWeek(now) onlineSince := now.Add(-onlineWindow) // 统计 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 } if err := s.db.Model(&model.User{}). Where("created_at >= ?", dayStart). Count(&data.Stats.TodayUsers).Error; err != nil { return nil, err } if err := s.db.Model(&model.Comment{}). Where("status = ? AND created_at >= ?", model.ContentStatusPublished, dayStart). Count(&data.Stats.TodayComments).Error; err != nil { return nil, err } // 在线用户(近 5 分钟有请求)与历史峰值 if err := s.db.Model(&model.User{}). Where("last_seen_at IS NOT NULL AND last_seen_at >= ? AND banned = ?", onlineSince, false). Count(&data.Stats.Online).Error; err != nil { return nil, err } data.Stats.PeakOnline = s.refreshPeakOnline(data.Stats.Online) // 热门榜:非置顶,按 赞 + 评论×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) // 本周活跃用户:发帖/评论分别计数,JOIN 用户过滤封禁/软删 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 created_at >= ? AND deleted_at IS NULL UNION ALL SELECT user_id, 0 AS is_post 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 COUNT(*) DESC, a.user_id ASC LIMIT 5` if err := s.db.Raw(activitySQL, model.ContentStatusPublished, weekStart, model.ContentStatusPublished, weekStart, 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, }) } } // 板块 + 各自帖子计数(一条 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]}) } // 站点公告(已发布,最新 5 条) var anns []model.Announcement if err := s.db.Where("published = ?", true). Order("created_at DESC, id DESC").Limit(5).Find(&anns).Error; err != nil { return nil, err } for _, a := range anns { data.Announcements = append(data.Announcements, AnnouncementItem{ ID: a.ID, Title: a.Title, Tag: a.Tag, TagColor: a.TagColor, CreatedAt: a.CreatedAt, }) } // 最新注册成员 var newUsers []model.User if err := s.db.Select("id, username, nickname, avatar, created_at"). Where("banned = ?", false). Order("created_at DESC").Limit(8).Find(&newUsers).Error; err != nil { return nil, err } for _, u := range newUsers { data.NewUsers = append(data.NewUsers, NewUserItem{ ID: u.ID, Username: u.Username, Nickname: u.Nickname, Avatar: u.Avatar, CreatedAt: u.CreatedAt, }) } return data, nil } // refreshPeakOnline 读取历史峰值,当前在线更高则落库刷新 func (s *OverviewService) refreshPeakOnline(online int64) int64 { var setting model.SiteSetting err := s.db.Where("key = ?", peakOnlineKey).First(&setting).Error if err == gorm.ErrRecordNotFound { setting = model.SiteSetting{Key: peakOnlineKey, Value: strconv.FormatInt(online, 10)} if online > 0 { s.db.Create(&setting) } return online } if err != nil { return 0 } peak, _ := strconv.ParseInt(setting.Value, 10, 64) if online > peak { s.db.Model(&model.SiteSetting{}).Where("key = ?", peakOnlineKey). Update("value", strconv.FormatInt(online, 10)) return online } return peak }