package admin import ( "fmt" "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" ) type userSpendingRequest struct { RangeType string `form:"rangeType"` StartDate string `form:"start"` EndDate string `form:"end"` } // userActivitySpending 用户在具体活动实例上的消费统计 type userActivitySpending struct { ActivityID int64 `json:"activity_id"` ActivityName string `json:"activity_name"` CategoryID int64 `json:"category_id"` CategoryName string `json:"category_name"` // 一番赏/盲盒/对对碰/直播间 Spending int64 `json:"spending"` // 消费金额(分) PrizeValue int64 `json:"prize_value"` // 产出价值(分) Profit int64 `json:"profit"` // 收益(分) OrderCount int64 `json:"order_count"` // 订单数 } type userSpendingResponse struct { UserID int64 `json:"user_id"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` TotalSpend int64 `json:"total_spend"` TotalPrize int64 `json:"total_prize"` TotalProfit int64 `json:"total_profit"` TotalOrders int64 `json:"total_orders"` Activities []userActivitySpending `json:"activities"` } var categoryNames = map[int64]string{ 1: "一番赏", 2: "盲盒/无限", 3: "对对碰", } func (h *handler) GetUserSpendingDashboard() core.HandlerFunc { return func(ctx core.Context) { userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID")) return } req := new(userSpendingRequest) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } var start, end time.Time hasRange := req.RangeType != "" && req.RangeType != "all" if hasRange { start, end = parseRange(req.RangeType, req.StartDate, req.EndDate) } db := h.repo.GetDbR().WithContext(ctx.RequestContext()) rsp := &userSpendingResponse{UserID: userID} // 获取用户基本信息 user, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.Eq(userID)).First() if user != nil { rsp.Nickname = user.Nickname rsp.Avatar = user.Avatar } // 1. 按活动实例统计消费 type activityStat struct { ActivityID int64 ActivityName string CategoryID int64 Spending int64 OrderCount int64 } var actStats []activityStat query := db.Table(model.TableNameOrders). Joins("LEFT JOIN activity_draw_logs ON activity_draw_logs.order_id = orders.id"). Joins("LEFT JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id"). Joins("LEFT JOIN activities ON activities.id = activity_issues.activity_id"). Where("orders.user_id = ?", userID). Where("orders.status = ?", 2) if hasRange { query = query.Where("orders.created_at >= ?", start).Where("orders.created_at <= ?", end) } if err := query.Select(` COALESCE(activities.id, 0) as activity_id, COALESCE(activities.name, '其他') as activity_name, COALESCE(activities.activity_category_id, 0) as category_id, SUM(orders.total_amount) as spending, COUNT(DISTINCT orders.id) as order_count `). Group("COALESCE(activities.id, 0)"). Order("spending DESC"). Scan(&actStats).Error; err != nil { h.logger.Error(fmt.Sprintf("UserSpending SQL error: %v", err)) ctx.AbortWithError(core.Error(http.StatusBadRequest, 21030, err.Error())) return } // 2. 按活动实例统计产出价值 type prizeStat struct { ActivityID int64 PrizeValue int64 } var prizeStats []prizeStat prizeQuery := db.Table(model.TableNameUserInventory). Where("user_inventory.user_id = ?", userID). Where("user_inventory.status IN ?", []int{1, 3}). Where("user_inventory.remark NOT LIKE ?", "%void%") if hasRange { prizeQuery = prizeQuery.Where("user_inventory.created_at >= ?", start).Where("user_inventory.created_at <= ?", end) } prizeQuery.Select(` COALESCE(user_inventory.activity_id, 0) as activity_id, SUM(user_inventory.value_cents) as prize_value `). Group("COALESCE(user_inventory.activity_id, 0)"). Scan(&prizeStats) prizeMap := make(map[int64]int64) for _, p := range prizeStats { prizeMap[p.ActivityID] = p.PrizeValue } // 3. 直播间消费统计 type livestreamStat struct { ActivityID int64 ActivityName string Spending int64 OrderCount int64 } var lsStats []livestreamStat lsQuery := db.Table("douyin_orders"). Joins("LEFT JOIN livestream_activities ON livestream_activities.id = douyin_orders.livestream_activity_id"). Select(` COALESCE(douyin_orders.livestream_activity_id, 0) as activity_id, COALESCE(livestream_activities.name, '直播间') as activity_name, SUM(actual_pay_amount) as spending, COUNT(*) as order_count `). Where("CAST(local_user_id AS SIGNED) = ?", userID). Where("local_user_id != '' AND local_user_id != '0'") if hasRange { lsQuery = lsQuery.Where("douyin_orders.created_at >= ?", start).Where("douyin_orders.created_at <= ?", end) } lsQuery.Group("COALESCE(douyin_orders.livestream_activity_id, 0)").Scan(&lsStats) // 直播间产出 type lsPrizeStat struct { ActivityID int64 PrizeValue int64 } var lsPrizeStats []lsPrizeStat lsPrizeQuery := db.Table("livestream_draw_logs"). Joins("JOIN products ON products.id = livestream_draw_logs.product_id"). Select(` livestream_draw_logs.livestream_activity_id as activity_id, SUM(products.price) as prize_value `). Where("livestream_draw_logs.local_user_id = ?", userID). Where("livestream_draw_logs.is_refunded = 0"). Where("livestream_draw_logs.product_id > 0") if hasRange { lsPrizeQuery = lsPrizeQuery.Where("livestream_draw_logs.created_at >= ?", start).Where("livestream_draw_logs.created_at <= ?", end) } lsPrizeQuery.Group("livestream_draw_logs.livestream_activity_id").Scan(&lsPrizeStats) lsPrizeMap := make(map[int64]int64) for _, p := range lsPrizeStats { lsPrizeMap[p.ActivityID] = p.PrizeValue } // 4. 组装结果 activities := make([]userActivitySpending, 0) var totalSpend, totalPrize, totalOrders int64 for _, s := range actStats { prize := prizeMap[s.ActivityID] catName := categoryNames[s.CategoryID] if catName == "" { catName = "其他" } item := userActivitySpending{ ActivityID: s.ActivityID, ActivityName: s.ActivityName, CategoryID: s.CategoryID, CategoryName: catName, Spending: s.Spending, PrizeValue: prize, Profit: s.Spending - prize, OrderCount: s.OrderCount, } activities = append(activities, item) totalSpend += s.Spending totalPrize += prize totalOrders += s.OrderCount } // 追加直播间活动 for _, ls := range lsStats { prize := lsPrizeMap[ls.ActivityID] item := userActivitySpending{ ActivityID: ls.ActivityID + 100000, // 避免和普通活动 ID 冲突 ActivityName: ls.ActivityName, CategoryID: 4, CategoryName: "直播间", Spending: ls.Spending, PrizeValue: prize, Profit: ls.Spending - prize, OrderCount: ls.OrderCount, } activities = append(activities, item) totalSpend += ls.Spending totalPrize += prize totalOrders += ls.OrderCount } rsp.TotalSpend = totalSpend rsp.TotalPrize = totalPrize rsp.TotalProfit = totalSpend - totalPrize rsp.TotalOrders = totalOrders rsp.Activities = activities ctx.Payload(rsp) } }