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:
parent
4353b0f053
commit
dd1034dda8
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user