package mysqldao import ( "context" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel" "gorm.io/gorm" ) // StatsDao 仪表盘聚合统计数据访问 type StatsDao struct { db *gorm.DB } // NewStatsDao 创建统计 DAO func NewStatsDao() *StatsDao { return &StatsDao{db: global.MysqlEngine} } // UserStats 用户统计。 type UserStats struct { Total int64 // 用户总数 TodayNew int64 // 今日新增 Normal int64 // 正常 TempBan int64 // 临时禁用 PermBan int64 // 永久禁用 Deactivated int64 // 已注销 } // OrderStats 订单统计。 type OrderStats struct { Total int64 // 订单总数 Pending int64 // 待支付 Paid int64 // 已支付 Cancelled int64 // 已取消 Expired int64 // 已过期 Refunded int64 // 已退款 } // RevenueStats 营收统计(单位分)。 type RevenueStats struct { TotalRevenue int64 // 累计营收(已支付+已退款订单实付金额) TodayRevenue int64 // 今日营收(今日支付订单实付金额) RefundTotal int64 // 累计退款金额 } // TodoStats 待办统计。 type TodoStats struct { RefundPending int64 // 待处理退款数 OutfitPendingAudit int64 // 待审核穿搭数 GoodsOnShelf int64 // 上架商品数 } // UserStatsGet 聚合用户总数与今日新增 func (d *StatsDao) UserStatsGet(ctx context.Context, todayStart int64) (*UserStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserStatsGet") defer span.End() s := &UserStats{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).Count(&s.Total).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).Where("created_at >= ?", todayStart).Count(&s.TodayNew).Error; err != nil { span.RecordError(err) return nil, err } return s, nil } // UserStatsGetByStatus 按状态统计用户数(单次 group by) func (d *StatsDao) UserStatsGetByStatus(ctx context.Context) (map[int8]int64, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserStatsGetByStatus") defer span.End() rows := []struct { Status int8 Cnt int64 }{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Select("status, COUNT(*) AS cnt"). Group("status"). Scan(&rows).Error; err != nil { span.RecordError(err) return nil, err } m := make(map[int8]int64, len(rows)) for _, r := range rows { m[r.Status] = r.Cnt } return m, nil } // OrderStatsGet 聚合订单状态统计(单次 group by) func (d *StatsDao) OrderStatsGet(ctx context.Context) (*OrderStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderStatsGet") defer span.End() rows := []struct { OrderStatus int8 Cnt int64 }{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Select("order_status, COUNT(*) AS cnt"). Group("order_status"). Scan(&rows).Error; err != nil { span.RecordError(err) return nil, err } s := &OrderStats{} for _, r := range rows { s.Total += r.Cnt switch r.OrderStatus { case 1: s.Pending = r.Cnt case 2: s.Paid = r.Cnt case 3: s.Cancelled = r.Cnt case 4: s.Expired = r.Cnt case 5: s.Refunded = r.Cnt } } return s, nil } // RevenueStatsGet 聚合营收统计(已支付与已退款订单均计营收;今日按支付时间统计) func (d *StatsDao) RevenueStatsGet(ctx context.Context, todayStart int64) (*RevenueStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.RevenueStatsGet") defer span.End() s := &RevenueStats{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_status IN ?", []int8{2, 5}). Select("COALESCE(SUM(pay_amount), 0)"). Scan(&s.TotalRevenue).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_status IN ? AND pay_time >= ?", []int8{2, 5}, todayStart). Select("COALESCE(SUM(pay_amount), 0)"). Scan(&s.TodayRevenue).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_status = ?", int8(5)). Select("COALESCE(SUM(refund_amount), 0)"). Scan(&s.RefundTotal).Error; err != nil { span.RecordError(err) return nil, err } return s, nil } // TodoStatsGet 聚合待办统计 func (d *StatsDao) TodoStatsGet(ctx context.Context) (*TodoStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.TodoStatsGet") defer span.End() s := &TodoStats{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{}). Where("status = ?", int8(0)). Count(&s.RefundPending).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}). Where("is_public = ? AND audit_status = ?", int8(2), int8(1)). Count(&s.OutfitPendingAudit).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}). Where("status = ?", int8(1)). Count(&s.GoodsOnShelf).Error; err != nil { span.RecordError(err) return nil, err } return s, nil } // countByInt8 通用分组计数行(小整型枚举 + 计数)。 type countByInt8 struct { K int8 Cnt int64 } // dateCount 按日分组计数行。 type dateCount struct { D string Cnt int64 } // dateRevenue 按日分组营收行。 type dateRevenue struct { D string Cnt int64 Rev int64 } // ContentStats 内容审核统计。 type ContentStats struct { OutfitTotal int64 OutfitPending int64 OutfitPassed int64 OutfitRejected int64 OutfitBanned int64 ClothesTotal int64 ClothesProcessing int64 ClothesSuccess int64 ClothesFailed int64 ShareTotal int64 ShareNormal int64 ShareCancelled int64 ShareExpired int64 ShareBanned int64 } // VIPStats 会员统计。 type VIPStats struct { MemberTotal int64 Monthly int64 Yearly int64 Trial int64 ExpiringSoon int64 } // GoodsTypeCount 商品类型数量。 type GoodsTypeCount struct { Type int8 Count int64 } // GoodsStats 商品统计。 type GoodsStats struct { Total int64 OnShelf int64 OffShelf int64 ByType []GoodsTypeCount } // TrendStats 趋势原始聚合(按 YYYY-MM-DD 分组)。 type TrendStats struct { NewUsers map[string]int64 // 新增用户 Orders map[string]int64 // 已支付订单量 Revenue map[string]int64 // 营收(分) } // ContentStatsGet 聚合内容审核概况:穿搭/衣物/衣柜分享。 func (d *StatsDao) ContentStatsGet(ctx context.Context) (*ContentStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.ContentStatsGet") defer span.End() s := &ContentStats{} // 穿搭:audit_status 分布 + 封禁数 outfitRows := []countByInt8{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}). Select("audit_status AS k, COUNT(*) AS cnt"). Group("audit_status"). Scan(&outfitRows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range outfitRows { s.OutfitTotal += r.Cnt switch r.K { case 1: s.OutfitPending = r.Cnt case 2: s.OutfitPassed = r.Cnt case 3: s.OutfitRejected = r.Cnt } } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}). Where("status = ?", int8(2)). Count(&s.OutfitBanned).Error; err != nil { span.RecordError(err) return nil, err } // 衣物:image_process_status 分布(1/2/3 处理中,4 成功,5 失败) clothesRows := []countByInt8{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{}). Select("image_process_status AS k, COUNT(*) AS cnt"). Group("image_process_status"). Scan(&clothesRows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range clothesRows { s.ClothesTotal += r.Cnt switch r.K { case 1, 2, 3: s.ClothesProcessing += r.Cnt case 4: s.ClothesSuccess = r.Cnt case 5: s.ClothesFailed = r.Cnt } } // 衣柜分享:status 分布 shareRows := []countByInt8{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.ClosetShare{}). Select("status AS k, COUNT(*) AS cnt"). Group("status"). Scan(&shareRows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range shareRows { s.ShareTotal += r.Cnt switch r.K { case 1: s.ShareNormal = r.Cnt case 2: s.ShareCancelled = r.Cnt case 3: s.ShareExpired = r.Cnt case 4: s.ShareBanned = r.Cnt } } return s, nil } // VIPStatsGet 聚合会员概况(有效会员 + 即将到期)。 func (d *StatsDao) VIPStatsGet(ctx context.Context, now, soonThreshold int64) (*VIPStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.VIPStatsGet") defer span.End() s := &VIPStats{} activeVIP := []int8{2, 3, 4} if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("vip_type IN ? AND vip_expire > ?", activeVIP, now). Count(&s.MemberTotal).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("vip_type = ? AND vip_expire > ?", int8(2), now). Count(&s.Monthly).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("vip_type = ? AND vip_expire > ?", int8(3), now). Count(&s.Yearly).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("vip_type = ? AND vip_expire > ?", int8(4), now). Count(&s.Trial).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("vip_type IN ? AND vip_expire >= ? AND vip_expire <= ?", activeVIP, now, soonThreshold). Count(&s.ExpiringSoon).Error; err != nil { span.RecordError(err) return nil, err } return s, nil } // GoodsStatsGet 聚合商品统计(上架/下架 + 类型分布)。 func (d *StatsDao) GoodsStatsGet(ctx context.Context) (*GoodsStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsStatsGet") defer span.End() s := &GoodsStats{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).Count(&s.Total).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}). Where("status = ?", int8(1)). Count(&s.OnShelf).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}). Where("status = ?", int8(2)). Count(&s.OffShelf).Error; err != nil { span.RecordError(err) return nil, err } rows := []countByInt8{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}). Select("goods_type AS k, COUNT(*) AS cnt"). Group("goods_type"). Scan(&rows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range rows { s.ByType = append(s.ByType, GoodsTypeCount{Type: r.K, Count: r.Cnt}) } return s, nil } // TrendStatsGet 聚合近 N 天趋势(新增用户/已支付订单量/营收,按天分组)。 func (d *StatsDao) TrendStatsGet(ctx context.Context, startTs int64) (*TrendStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.TrendStatsGet") defer span.End() s := &TrendStats{ NewUsers: map[string]int64{}, Orders: map[string]int64{}, Revenue: map[string]int64{}, } // 新增用户:按创建时间 userRows := []dateCount{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}). Where("created_at >= ?", startTs). Select("DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m-%d') AS d, COUNT(*) AS cnt"). Group("d"). Scan(&userRows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range userRows { s.NewUsers[r.D] = r.Cnt } // 已支付/已退款订单量 + 营收:按支付时间 orderRows := []dateRevenue{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}). Where("order_status IN ? AND pay_time >= ?", []int8{2, 5}, startTs). Select("DATE_FORMAT(FROM_UNIXTIME(pay_time), '%Y-%m-%d') AS d, COUNT(*) AS cnt, COALESCE(SUM(pay_amount), 0) AS rev"). Group("d"). Scan(&orderRows).Error; err != nil { span.RecordError(err) return nil, err } for _, r := range orderRows { s.Orders[r.D] = r.Cnt s.Revenue[r.D] = r.Rev } return s, nil } // UsageStats 今日消耗统计。 type UsageStats struct { AIRecommend int64 // 今日AI推荐消耗次数 ClosetShare int64 // 今日衣柜分享消耗次数 OutfitShare int64 // 今日社区分享消耗次数 } // SocialStats 内容互动统计。 type SocialStats struct { CosImageCount int64 // COS总图数 LikeCount int64 // 点赞数 CollectCount int64 // 收藏数 } // UsageStatsGet 聚合今日消耗次数(AI推荐/衣柜分享/社区分享,按 stat_date 单次求和)。 func (d *StatsDao) UsageStatsGet(ctx context.Context, statDate int64) (*UsageStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.UsageStatsGet") defer span.End() s := &UsageStats{} row := struct { AIRecommend int64 ClosetShare int64 OutfitShare int64 }{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}). Where("stat_date = ?", statDate). Select("COALESCE(SUM(ai_recommend_count), 0) AS ai_recommend, COALESCE(SUM(closet_share_count), 0) AS closet_share, COALESCE(SUM(outfit_share_count), 0) AS outfit_share"). Scan(&row).Error; err != nil { span.RecordError(err) return nil, err } s.AIRecommend = row.AIRecommend s.ClosetShare = row.ClosetShare s.OutfitShare = row.OutfitShare return s, nil } // SocialStatsGet 聚合内容互动统计(COS 总图数/点赞数/收藏数)。 func (d *StatsDao) SocialStatsGet(ctx context.Context) (*SocialStats, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.SocialStatsGet") defer span.End() s := &SocialStats{} if err := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{}). Where("processed_image <> ''"). Count(&s.CosImageCount).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Like{}). Where("status = ?", int8(1)). Count(&s.LikeCount).Error; err != nil { span.RecordError(err) return nil, err } if err := d.db.WithContext(ctx).Model(&mysqlmodel.Collect{}). Where("status = ?", int8(1)). Count(&s.CollectCount).Error; err != nil { span.RecordError(err) return nil, err } return s, nil }