fix(dashboard): 按订单链路修正用户消费成本统计
将玩家消费排行榜和用户消费详情中的产出成本口径从 user_inventory 快照值改为订单抽奖链路成本。 排行榜按 user_id + 活动分类聚合,详情按 user_id + activity_id 聚合,成本统一基于已支付订单对应的 draw log、奖励配置、商品成本价、掉落数量和翻倍道具卡补量规则计算,使列表、详情与活动盈亏页的业务口径保持一致。
This commit is contained in:
parent
c927f46cdd
commit
25dacc066a
@ -123,44 +123,44 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
}
|
||||
|
||||
if err := query.Select(`
|
||||
orders.user_id,
|
||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END) as total_amount,
|
||||
COUNT(orders.id) as order_count,
|
||||
SUM(orders.discount_amount) as total_discount,
|
||||
SUM(orders.points_amount) as total_points,
|
||||
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
|
||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE 0
|
||||
END) as game_pass_spending,
|
||||
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
|
||||
SUM(CASE WHEN oa.category_id = 1 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
orders.user_id,
|
||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as ichiban_spending,
|
||||
SUM(CASE WHEN oa.category_id = 1 THEN 1 ELSE 0 END) as ichiban_count,
|
||||
SUM(CASE WHEN oa.category_id = 2 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
END) as total_amount,
|
||||
COUNT(orders.id) as order_count,
|
||||
SUM(orders.discount_amount) as total_discount,
|
||||
SUM(orders.points_amount) as total_points,
|
||||
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
|
||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as infinite_spending,
|
||||
SUM(CASE WHEN oa.category_id = 2 THEN 1 ELSE 0 END) as infinite_count,
|
||||
SUM(CASE WHEN oa.category_id = 3 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as matching_spending,
|
||||
SUM(CASE WHEN oa.category_id = 3 THEN 1 ELSE 0 END) as matching_count,
|
||||
0 as livestream_spending,
|
||||
0 as livestream_count
|
||||
`).
|
||||
ELSE 0
|
||||
END) as game_pass_spending,
|
||||
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
|
||||
SUM(CASE WHEN oa.category_id = 1 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as ichiban_spending,
|
||||
SUM(CASE WHEN oa.category_id = 1 THEN 1 ELSE 0 END) as ichiban_count,
|
||||
SUM(CASE WHEN oa.category_id = 2 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as infinite_spending,
|
||||
SUM(CASE WHEN oa.category_id = 2 THEN 1 ELSE 0 END) as infinite_count,
|
||||
SUM(CASE WHEN oa.category_id = 3 THEN
|
||||
CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(oa.price_draw * oa.draw_count, 0)
|
||||
ELSE orders.actual_amount
|
||||
END
|
||||
ELSE 0 END) as matching_spending,
|
||||
SUM(CASE WHEN oa.category_id = 3 THEN 1 ELSE 0 END) as matching_count,
|
||||
0 as livestream_spending,
|
||||
0 as livestream_count
|
||||
`).
|
||||
Group("orders.user_id").
|
||||
Order("total_amount DESC").
|
||||
Limit(100).
|
||||
@ -226,7 +226,6 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
|
||||
if len(userIDs) > 0 {
|
||||
// 3. Get User Info
|
||||
// Use h.readDB.Users (GEN) as it's simple
|
||||
users, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.In(userIDs...)).Find()
|
||||
for _, u := range users {
|
||||
if item, ok := statMap[u.ID]; ok {
|
||||
@ -235,58 +234,57 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Calculate Prize Value (Inventory)
|
||||
type invStat struct {
|
||||
UserID int64
|
||||
TotalValue int64
|
||||
IchibanPrize int64
|
||||
InfinitePrize int64
|
||||
MatchingPrize int64
|
||||
LivestreamPrize int64
|
||||
// 4. Calculate Prize Cost by user + category from order chain
|
||||
type costStat struct {
|
||||
UserID int64
|
||||
CategoryID int64
|
||||
TotalCost int64
|
||||
}
|
||||
var invStats []invStat
|
||||
var costStats []costStat
|
||||
|
||||
// Join with Products, Activities, and Orders (for livestream detection)
|
||||
query := db.Table(model.TableNameUserInventory).
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = user_inventory.reward_id").
|
||||
Joins("LEFT JOIN activity_issues AS cost_issues ON cost_issues.id = activity_reward_settings.issue_id").
|
||||
Joins("LEFT JOIN activities ON activities.id = COALESCE(NULLIF(user_inventory.activity_id, 0), cost_issues.activity_id)").
|
||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||||
Joins("LEFT JOIN products ON products.id = user_inventory.product_id").
|
||||
costQuery := db.Table(model.TableNameActivityDrawLogs).
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
Joins("JOIN activities ON activities.id = activity_issues.activity_id").
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
|
||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
||||
Where("user_inventory.user_id IN ?", userIDs)
|
||||
Where("orders.user_id IN ?", userIDs).
|
||||
Where("orders.status = ?", 2)
|
||||
|
||||
if req.RangeType != "all" {
|
||||
query = query.Where("user_inventory.created_at >= ?", start).
|
||||
Where("user_inventory.created_at <= ?", end)
|
||||
costQuery = costQuery.Where("orders.created_at >= ?", start).
|
||||
Where("orders.created_at <= ?", end)
|
||||
}
|
||||
// Only include Holding (1) and Shipped/Used (3) items. Exclude Void/Decomposed (2) and redeemed-to-points (3+redeemed).
|
||||
query = query.Where("user_inventory.status IN ?", []int{1, 3}).
|
||||
Where("COALESCE(user_inventory.remark, '') NOT LIKE ?", "%void%").
|
||||
Where("NOT (user_inventory.status = 3 AND (COALESCE(user_inventory.remark, '') LIKE ? OR COALESCE(user_inventory.remark, '') LIKE ?))", "%redeemed_points%", "%batch_redeemed%")
|
||||
|
||||
err := query.Select(`
|
||||
user_inventory.user_id,
|
||||
CAST(SUM(COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000) AS SIGNED) as total_value,
|
||||
CAST(SUM(CASE WHEN activities.activity_category_id = 1 THEN COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000 ELSE 0 END) AS SIGNED) as ichiban_prize,
|
||||
CAST(SUM(CASE WHEN activities.activity_category_id = 2 THEN COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000 ELSE 0 END) AS SIGNED) as infinite_prize,
|
||||
CAST(SUM(CASE WHEN activities.activity_category_id = 3 THEN COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000 ELSE 0 END) AS SIGNED) as matching_prize
|
||||
err := costQuery.Select(`
|
||||
orders.user_id as user_id,
|
||||
activities.activity_category_id as category_id,
|
||||
CAST(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
|
||||
)) AS SIGNED) as total_cost
|
||||
`).
|
||||
Group("user_inventory.user_id").
|
||||
Scan(&invStats).Error
|
||||
Group("orders.user_id, activities.activity_category_id").
|
||||
Scan(&costStats).Error
|
||||
|
||||
if err == nil {
|
||||
for _, is := range invStats {
|
||||
if item, ok := statMap[is.UserID]; ok {
|
||||
item.TotalPrizeValue = is.TotalValue
|
||||
item.IchibanPrize = is.IchibanPrize
|
||||
item.InfinitePrize = is.InfinitePrize
|
||||
item.MatchingPrize = is.MatchingPrize
|
||||
for _, cs := range costStats {
|
||||
if item, ok := statMap[cs.UserID]; ok {
|
||||
item.TotalPrizeValue += cs.TotalCost
|
||||
switch cs.CategoryID {
|
||||
case 1:
|
||||
item.IchibanPrize = cs.TotalCost
|
||||
case 2:
|
||||
item.InfinitePrize = cs.TotalCost
|
||||
case 3:
|
||||
item.MatchingPrize = cs.TotalCost
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
h.logger.Error(fmt.Sprintf("DashboardPlayerSpendingLeaderboard inventory cost stats error: %v", err))
|
||||
h.logger.Error(fmt.Sprintf("DashboardPlayerSpendingLeaderboard order-chain cost stats error: %v", err))
|
||||
}
|
||||
|
||||
// 4.1 Calculate Livestream Prize Value (snapshot priority; fallback prize cost)
|
||||
@ -387,6 +385,7 @@ func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
||||
for uid, amount := range livestreamPrizeByUser {
|
||||
if item, ok := statMap[uid]; ok {
|
||||
item.LivestreamPrize = amount
|
||||
item.TotalPrizeValue += amount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,16 +109,16 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc {
|
||||
}
|
||||
|
||||
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,
|
||||
CAST(ROUND(SUM(CASE
|
||||
WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(order_activity_draws.draw_count * activities.price_draw, 0)
|
||||
ELSE COALESCE(orders.actual_amount * order_activity_draws.draw_count / NULLIF(order_total_draws.total_count, 0), 0)
|
||||
END), 0) AS SIGNED) as spending,
|
||||
COUNT(DISTINCT orders.id) as order_count
|
||||
`).
|
||||
COALESCE(activities.id, 0) as activity_id,
|
||||
COALESCE(activities.name, '其他') as activity_name,
|
||||
COALESCE(activities.activity_category_id, 0) as category_id,
|
||||
CAST(ROUND(SUM(CASE
|
||||
WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' OR (orders.actual_amount = 0 AND orders.remark LIKE '%use_game_pass%')
|
||||
THEN COALESCE(order_activity_draws.draw_count * activities.price_draw, 0)
|
||||
ELSE COALESCE(orders.actual_amount * order_activity_draws.draw_count / NULLIF(order_total_draws.total_count, 0), 0)
|
||||
END), 0) AS SIGNED) as spending,
|
||||
COUNT(DISTINCT orders.id) as order_count
|
||||
`).
|
||||
Group("COALESCE(activities.id, 0)").
|
||||
Order("spending DESC").
|
||||
Scan(&actStats).Error; err != nil {
|
||||
@ -127,37 +127,37 @@ func (h *handler) GetUserSpendingDashboard() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 按活动实例统计产出价值
|
||||
// 2. 按用户 + 活动实例统计订单链路产出成本
|
||||
type prizeStat struct {
|
||||
ActivityID int64
|
||||
PrizeValue int64
|
||||
}
|
||||
var prizeStats []prizeStat
|
||||
|
||||
prizeQuery := db.Table(model.TableNameUserInventory).
|
||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = user_inventory.reward_id").
|
||||
Joins("LEFT JOIN activity_issues AS cost_issues ON cost_issues.id = activity_reward_settings.issue_id").
|
||||
Joins("LEFT JOIN products ON products.id = user_inventory.product_id").
|
||||
prizeQuery := db.Table(model.TableNameActivityDrawLogs).
|
||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
||||
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
|
||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
||||
Where("user_inventory.user_id = ?", userID).
|
||||
Where("user_inventory.status IN ?", []int{1, 3}).
|
||||
Where("COALESCE(user_inventory.remark, '') NOT LIKE ?", "%void%").
|
||||
Where("NOT (user_inventory.status = 3 AND (COALESCE(user_inventory.remark, '') LIKE ? OR COALESCE(user_inventory.remark, '') LIKE ?))", "%redeemed_points%", "%batch_redeemed%").
|
||||
Where("COALESCE(NULLIF(user_inventory.activity_id, 0), cost_issues.activity_id, 0) > 0")
|
||||
Where("orders.user_id = ?", userID).
|
||||
Where("orders.status = ?", 2)
|
||||
|
||||
if hasRange {
|
||||
prizeQuery = prizeQuery.Where("user_inventory.created_at >= ?", start).Where("user_inventory.created_at <= ?", end)
|
||||
prizeQuery = prizeQuery.Where("orders.created_at >= ?", start).Where("orders.created_at <= ?", end)
|
||||
}
|
||||
|
||||
if err := prizeQuery.Select(`
|
||||
COALESCE(NULLIF(user_inventory.activity_id, 0), cost_issues.activity_id, 0) as activity_id,
|
||||
CAST(SUM(COALESCE(NULLIF(user_inventory.value_cents, 0), activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000) AS SIGNED) as prize_value
|
||||
activity_issues.activity_id as activity_id,
|
||||
CAST(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
|
||||
)) AS SIGNED) as prize_value
|
||||
`).
|
||||
Group("COALESCE(NULLIF(user_inventory.activity_id, 0), cost_issues.activity_id, 0)").
|
||||
Group("activity_issues.activity_id").
|
||||
Scan(&prizeStats).Error; err != nil {
|
||||
h.logger.Error(fmt.Sprintf("GetUserSpendingDashboard prize stats error: %v", err))
|
||||
h.logger.Error(fmt.Sprintf("GetUserSpendingDashboard order-chain prize stats error: %v", err))
|
||||
}
|
||||
|
||||
prizeMap := make(map[int64]int64)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user