本次提交完成了多个核心功能迭代: 1. 新增用户每日签到系统,包含签到状态查询、连续天数统计、积分累计逻辑 2. 新增"新帖子"排序选项,替换原有的latest排序为新评论排序 3. 重构首页信息流布局,优化移动端与桌面端展示样式 4. 新增最新评论、最新注册用户模块,完善社区面板数据展示 5. 新增静态友情链接配置模块 6. 提取通用时间格式化工具函数,统一全站相对时间展示 7. 补充数据库签到模型与后端API接口,完善前后端交互链路
270 lines
7.7 KiB
Go
270 lines
7.7 KiB
Go
package service
|
||
|
||
import (
|
||
"strings"
|
||
"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"`
|
||
}
|
||
|
||
// LatestCommentItem 首页"最新评论"条目(纯文本摘要 + 所属帖子)
|
||
type LatestCommentItem struct {
|
||
ID uint `json:"id"`
|
||
Content string `json:"content"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
User struct {
|
||
ID uint `json:"id"`
|
||
Username string `json:"username"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
} `json:"user"`
|
||
Post struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
} `json:"post"`
|
||
}
|
||
|
||
// 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"`
|
||
LatestComments []LatestCommentItem `json:"latest_comments"`
|
||
NewUsers []NewUserItem `json:"new_users"`
|
||
Checkin *CheckinStatus `json:"checkin,omitempty"`
|
||
}
|
||
|
||
// truncateText 折叠空白并按 rune 截断(评论为纯文本,无 HTML 注入面)
|
||
func truncateText(s string, n int) string {
|
||
s = strings.Join(strings.Fields(s), " ")
|
||
r := []rune(s)
|
||
if len(r) > n {
|
||
return string(r[:n]) + "…"
|
||
}
|
||
return s
|
||
}
|
||
|
||
const activeUserWindow = 14 * 24 * time.Hour
|
||
|
||
// Get 聚合首页数据。hot 的 liked 态由 handler 层按当前用户填充。
|
||
func (s *OverviewService) Get() (*OverviewData, error) {
|
||
data := &OverviewData{
|
||
Hot: []PostListItem{},
|
||
ActiveUsers: []ActiveUser{},
|
||
Boards: []BoardCount{},
|
||
LatestComments: []LatestCommentItem{},
|
||
NewUsers: []NewUserItem{},
|
||
}
|
||
|
||
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]})
|
||
}
|
||
|
||
// 最新评论:JOIN 未封禁用户与已发布帖,一条 SQL 取齐
|
||
type latestCommentRow struct {
|
||
ID uint
|
||
Content string
|
||
CreatedAt time.Time
|
||
UID uint
|
||
Username string
|
||
Nickname string
|
||
Avatar string
|
||
PID uint
|
||
Title string
|
||
}
|
||
var commentRows []latestCommentRow
|
||
commentSQL := `
|
||
SELECT c.id, c.content, c.created_at,
|
||
u.id AS uid, u.username AS username, u.nickname AS nickname, u.avatar AS avatar,
|
||
p.id AS pid, p.title AS title
|
||
FROM comments c
|
||
JOIN users u ON u.id = c.user_id AND u.banned = ? AND u.deleted_at IS NULL
|
||
JOIN posts p ON p.id = c.post_id AND p.status = ? AND p.deleted_at IS NULL
|
||
WHERE c.status = ? AND c.deleted_at IS NULL
|
||
ORDER BY c.created_at DESC
|
||
LIMIT 6`
|
||
if err := s.db.Raw(commentSQL,
|
||
false, model.ContentStatusPublished, model.ContentStatusPublished).
|
||
Scan(&commentRows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, r := range commentRows {
|
||
item := LatestCommentItem{
|
||
ID: r.ID,
|
||
Content: truncateText(r.Content, 60),
|
||
CreatedAt: r.CreatedAt,
|
||
}
|
||
item.User.ID = r.UID
|
||
item.User.Username = r.Username
|
||
item.User.Nickname = r.Nickname
|
||
item.User.Avatar = r.Avatar
|
||
item.Post.ID = r.PID
|
||
item.Post.Title = r.Title
|
||
data.LatestComments = append(data.LatestComments, item)
|
||
}
|
||
|
||
// 最新注册成员
|
||
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
|
||
}
|