first commit
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user