From 0e202fabd863bc4f705c1f65e93b68dc9bbe02fa Mon Sep 17 00:00:00 2001 From: Zuncle <34310384@qq.com> Date: Sat, 18 Apr 2026 01:15:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E7=BB=9F=E4=B8=80=E7=8E=A9?= =?UTF-8?q?=E5=AE=B6=E7=9B=88=E4=BA=8F=E5=88=86=E6=9E=90=E4=BA=A7=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将玩家盈亏趋势中的商品产出从当前资产快照估值, 调整为按用户、订单、抽奖日志链路聚合的商品成本口径。 这样可使商品产出与消费看板中的活动产出统计保持一致, 避免同一用户在两个面板中看到不同的商品产出口径。 同时保留积分、道具卡、优惠券三项当前分项展示, 避免接口结构调整后页面字段缺失或被误显示为 0。 --- internal/api/admin/users_profit_loss.go | 106 +++++++++++++++++------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/internal/api/admin/users_profit_loss.go b/internal/api/admin/users_profit_loss.go index dd002f1..f12f1bf 100755 --- a/internal/api/admin/users_profit_loss.go +++ b/internal/api/admin/users_profit_loss.go @@ -19,7 +19,7 @@ type userProfitLossRequest struct { type userProfitLossPoint struct { Date string `json:"date"` Cost int64 `json:"cost"` // 累计投入(已支付-已退款) - Value int64 `json:"value"` // 累计产出(当前资产快照) + Value int64 `json:"value"` // 累计产出(订单链路商品成本) Profit int64 `json:"profit"` // 净盈亏 Ratio float64 `json:"ratio"` // 盈亏比 Breakdown struct { @@ -79,7 +79,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc { return } - // --- 1. 获取当前资产快照(实时余额)--- + // --- 1. 获取当前资产快照(用于积分/道具卡/优惠券分项展示)--- var curAssets struct { Points int64 Products int64 @@ -87,24 +87,46 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc { 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 CAST(COALESCE(SUM( - COALESCE(NULLIF(ui.value_cents, 0), ars.price_snapshot_cents, p.price, 0) - * GREATEST(COALESCE(sic.reward_multiplier_x1000, 1000), 1000) / 1000 - ), 0) AS SIGNED) - FROM user_inventory ui - LEFT JOIN products p ON p.id = ui.product_id - LEFT JOIN activity_reward_settings ars ON ars.id = ui.reward_id - LEFT JOIN orders o ON o.id = ui.order_id - LEFT JOIN user_item_cards uic ON uic.id = o.item_card_id - LEFT JOIN system_item_cards sic ON sic.id = uic.card_id - WHERE ui.user_id = ? AND ui.status IN (1, 3) - AND COALESCE(ui.remark, '') NOT LIKE '%%void%%' - AND NOT (ui.status = 3 AND (COALESCE(ui.remark, '') LIKE '%%redeemed_points%%' OR COALESCE(ui.remark, '') LIKE '%%batch_redeemed%%')) - `, 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. 获取订单链路商品产出(与 spending 保持一致,仅统计普通活动)--- + type prizeRow struct { + CreatedAt time.Time + PrizeValue int64 + } + var basePrizeValue int64 = 0 + _ = h.repo.GetDbR().Raw(` + SELECT CAST(COALESCE(SUM(COALESCE(products.cost_price, 0) * ( + COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) + + CASE WHEN user_item_cards.used_draw_log_id = activity_draw_logs.id AND system_item_cards.effect_type = 1 AND system_item_cards.reward_multiplier_x1000 >= 2000 THEN 1 ELSE 0 END + )), 0) AS SIGNED) as prize_value + FROM activity_draw_logs + JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id + LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id + LEFT JOIN products ON products.id = activity_reward_settings.product_id + LEFT JOIN orders ON orders.id = activity_draw_logs.order_id + LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id + LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id + WHERE orders.user_id = ? AND orders.status = 2 AND orders.created_at < ? + `, userID, start).Scan(&basePrizeValue).Error + + var prizeRows []prizeRow + _ = h.repo.GetDbR().Raw(` + SELECT orders.created_at, + CAST(COALESCE(products.cost_price, 0) * ( + COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) + + CASE WHEN user_item_cards.used_draw_log_id = activity_draw_logs.id AND system_item_cards.effect_type = 1 AND system_item_cards.reward_multiplier_x1000 >= 2000 THEN 1 ELSE 0 END + ) AS SIGNED) as prize_value + FROM activity_draw_logs + JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id + LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id + LEFT JOIN products ON products.id = activity_reward_settings.product_id + LEFT JOIN orders ON orders.id = activity_draw_logs.order_id + LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id + LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id + WHERE orders.user_id = ? AND orders.status = 2 AND orders.created_at BETWEEN ? AND ? + `, userID, start, end).Scan(&prizeRows).Error // --- 2. 获取订单数据(仅 status=2 已支付) --- // 注意:为了计算累计趋势,我们需要获取 start 之前的所有已支付订单总额作为基数 @@ -183,6 +205,7 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc { } cumulativeCost := baseCost + cumulativeValue := basePrizeValue for i, b := range buckets { p := &list[i] @@ -207,13 +230,19 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc { } p.Cost = cumulativeCost - // 产出值:当前资产是一个存量值。 - // 理想逻辑是回溯各时间点的余额,简化逻辑下: - // 如果该点还没有在该范围内发生过任何投入(且没有基数),则显示0;否则显示当前快照值。 - // 这里我们统一显示当前快照,但在前端图表上它会是一条水平线或阶梯线。 - p.Value = totalAssetValue + var periodValueDelta int64 = 0 + for _, prize := range prizeRows { + if inBucket(prize.CreatedAt, b) { + periodValueDelta += prize.PrizeValue + } + } + cumulativeValue += periodValueDelta + if cumulativeValue < 0 { + cumulativeValue = 0 + } + p.Value = cumulativeValue + p.Breakdown.Products = cumulativeValue p.Breakdown.Points = curAssets.Points - p.Breakdown.Products = curAssets.Products p.Breakdown.Cards = curAssets.Cards p.Breakdown.Coupons = curAssets.Coupons @@ -257,24 +286,39 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc { finalNetCost = 0 } + var totalValue int64 = 0 + _ = h.repo.GetDbR().Raw(` + SELECT CAST(COALESCE(SUM(COALESCE(products.cost_price, 0) * ( + COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) + + CASE WHEN user_item_cards.used_draw_log_id = activity_draw_logs.id AND system_item_cards.effect_type = 1 AND system_item_cards.reward_multiplier_x1000 >= 2000 THEN 1 ELSE 0 END + )), 0) AS SIGNED) as prize_value + FROM activity_draw_logs + JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id + LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id + LEFT JOIN products ON products.id = activity_reward_settings.product_id + LEFT JOIN orders ON orders.id = activity_draw_logs.order_id + LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id + LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id + WHERE orders.user_id = ? AND orders.status = 2 + `, userID).Scan(&totalValue).Error + resp := userProfitLossResponse{ Granularity: gran, List: list, } resp.Summary.TotalCost = finalNetCost - resp.Summary.TotalValue = totalAssetValue - resp.Summary.TotalProfit = finalNetCost - totalAssetValue - if totalAssetValue > 0 { - resp.Summary.AvgRatio = float64(finalNetCost) / float64(totalAssetValue) + resp.Summary.TotalValue = totalValue + resp.Summary.TotalProfit = finalNetCost - totalValue + if totalValue > 0 { + resp.Summary.AvgRatio = float64(finalNetCost) / float64(totalValue) } else if finalNetCost > 0 { resp.Summary.AvgRatio = 99.9 } - resp.CurrentAssets.Points = curAssets.Points - resp.CurrentAssets.Products = curAssets.Products + resp.CurrentAssets.Products = totalValue resp.CurrentAssets.Cards = curAssets.Cards resp.CurrentAssets.Coupons = curAssets.Coupons - resp.CurrentAssets.Total = totalAssetValue + resp.CurrentAssets.Total = totalValue + curAssets.Points + curAssets.Cards + curAssets.Coupons ctx.Payload(resp) }