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