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"
)
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,
}