From 25dacc066aa37dc7ddcfb078caa1fd9472e82953 Mon Sep 17 00:00:00 2001 From: Zuncle <34310384@qq.com> Date: Mon, 13 Apr 2026 19:23:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E6=8C=89=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E4=BF=AE=E6=AD=A3=E7=94=A8=E6=88=B7=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E6=88=90=E6=9C=AC=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将玩家消费排行榜和用户消费详情中的产出成本口径从 user_inventory 快照值改为订单抽奖链路成本。 排行榜按 user_id + 活动分类聚合,详情按 user_id + activity_id 聚合,成本统一基于已支付订单对应的 draw log、奖励配置、商品成本价、掉落数量和翻倍道具卡补量规则计算,使列表、详情与活动盈亏页的业务口径保持一致。 --- internal/api/admin/dashboard_spending.go | 147 +++++++++--------- internal/api/admin/dashboard_user_spending.go | 52 +++---- 2 files changed, 99 insertions(+), 100 deletions(-) diff --git a/internal/api/admin/dashboard_spending.go b/internal/api/admin/dashboard_spending.go index a76d834..3529051 100755 --- a/internal/api/admin/dashboard_spending.go +++ b/internal/api/admin/dashboard_spending.go @@ -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 } } } diff --git a/internal/api/admin/dashboard_user_spending.go b/internal/api/admin/dashboard_user_spending.go index f9bd16c..5da71b7 100755 --- a/internal/api/admin/dashboard_user_spending.go +++ b/internal/api/admin/dashboard_user_spending.go @@ -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)