Files
zt-closet-manage-backend/internal/mysqldao/stats.go
T
2026-09-11 01:47:20 +08:00

519 lines
14 KiB
Go

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
}