fix(finance): correct activity profit-loss cost aggregation

Use handler-specific activity profit/loss rules for the dashboard endpoints, counting cash, coupons, and game pass value against product cost_price. Fix missing extra reward cost aggregation and align activity logs with the same profit calculation semantics.
This commit is contained in:
Zuncle 2026-04-02 22:27:45 +08:00
parent 4353b0f053
commit dd1034dda8

View File

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