fix(dashboard): 统一玩家盈亏分析产出口径

将玩家盈亏趋势中的商品产出从当前资产快照估值,
调整为按用户、订单、抽奖日志链路聚合的商品成本口径。

这样可使商品产出与消费看板中的活动产出统计保持一致,
避免同一用户在两个面板中看到不同的商品产出口径。

同时保留积分、道具卡、优惠券三项当前分项展示,
避免接口结构调整后页面字段缺失或被误显示为 0。
This commit is contained in:
Zuncle 2026-04-18 01:15:11 +08:00
parent 1cf57657ca
commit 0e202fabd8

View File

@ -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)
}