package admin import ( "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" ) type userProfitLossRequest struct { RangeType string `form:"rangeType"` Granularity string `form:"granularity"` } type userProfitLossPoint struct { Date string `json:"date"` Cost int64 `json:"cost"` // 净支出(仅已支付未退款订单) Value int64 `json:"value"` // 当前资产快照(实时) Profit int64 `json:"profit"` // 净盈亏 Ratio float64 `json:"ratio"` // 盈亏比 Breakdown struct { Products int64 `json:"products"` Points int64 `json:"points"` Cards int64 `json:"cards"` Coupons int64 `json:"coupons"` } `json:"breakdown"` } type userProfitLossResponse struct { Granularity string `json:"granularity"` List []userProfitLossPoint `json:"list"` CurrentAssets struct { Points int64 `json:"points"` Products int64 `json:"products"` Cards int64 `json:"cards"` Coupons int64 `json:"coupons"` Total int64 `json:"total"` } `json:"currentAssets"` } func (h *handler) GetUserProfitLossTrend() 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(userProfitLossRequest) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } start, end := parseRange(req.RangeType, "", "") if req.RangeType == "all" { u, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First() if u != nil { start = u.CreatedAt } else { start = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local) } } gran := normalizeGranularity(req.Granularity) buckets := buildBuckets(start, end, gran) if len(buckets) > 1500 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "范围过大")) return } // --- 1. 获取当前资产快照(实时余额)--- var curAssets struct { Points int64 Products int64 Cards int64 Coupons int64 } _ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(points), 0) FROM user_points WHERE user_id = ? AND (valid_end IS NULL OR valid_end > NOW())", userID).Scan(&curAssets.Points).Error _ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(p.price), 0) FROM user_inventory ui LEFT JOIN products p ON p.id = ui.product_id WHERE ui.user_id = ? AND ui.status = 1", userID).Scan(&curAssets.Products).Error _ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(sc.price), 0) FROM user_item_cards uic LEFT JOIN system_item_cards sc ON sc.id = uic.card_id WHERE uic.user_id = ? AND uic.status = 1", userID).Scan(&curAssets.Cards).Error _ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(balance_amount), 0) FROM user_coupons WHERE user_id = ? AND status = 1", userID).Scan(&curAssets.Coupons).Error totalAssetValue := curAssets.Points + curAssets.Products + curAssets.Cards + curAssets.Coupons // --- 2. 获取订单数据(仅 status=2 已支付未退款)--- orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB(). Where(h.readDB.Orders.UserID.Eq(userID)). Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款 Where(h.readDB.Orders.CreatedAt.Gte(start)). Where(h.readDB.Orders.CreatedAt.Lte(end)). Find() // --- 3. 按时间分桶计算 --- list := make([]userProfitLossPoint, len(buckets)) inBucket := func(t time.Time, b bucket) bool { return (t.After(b.Start) || t.Equal(b.Start)) && t.Before(b.End) } var cumulativeCost int64 = 0 for i, b := range buckets { p := &list[i] p.Date = b.Label // 计算该时间段内的支出 var periodCost int64 = 0 for _, o := range orderRows { if inBucket(o.CreatedAt, b) { periodCost += o.ActualAmount } } cumulativeCost += periodCost p.Cost = periodCost // 使用当前资产快照作为产出值(最后一个桶显示完整值,其他桶按比例或显示0) // 简化:所有桶都显示当前快照值,让用户一眼看到当前状态 p.Value = totalAssetValue p.Breakdown.Points = curAssets.Points p.Breakdown.Products = curAssets.Products p.Breakdown.Cards = curAssets.Cards p.Breakdown.Coupons = curAssets.Coupons p.Profit = p.Value - p.Cost if p.Cost > 0 { p.Ratio = float64(p.Value) / float64(p.Cost) } else if p.Value > 0 { p.Ratio = 99.9 } } // 计算累计值用于汇总显示 var totalCost int64 = 0 for _, o := range orderRows { totalCost += o.ActualAmount } // 最后一个桶使用累计成本 if len(list) > 0 { lastIdx := len(list) - 1 // 汇总数据:使用累计成本和当前资产值 list[lastIdx].Cost = totalCost list[lastIdx].Value = totalAssetValue list[lastIdx].Profit = totalAssetValue - totalCost if totalCost > 0 { list[lastIdx].Ratio = float64(totalAssetValue) / float64(totalCost) } else if totalAssetValue > 0 { list[lastIdx].Ratio = 99.9 } } ctx.Payload(userProfitLossResponse{ Granularity: gran, List: list, CurrentAssets: struct { Points int64 `json:"points"` Products int64 `json:"products"` Cards int64 `json:"cards"` Coupons int64 `json:"coupons"` Total int64 `json:"total"` }{ Points: curAssets.Points, Products: curAssets.Products, Cards: curAssets.Cards, Coupons: curAssets.Coupons, Total: totalAssetValue, }, }) } }