1. Revenue 口径统一为 actual_amount(真实现金到账)
- 优惠券(discount_amount)和积分(points_amount)是平台免费发放的营销补贴,
不算收入,改为展示字段
- 涉及: profit_metrics.go, dashboard_spending.go, users_profit_loss.go,
dashboard_user_spending.go, activity_rankings_admin.go
2. Cost 口径统一为奖品库存价值
- 删除 finance service 中的积分成本扫描(Step 3)和优惠券成本扫描(Step 4)
- 之前优惠券同时算在收入和成本两侧,导致利润被人为压低
- 涉及: query_user.go, query_activity.go
3. 统一 value_cents fallback chain
- finance service 改为与排行榜一致的三级回退:
COALESCE(NULLIF(value_cents,0), price_snapshot_cents, products.price, 0)
- 涉及: query_user.go, query_activity.go
4. 活动盈亏收入统一到 finance service
- 删除 dashboard_activity.go 自有的 revenue SQL(含比例分摊逻辑)
- 收入和成本统一由 finance.Service.QueryActivityProfitLoss() 提供
- 修复日志明细 profit:道具卡倍率改用 ComputePrizeCostWithMultiplier
5. finance service 新增展示字段
- ProfitLossDetail 增加 CouponDiscount, PointsDiscount, GamePassValue
- 不参与 Revenue/Cost/Profit 计算,仅供前端展示营销补贴明细
6. 修复对对碰次卡订单 discount_amount 数据污染
- matching_game_app.go 次卡下单时 DiscountAmount 错误设为活动全价
- 改为 0(次卡支付不涉及优惠券)
- 附带历史数据修复 migration SQL
7. 排除已分解奖品的成本重复计算
- 用户可以把奖品分解成积分再兑换新商品,导致同一份价值被计算两次
- 所有库存查询增加排除条件: status=3 且 remark 含 redeemed_points 或 batch_redeemed
- 涉及 6 个文件的库存成本/资产查询
8. 排行榜详情抽屉限定活动范围
- prize 查询增加 activity_id > 0 过滤,排除积分兑换/转入/合成等非活动产出
- 使排行榜与其详情抽屉口径一致
修改文件(12个):
- internal/service/finance/profit_metrics.go
- internal/service/finance/query_user.go
- internal/service/finance/query_activity.go
- internal/service/finance/types.go
- internal/api/admin/dashboard_activity.go
- internal/api/admin/dashboard_spending.go
- internal/api/admin/dashboard_user_spending.go
- internal/api/admin/users_profit_loss.go
- internal/api/admin/users_profile.go
- internal/api/admin/activity_rankings_admin.go
- internal/api/activity/matching_game_app.go
- migrations/20260325_fix_matching_gamepass_discount.sql
85 lines
2.1 KiB
Go
85 lines
2.1 KiB
Go
package finance
|
|
|
|
import "strings"
|
|
|
|
const defaultMultiplierX1000 int64 = 1000
|
|
|
|
type SpendingBreakdown struct {
|
|
PaidCoupon int64
|
|
GamePass int64
|
|
Total int64
|
|
IsGamePass bool
|
|
}
|
|
|
|
// ClassifyOrderSpending applies the unified rule:
|
|
// - game pass order: spending = game pass value
|
|
// - normal order: spending = actual_amount only (real cash received)
|
|
//
|
|
// Note: discount_amount (coupon) and points_amount are FREE marketing subsidies,
|
|
// NOT real revenue. They are tracked as display-only fields separately.
|
|
func ClassifyOrderSpending(sourceType int32, orderNo string, actualAmount, discountAmount int64, remark string, gamePassValue int64) SpendingBreakdown {
|
|
isGamePass := IsGamePassOrder(sourceType, orderNo, actualAmount, remark)
|
|
if isGamePass {
|
|
if gamePassValue < 0 {
|
|
gamePassValue = 0
|
|
}
|
|
return SpendingBreakdown{
|
|
PaidCoupon: 0,
|
|
GamePass: gamePassValue,
|
|
Total: gamePassValue,
|
|
IsGamePass: true,
|
|
}
|
|
}
|
|
|
|
cashRevenue := actualAmount
|
|
if cashRevenue < 0 {
|
|
cashRevenue = 0
|
|
}
|
|
return SpendingBreakdown{
|
|
PaidCoupon: cashRevenue,
|
|
GamePass: 0,
|
|
Total: cashRevenue,
|
|
IsGamePass: false,
|
|
}
|
|
}
|
|
|
|
func IsGamePassOrder(sourceType int32, orderNo string, actualAmount int64, remark string) bool {
|
|
if sourceType == 4 {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(orderNo, "GP") {
|
|
return true
|
|
}
|
|
return actualAmount == 0 && strings.Contains(remark, "use_game_pass")
|
|
}
|
|
|
|
func ComputeGamePassValue(drawCount, activityPrice int64) int64 {
|
|
if drawCount <= 0 || activityPrice <= 0 {
|
|
return 0
|
|
}
|
|
return drawCount * activityPrice
|
|
}
|
|
|
|
func NormalizeMultiplierX1000(multiplierX1000 int64) int64 {
|
|
if multiplierX1000 <= 0 {
|
|
return defaultMultiplierX1000
|
|
}
|
|
return multiplierX1000
|
|
}
|
|
|
|
func ComputePrizeCostWithMultiplier(baseCost, multiplierX1000 int64) int64 {
|
|
if baseCost <= 0 {
|
|
return 0
|
|
}
|
|
n := NormalizeMultiplierX1000(multiplierX1000)
|
|
return baseCost * n / defaultMultiplierX1000
|
|
}
|
|
|
|
func ComputeProfit(spending, prizeCost int64) (int64, float64) {
|
|
profit := spending - prizeCost
|
|
if spending <= 0 {
|
|
return profit, 0
|
|
}
|
|
return profit, float64(profit) / float64(spending)
|
|
}
|