2026-03-14 18:01:55 +08:00
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
2026-03-26 18:14:16 +08:00
userRepo repository . UserRepository
postRepo repository . PostRepository
commentRepo repository . CommentRepository
2026-03-14 18:01:55 +08:00
groupRepo repository . GroupRepository
2026-03-26 18:14:16 +08:00
activityRepo repository . UserActivityRepository
2026-03-14 18:01:55 +08:00
}
// NewAdminDashboardService 创建管理端仪表盘服务
func NewAdminDashboardService (
db * gorm . DB ,
2026-03-26 18:14:16 +08:00
userRepo repository . UserRepository ,
postRepo repository . PostRepository ,
commentRepo repository . CommentRepository ,
2026-03-14 18:01:55 +08:00
groupRepo repository . GroupRepository ,
2026-03-26 18:14:16 +08:00
activityRepo repository . UserActivityRepository ,
2026-03-14 18:01:55 +08:00
) 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
2026-04-15 11:50:47 +08:00
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
2026-03-14 18:01:55 +08:00
// 计算待审核总数
2026-04-15 11:50:47 +08:00
stats . PendingReview = stats . PendingPosts + stats . PendingComments + stats . PendingAvatars + stats . PendingCovers + stats . PendingBios
2026-03-14 18:01:55 +08:00
stats . PendingPostsCount = stats . PendingPosts
return & stats , nil
}
// GetUserActivityTrend 获取用户活跃度趋势
func ( s * adminDashboardServiceImpl ) GetUserActivityTrend ( ctx context . Context , days int ) ( [ ] dto . UserActivityTrendResponse , error ) {
2026-03-30 04:49:35 +08:00
days = min ( max ( days , 1 ) , 30 )
2026-03-14 18:01:55 +08:00
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 ) {
2026-03-30 04:49:35 +08:00
limit = min ( max ( limit , 1 ) , 50 )
2026-03-14 18:01:55 +08:00
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 )
}