Files
backend/internal/service/admin_dashboard_service.go
lafay 90495385cd
All checks were successful
Build Backend / build (push) Successful in 4m24s
Build Backend / build-docker (push) Successful in 1m14s
feat(moderation): add user profile content moderation for avatars, covers, and bios
2026-04-15 11:50:47 +08:00

443 lines
14 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 (
"context"
"time"
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
)
// AdminDashboardService 管理端仪表盘服务接口
type AdminDashboardService interface {
// GetStats 获取统计数据
GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error)
// GetUserActivityTrend 获取用户活跃度趋势
GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error)
// GetContentStats 获取内容统计
GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error)
// GetPendingContent 获取待审核内容
GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error)
// GetOnlineUsers 获取在线用户数
GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error)
}
// adminDashboardServiceImpl 管理端仪表盘服务实现
type adminDashboardServiceImpl struct {
db *gorm.DB
userRepo repository.UserRepository
postRepo repository.PostRepository
commentRepo repository.CommentRepository
groupRepo repository.GroupRepository
activityRepo repository.UserActivityRepository
}
// NewAdminDashboardService 创建管理端仪表盘服务
func NewAdminDashboardService(
db *gorm.DB,
userRepo repository.UserRepository,
postRepo repository.PostRepository,
commentRepo repository.CommentRepository,
groupRepo repository.GroupRepository,
activityRepo repository.UserActivityRepository,
) AdminDashboardService {
return &adminDashboardServiceImpl{
db: db,
userRepo: userRepo,
postRepo: postRepo,
commentRepo: commentRepo,
groupRepo: groupRepo,
activityRepo: activityRepo,
}
}
// GetStats 获取统计数据
func (s *adminDashboardServiceImpl) GetStats(ctx context.Context) (*dto.DashboardStatsResponse, error) {
var stats dto.DashboardStatsResponse
// 获取当前时间
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
weekStart := todayStart.AddDate(0, 0, -int(now.Weekday()))
// 总用户数
if err := s.db.Model(&model.User{}).Count(&stats.TotalUsers).Error; err != nil {
return nil, err
}
// 今日新增用户
if err := s.db.Model(&model.User{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewUsers).Error; err != nil {
return nil, err
}
// 今日活跃用户
todayActive, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果出错,尝试从数据库获取
stats.ActiveUsersToday = 0
} else {
stats.ActiveUsersToday = todayActive
}
// 本周活跃用户(使用 WAU
year, week := now.ISOWeek()
weekActive, err := s.activityRepo.GetWAUFromRedis(ctx, year, week)
if err != nil {
// 如果 Redis 获取失败,从数据库计算
var weekActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", weekStart).
Distinct("user_id").
Count(&weekActiveCount).Error
if err == nil {
stats.ActiveUsersWeek = weekActiveCount
}
} else {
stats.ActiveUsersWeek = weekActive
}
// 本月活跃用户(使用 MAU
monthActive, err := s.activityRepo.GetMAUFromRedis(ctx, now.Year(), int(now.Month()))
if err != nil {
// 如果 Redis 获取失败,从数据库计算
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
var monthActiveCount int64
err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", monthStart).
Distinct("user_id").
Count(&monthActiveCount).Error
if err == nil {
stats.ActiveUsersMonth = monthActiveCount
}
} else {
stats.ActiveUsersMonth = monthActive
}
// 获取留存率数据(从统计快照表获取)
retentionStats := s.getRetentionStats(ctx, now)
stats.Retention1Day = retentionStats.Retention1Day
stats.Retention7Day = retentionStats.Retention7Day
stats.Retention30Day = retentionStats.Retention30Day
// 总帖子数
if err := s.db.Model(&model.Post{}).Count(&stats.TotalPosts).Error; err != nil {
return nil, err
}
// 今日帖子数
if err := s.db.Model(&model.Post{}).Where("created_at >= ?", todayStart).Count(&stats.TodayPosts).Error; err != nil {
return nil, err
}
// 待审核帖子数
if err := s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&stats.PendingPosts).Error; err != nil {
return nil, err
}
// 总评论数
if err := s.db.Model(&model.Comment{}).Count(&stats.TotalComments).Error; err != nil {
return nil, err
}
// 今日评论数
if err := s.db.Model(&model.Comment{}).Where("created_at >= ?", todayStart).Count(&stats.TodayComments).Error; err != nil {
return nil, err
}
// 总群组数
if err := s.db.Model(&model.Group{}).Count(&stats.TotalGroups).Error; err != nil {
return nil, err
}
// 今日新建群组
if err := s.db.Model(&model.Group{}).Where("created_at >= ?", todayStart).Count(&stats.TodayNewGroups).Error; err != nil {
return nil, err
}
// 待审核评论数
var pendingComments int64
if err := s.db.Model(&model.Comment{}).Where("status = ?", model.CommentStatusPending).Count(&pendingComments).Error; err != nil {
return nil, err
}
stats.PendingComments = pendingComments
var pendingAvatars, pendingCovers, pendingBios int64
s.db.Model(&model.UserProfileAudit{}).Where("content_type = ? AND status = ?", model.UserProfileContentTypeAvatar, model.UserProfileAuditStatusPending).Count(&pendingAvatars)
s.db.Model(&model.UserProfileAudit{}).Where("content_type = ? AND status = ?", model.UserProfileContentTypeCover, model.UserProfileAuditStatusPending).Count(&pendingCovers)
s.db.Model(&model.UserProfileAudit{}).Where("content_type = ? AND status = ?", model.UserProfileContentTypeBio, model.UserProfileAuditStatusPending).Count(&pendingBios)
stats.PendingAvatars = pendingAvatars
stats.PendingCovers = pendingCovers
stats.PendingBios = pendingBios
// 计算待审核总数
stats.PendingReview = stats.PendingPosts + stats.PendingComments + stats.PendingAvatars + stats.PendingCovers + stats.PendingBios
stats.PendingPostsCount = stats.PendingPosts
return &stats, nil
}
// GetUserActivityTrend 获取用户活跃度趋势
func (s *adminDashboardServiceImpl) GetUserActivityTrend(ctx context.Context, days int) ([]dto.UserActivityTrendResponse, error) {
days = min(max(days, 1), 30)
now := time.Now()
results := make([]dto.UserActivityTrendResponse, days)
for i := days - 1; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
trend := dto.UserActivityTrendResponse{
Date: dateStart.Format("2006-01-02"),
}
// 新增用户数
var newUsers int64
if err := s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers).Error; err == nil {
trend.NewUsers = newUsers
}
// 活跃用户数
activeUsers, err := s.activityRepo.GetDAU(ctx, date)
if err == nil {
trend.ActiveUsers = activeUsers
}
// 帖子数
var posts int64
if err := s.db.Model(&model.Post{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&posts).Error; err == nil {
trend.Posts = posts
}
// 评论数
var comments int64
if err := s.db.Model(&model.Comment{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&comments).Error; err == nil {
trend.Comments = comments
}
results[days-1-i] = trend
}
return results, nil
}
// GetContentStats 获取内容统计
func (s *adminDashboardServiceImpl) GetContentStats(ctx context.Context) (*dto.ContentStatsResponse, error) {
var response dto.ContentStatsResponse
// 内容分布
var totalPosts, totalComments, totalGroups int64
s.db.Model(&model.Post{}).Count(&totalPosts)
s.db.Model(&model.Comment{}).Count(&totalComments)
s.db.Model(&model.Group{}).Count(&totalGroups)
response.ContentDistribution = []dto.ContentDistributionItem{
{Type: "posts", Name: "帖子", Count: totalPosts},
{Type: "comments", Name: "评论", Count: totalComments},
{Type: "groups", Name: "群组", Count: totalGroups},
}
// 帖子状态分布
var publishedCount, pendingCount, rejectedCount, draftCount int64
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).Count(&publishedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusPending).Count(&pendingCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusRejected).Count(&rejectedCount)
s.db.Model(&model.Post{}).Where("status = ?", model.PostStatusDraft).Count(&draftCount)
response.PostStatusDistribution = []dto.PostStatusDistributionItem{
{Status: string(model.PostStatusPublished), Name: "已发布", Count: publishedCount},
{Status: string(model.PostStatusPending), Name: "待审核", Count: pendingCount},
{Status: string(model.PostStatusRejected), Name: "已拒绝", Count: rejectedCount},
{Status: string(model.PostStatusDraft), Name: "草稿", Count: draftCount},
}
// 用户增长趋势最近7天
now := time.Now()
response.UserGrowth = make([]dto.UserGrowthItem, 7)
for i := 6; i >= 0; i-- {
date := now.AddDate(0, 0, -i)
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
dateEnd := dateStart.Add(24 * time.Hour)
var newUsers int64
s.db.Model(&model.User{}).
Where("created_at >= ? AND created_at < ?", dateStart, dateEnd).
Count(&newUsers)
response.UserGrowth[6-i] = dto.UserGrowthItem{
Date: dateStart.Format("2006-01-02"),
NewUsers: newUsers,
}
}
return &response, nil
}
// GetPendingContent 获取待审核内容
func (s *adminDashboardServiceImpl) GetPendingContent(ctx context.Context, limit int) ([]dto.PendingContentResponse, error) {
limit = min(max(limit, 1), 50)
results := make([]dto.PendingContentResponse, 0)
// 获取待审核帖子
var posts []*model.Post
if err := s.db.Preload("User").
Where("status = ?", model.PostStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&posts).Error; err != nil {
return nil, err
}
for _, post := range posts {
item := dto.PendingContentResponse{
ID: post.ID,
Type: "post",
Title: post.Title,
Content: post.Content,
CreatedAt: post.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if post.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: post.User.ID,
Username: post.User.Username,
Nickname: post.User.Nickname,
Avatar: post.User.Avatar,
}
}
results = append(results, item)
}
// 获取待审核评论
var comments []*model.Comment
if err := s.db.Preload("User").
Where("status = ?", model.CommentStatusPending).
Order("created_at DESC").
Limit(limit).
Find(&comments).Error; err != nil {
return nil, err
}
for _, comment := range comments {
item := dto.PendingContentResponse{
ID: comment.ID,
Type: "comment",
Title: "",
Content: comment.Content,
CreatedAt: comment.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if comment.User != nil {
item.Author = &dto.PendingContentAuthor{
ID: comment.User.ID,
Username: comment.User.Username,
Nickname: comment.User.Nickname,
Avatar: comment.User.Avatar,
}
}
results = append(results, item)
}
// 按时间排序并限制数量
if len(results) > limit {
results = results[:limit]
}
return results, nil
}
// GetOnlineUsers 获取在线用户数
func (s *adminDashboardServiceImpl) GetOnlineUsers(ctx context.Context) (*dto.OnlineUsersResponse, error) {
// 获取当前日期的活跃用户数作为在线用户数的近似值
// 实际应用中可能需要使用 WebSocket 连接数或 Redis 中的在线状态
now := time.Now()
count, err := s.activityRepo.GetDAU(ctx, now)
if err != nil {
// 如果 Redis 获取失败,从数据库获取
var dbCount int64
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
if err := s.db.Model(&model.UserActiveLog{}).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&dbCount).Error; err != nil {
return nil, err
}
count = dbCount
}
return &dto.OnlineUsersResponse{
Count: count,
}, nil
}
// retentionStatsResult 留存率结果
type retentionStatsResult struct {
Retention1Day int64
Retention7Day int64
Retention30Day int64
}
// getRetentionStats 获取留存率统计数据
func (s *adminDashboardServiceImpl) getRetentionStats(ctx context.Context, now time.Time) retentionStatsResult {
var result retentionStatsResult
// 尝试从统计快照表获取昨日的留存率数据
yesterday := now.AddDate(0, 0, -1)
if stat, err := s.activityRepo.GetStat(ctx, "dau", yesterday); err == nil {
result.Retention1Day = int64(stat.Retention1)
result.Retention7Day = int64(stat.Retention7)
result.Retention30Day = int64(stat.Retention30)
return result
}
// 如果快照表没有数据,实时计算留存率
// 次日留存率:昨日新增用户中今日仍活跃的比例
result.Retention1Day = s.calculateRetention(ctx, 1)
result.Retention7Day = s.calculateRetention(ctx, 7)
result.Retention30Day = s.calculateRetention(ctx, 30)
return result
}
// calculateRetention 计算指定天数的留存率
func (s *adminDashboardServiceImpl) calculateRetention(ctx context.Context, days int) int64 {
now := time.Now()
// 获取days天前的新增用户
targetDate := now.AddDate(0, 0, -days)
targetDateStart := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), 0, 0, 0, 0, targetDate.Location())
targetDateEnd := targetDateStart.Add(24 * time.Hour)
var newUsers []string
if err := s.db.Model(&model.User{}).
Select("id").
Where("created_at >= ? AND created_at < ?", targetDateStart, targetDateEnd).
Pluck("id", &newUsers).Error; err != nil || len(newUsers) == 0 {
return 0
}
// 检查这些用户在今日是否活跃
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
var activeCount int64
if err := s.db.Model(&model.UserActiveLog{}).
Where("user_id IN ?", newUsers).
Where("active_date >= ?", todayStart).
Distinct("user_id").
Count(&activeCount).Error; err != nil {
return 0
}
// 计算留存率(百分比)
retention := float64(activeCount) / float64(len(newUsers)) * 100
return int64(retention)
}