From e6e4214df4189ee485207f85f7a03038bb510b78 Mon Sep 17 00:00:00 2001 From: Zuncle <34310384@qq.com> Date: Fri, 24 Apr 2026 21:35:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(dashboard):=20=E6=8C=89=E6=9C=89=E6=95=88?= =?UTF-8?q?=E6=8A=BD=E5=A5=96=E5=8F=A3=E5=BE=84=E4=BF=AE=E6=AD=A3=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=8A=A8=E9=94=80=E6=8E=92=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次仅调整产品动销排行接口的后端统计逻辑,不改动活动盈亏分析页面与接口。 详细说明: - 将销量从 activity_draw_logs 总条数改为有效抽奖次数,仅统计 orders.status = 2 的抽奖记录,避免把退款、取消等无效抽奖一并计入。 - 将营收统一改为“有效抽奖次数 × 活动单抽价格(price_draw)”,不再使用总日志数乘单价,确保产品动销排行内部次数与营收口径一致。 - 将成本从 price_snapshot_cents / products.price 的近似标价口径改为 products.cost_price 成本价口径,并继续保留 drop_quantity 与倍数卡 multiplier 的影响,避免利润长期被标价成本压低。 - 将盈亏与利润率统一为 Revenue - Cost 与 Profit / Revenue,使产品动销排行内部的销量、营收、成本、盈亏逻辑自洽。 - 将贡献率从按次数占比调整为按营收占比,更符合“营收贡献率”的业务含义。 本次未处理内容: - 不修改活动盈亏分析的现有统计口径。 - 不修改前端卡片结构与活动盈亏分析页面。 已核对的业务结论: - 活动 103 的产品动销排行 4696 次是最近 30 天有效抽奖次数;活动盈亏分析 5960 次是全量历史有效抽奖次数,两边差异来自时间范围而非当前产品动销排行次数公式。 --- internal/api/admin/dashboard_admin.go | 81 ++++++++++++++++----------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/internal/api/admin/dashboard_admin.go b/internal/api/admin/dashboard_admin.go index 7f5f6e5..4ec72d8 100755 --- a/internal/api/admin/dashboard_admin.go +++ b/internal/api/admin/dashboard_admin.go @@ -1855,18 +1855,44 @@ type productPerformanceItem struct { func (h *handler) OperationsProductPerformance() core.HandlerFunc { return func(ctx core.Context) { s, e := parseRange(ctx.Request().URL.Query().Get("rangeType"), "", "") + db := h.repo.GetDbR().WithContext(ctx.RequestContext()) - // 按活动聚合抽奖数据 - type drawRow struct { - ActivityID int64 `gorm:"column:activity_id"` - Count int64 `gorm:"column:count"` - TotalCost int64 `gorm:"column:total_cost"` + type performanceRow struct { + ActivityID int64 `gorm:"column:activity_id"` + SalesCount int64 `gorm:"column:sales_count"` + PaymentCount int64 `gorm:"column:payment_count"` + GamePassCount int64 `gorm:"column:game_pass_count"` + TotalCost int64 `gorm:"column:total_cost"` + PriceDraw int64 `gorm:"column:price_draw"` + RevenueCents int64 `gorm:"column:revenue_cents"` + ContributionBase int64 `gorm:"column:contribution_base"` } - var rows []drawRow + var rows []performanceRow - // 统计抽奖日志,按活动分组,并计算奖品成本 - if err := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).UnderlyingDB(). + if err := db.Table(model.TableNameActivityDrawLogs). + Select(` + activity_issues.activity_id, + SUM(CASE WHEN orders.status = 2 AND ( + orders.source_type = 4 + OR orders.order_no LIKE 'GP%' + OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%') + ) THEN 1 ELSE 0 END) as game_pass_count, + SUM(CASE WHEN orders.status = 2 AND NOT ( + orders.source_type = 4 + OR orders.order_no LIKE 'GP%' + OR (orders.actual_amount = 0 AND COALESCE(orders.remark, '') LIKE '%use_game_pass%') + ) THEN 1 ELSE 0 END) as payment_count, + SUM(CASE WHEN orders.status = 2 THEN 1 ELSE 0 END) as sales_count, + COALESCE(MAX(activities.price_draw), 0) as price_draw, + SUM(CASE WHEN orders.status = 2 THEN COALESCE(activities.price_draw, 0) ELSE 0 END) as revenue_cents, + SUM(CASE WHEN orders.status = 2 THEN COALESCE(activities.price_draw, 0) ELSE 0 END) as contribution_base, + CAST(SUM(CASE WHEN orders.status = 2 THEN 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 + ) ELSE 0 END) AS SIGNED) as total_cost + `). 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"). @@ -1874,41 +1900,33 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc { Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id"). Where("activity_draw_logs.created_at >= ?", s). Where("activity_draw_logs.created_at <= ?", e). - Select( - "activity_issues.activity_id", - "COUNT(activity_draw_logs.id) as count", - "CAST(SUM(IF(activity_draw_logs.is_winner = 1, COALESCE(activity_reward_settings.price_snapshot_cents, products.price, 0) * GREATEST(COALESCE(system_item_cards.reward_multiplier_x1000, 1000), 1000) / 1000, 0)) AS SIGNED) as total_cost", - ). Group("activity_issues.activity_id"). - Order("count DESC"). + Order("sales_count DESC"). Limit(10). Scan(&rows).Error; err != nil { - h.logger.Error(fmt.Sprintf("OperationsProductPerformance draw cost stats error: %v", err)) + h.logger.Error(fmt.Sprintf("OperationsProductPerformance stats error: %v", err)) } - // 获取活动详情(名称和单价) activityIDs := make([]int64, len(rows)) for i, r := range rows { activityIDs[i] = r.ActivityID } type actInfo struct { - Name string - PriceDraw int64 + Name string } actMap := make(map[int64]actInfo) if len(activityIDs) > 0 { acts, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).ReadDB(). Where(h.readDB.Activities.ID.In(activityIDs...)).Find() for _, a := range acts { - actMap[a.ID] = actInfo{Name: a.Name, PriceDraw: a.PriceDraw} + actMap[a.ID] = actInfo{Name: a.Name} } } - // 计算总数用于贡献率 - var totalCount int64 + var totalRevenueCents int64 for _, r := range rows { - totalCount += r.Count + totalRevenueCents += r.ContributionBase } out := make([]productPerformanceItem, len(rows)) @@ -1916,30 +1934,29 @@ func (h *handler) OperationsProductPerformance() core.HandlerFunc { info := actMap[r.ActivityID] var contribution float64 - if totalCount > 0 { - contribution = float64(r.Count) / float64(totalCount) * 100 + if totalRevenueCents > 0 { + contribution = float64(r.ContributionBase) / float64(totalRevenueCents) * 100 } - // 周转率简化计算 days := e.Sub(s).Hours() / 24 if days < 1 { days = 1 } - turnover := float64(r.Count) / days * 7 + turnover := float64(r.SalesCount) / days * 7 + profitCents := r.RevenueCents - r.TotalCost out[i] = productPerformanceItem{ ID: r.ActivityID, SeriesName: info.Name, - SalesCount: r.Count, - Amount: (r.Count * info.PriceDraw) / 100, // 转换为元 - Profit: (r.Count*info.PriceDraw - r.TotalCost) / 100, + SalesCount: r.SalesCount, + Amount: r.RevenueCents / 100, + Profit: profitCents / 100, ProfitRate: 0, ContributionRate: float64(int(contribution*10)) / 10.0, InventoryTurnover: float64(int(turnover*10)) / 10.0, } - if r.Count > 0 && info.PriceDraw > 0 { - revenue := r.Count * info.PriceDraw - pr := float64(revenue-r.TotalCost) / float64(revenue) * 100 + if r.RevenueCents > 0 { + pr := float64(profitCents) / float64(r.RevenueCents) * 100 out[i].ProfitRate = float64(int(pr*10)) / 10.0 } }