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 // 计算待审核总数 stats.PendingReview = stats.PendingPosts + stats.PendingComments 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) }