443 lines
14 KiB
Go
443 lines
14 KiB
Go
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)
|
||
}
|