diff --git a/internal/api/admin/dashboard_activity.go b/internal/api/admin/dashboard_activity.go index a7fdea9..82b4fa6 100755 --- a/internal/api/admin/dashboard_activity.go +++ b/internal/api/admin/dashboard_activity.go @@ -18,6 +18,14 @@ import ( "gorm.io/gorm" ) +func computeActivityProfit(spending, cost int64) (int64, float64) { + profit := spending - cost + if spending <= 0 { + return profit, 0 + } + return profit, float64(profit) / float64(spending) +} + type activityProfitLossRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` @@ -145,10 +153,18 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc { var drawStats []drawStat db.Table(model.TableNameActivityDrawLogs). Select(` - activity_issues.activity_id, + activity_issues.activity_id, COUNT(activity_draw_logs.id) as total_count, - SUM(CASE WHEN orders.status = 2 AND (orders.source_type = 4 OR orders.order_no LIKE 'GP%') 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%') THEN 1 ELSE 0 END) as payment_count, + 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 IN (3, 4) THEN 1 ELSE 0 END) as refund_count, COUNT(DISTINCT CASE WHEN orders.status = 2 THEN activity_draw_logs.user_id END) as player_count `). @@ -168,38 +184,92 @@ func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc { } } - // 3. 从 finance.Service 统一获取收入、成本和展示字段 - // Revenue = actual_amount (真实现金) - // Cost = inventory value × item card multiplier - // 展示字段: CouponDiscount, PointsDiscount, GamePassValue - financeParams := financesvc.ActivityProfitLossParams{ - ActivityIDs: activityIDs, + // 3. 按活动汇总收入(现金/优惠券/次卡) + type activityRevenueStat struct { + ActivityID int64 + SourceType int32 + OrderNo string + OrderAmount int64 + DiscountAmount int64 + OrderRemark string + DrawCount int64 + ActivityPrice int64 } - financeResult, financeErr := h.financeSvc.QueryActivityProfitLoss(ctx.RequestContext(), financeParams) - if financeErr != nil { - h.logger.Error(fmt.Sprintf("GetActivityProfitLoss finance error: %v", financeErr)) + var revenueStats []activityRevenueStat + db.Table(model.TableNameOrders). + Select(` + activity_issues.activity_id, + orders.source_type, + orders.order_no, + COALESCE(orders.actual_amount, 0) as order_amount, + COALESCE(orders.discount_amount, 0) as discount_amount, + COALESCE(orders.remark, '') as order_remark, + COUNT(activity_draw_logs.id) as draw_count, + COALESCE(MAX(activities.price_draw), 0) as activity_price + `). + Joins("JOIN activity_draw_logs ON activity_draw_logs.order_id = orders.id"). + Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id"). + Joins("JOIN activities ON activities.id = activity_issues.activity_id"). + Where("orders.status = ?", 2). + Where("activity_issues.activity_id IN ?", activityIDs). + Group("orders.id, activity_issues.activity_id, orders.source_type, orders.order_no, orders.actual_amount, orders.discount_amount, orders.remark"). + Scan(&revenueStats) + + for _, stat := range revenueStats { + item, ok := activityMap[stat.ActivityID] + if !ok { + continue + } + if financesvc.IsGamePassOrder(stat.SourceType, stat.OrderNo, stat.OrderAmount, stat.OrderRemark) { + item.TotalGamePassValue += stat.DrawCount * stat.ActivityPrice + continue + } + item.TotalRevenue += stat.OrderAmount + item.TotalDiscount += stat.DiscountAmount } - if financeResult != nil { - for _, d := range financeResult.Details { - if item, ok := activityMap[d.ActivityID]; ok { - item.TotalRevenue = d.Revenue - item.TotalCost = d.Cost - item.PrizeCostFinal = d.Cost - item.TotalDiscount = d.CouponDiscount + d.PointsDiscount - item.TotalGamePassValue = d.GamePassValue - } + + // 4. 按活动汇总产出成本,统一使用 products.cost_price + type activityCostStat struct { + ActivityID int64 + TotalCost int64 + } + var costStats []activityCostStat + db.Table(model.TableNameActivityDrawLogs). + Select(` + activity_issues.activity_id, + 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 total_cost + `). + 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("activity_issues.activity_id IN ?", activityIDs). + Where("orders.status = ?", 2). + Group("activity_issues.activity_id"). + Scan(&costStats) + + for _, stat := range costStats { + if item, ok := activityMap[stat.ActivityID]; ok { + item.TotalCost = stat.TotalCost + item.PrizeCostBase = stat.TotalCost + item.PrizeCostMultiplier = 1000 + item.PrizeCostFinal = stat.TotalCost } } - // 4. 计算盈亏和比率 - // Revenue = 现金到账, Cost = 奖品成本 - // Profit = Revenue - Cost (优惠券/积分/次卡不参与利润计算) + // 5. 计算盈亏和比率 finalList := make([]activityProfitLossItem, 0, len(activities)) for _, a := range activities { item := activityMap[a.ID] - item.SpendingPaidCoupon = item.TotalRevenue + item.SpendingPaidCoupon = item.TotalRevenue + item.TotalDiscount item.SpendingGamePass = item.TotalGamePassValue - item.Profit, item.ProfitRate = financesvc.ComputeProfit(item.TotalRevenue, item.TotalCost) + spending := item.TotalRevenue + item.TotalDiscount + item.TotalGamePassValue + item.Profit, item.ProfitRate = computeActivityProfit(spending, item.TotalCost) finalList = append(finalList, *item) } @@ -335,6 +405,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { ProductName string ImagesJSON string ProductPrice int64 + ProductCost int64 OrderAmount int64 DiscountAmount int64 PointsAmount int64 // 积分抵扣金额 @@ -346,6 +417,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { ItemCardName string EffectType int32 Multiplier int32 + DropQuantity int64 OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息 OrderNo string // 订单号 DrawCount int64 // 该订单的总抽奖次数(用于金额分摊) @@ -364,6 +436,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { COALESCE(products.name, '') as product_name, COALESCE(products.images_json, '[]') as images_json, COALESCE(activity_reward_settings.price_snapshot_cents, products.price, 0) as product_price, + COALESCE(products.cost_price, 0) as product_cost, COALESCE(orders.actual_amount, 0) as order_amount, COALESCE(orders.discount_amount, 0) as discount_amount, COALESCE(orders.points_amount, 0) as points_amount, @@ -375,6 +448,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { COALESCE(system_item_cards.name, '') as item_card_name, COALESCE(system_item_cards.effect_type, 0) as effect_type, COALESCE(system_item_cards.reward_multiplier_x1000, 0) as multiplier, + COALESCE(NULLIF(activity_reward_settings.drop_quantity, 0), 1) as drop_quantity, COALESCE(orders.remark, '') as order_remark, COALESCE(orders.order_no, '') as order_no, COALESCE(order_draw_counts.draw_count, 1) as draw_count, @@ -416,8 +490,10 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { productImage = images[0] } - // Default quantity is 1 - quantity := int64(1) + quantity := l.DropQuantity + if quantity <= 0 { + quantity = 1 + } // Determine PayType and UsedCard + PaymentDetails payType := "现金支付" @@ -454,9 +530,9 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { } payType = "道具卡" - // 计算双倍/多倍卡数量 - if l.EffectType == 1 && l.Multiplier > 1000 { - quantity = quantity * int64(l.Multiplier) / 1000 + // 当前实现里,道具卡额外发放的是单个额外奖品,不是整组倍率放大 + if l.EffectType == 1 && l.Multiplier >= 2000 { + quantity++ } } @@ -467,7 +543,7 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { gamePassInfo := "次数卡" if strings.Contains(l.OrderRemark, "gp_use:") { // 从remark中提取次数卡信息,格式: use_game_pass;gp_use:ID:Count;gp_use:ID:Count - parts := strings.Split(l.OrderRemark, ";") + parts := strings.Split(l.OrderRemark, "|") var gpParts []string for _, p := range parts { if strings.HasPrefix(p, "gp_use:") { @@ -524,22 +600,24 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { perDrawOrderAmount := l.OrderAmount / drawCount // actual_amount 分摊(现金) perDrawDiscountAmount := l.DiscountAmount / drawCount // 展示用 perDrawPointsAmount := l.PointsAmount / drawCount // 展示用 + perDrawGamePassAmount := int64(0) // 次卡单口径:仅记次卡价值,不再叠加 discount,避免”次卡+现金”双计 if isGamePassOrder { - if l.ActivityPrice > 0 { - perDrawOrderAmount = l.ActivityPrice - } + perDrawOrderAmount = 0 perDrawDiscountAmount = 0 perDrawPointsAmount = 0 + if l.ActivityPrice > 0 { + perDrawGamePassAmount = l.ActivityPrice + } } // 设置支付详情中的分摊金额 paymentDetails.CouponDiscount = perDrawDiscountAmount paymentDetails.PointsDiscount = perDrawPointsAmount - // 计算单次抽奖成本:使用 value_cents × 道具卡倍率(与 finance service 一致) - prizeCost := financesvc.ComputePrizeCostWithMultiplier(l.ProductPrice, int64(l.Multiplier)*1000) + prizeCost := l.ProductCost * quantity + profitSpending := perDrawOrderAmount + perDrawDiscountAmount + perDrawGamePassAmount list[i] = activityLogItem{ ID: l.ID, @@ -549,15 +627,15 @@ func (h *handler) DashboardActivityLogs() core.HandlerFunc { ProductID: l.ProductID, ProductName: l.ProductName, ProductImage: productImage, - ProductPrice: l.ProductPrice, + ProductPrice: l.ProductCost, ProductQuantity: quantity, - OrderAmount: perDrawOrderAmount, // 单次抽奖分摊的现金金额 - OrderNo: l.OrderNo, // 订单号 - DiscountAmount: perDrawDiscountAmount, // 单次抽奖分摊的优惠金额(展示用) + OrderAmount: perDrawOrderAmount + perDrawGamePassAmount, + OrderNo: l.OrderNo, + DiscountAmount: perDrawDiscountAmount, PayType: payType, UsedCard: usedCard, OrderStatus: l.OrderStatus, - Profit: perDrawOrderAmount - prizeCost, // 单次盈亏 = 现金收入 - 奖品成本(含倍率) + Profit: profitSpending - prizeCost, CreatedAt: l.CreatedAt, PaymentDetails: paymentDetails, }