fix: settle gift contribution by coin spent

This commit is contained in:
zhx 2026-06-05 13:51:14 +08:00
parent 7a59d3571c
commit b1a9d818aa
5 changed files with 34 additions and 39 deletions

View File

@ -75,7 +75,6 @@ type giftDTO struct {
ChargeAssetType string `json:"chargeAssetType"` ChargeAssetType string `json:"chargeAssetType"`
CoinPrice int64 `json:"coinPrice"` CoinPrice int64 `json:"coinPrice"`
GiftPointAmount int64 `json:"giftPointAmount"` GiftPointAmount int64 `json:"giftPointAmount"`
HeatValue int64 `json:"heatValue"`
EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveFromMS int64 `json:"effectiveFromMs"`
EffectiveToMS int64 `json:"effectiveToMs"` EffectiveToMS int64 `json:"effectiveToMs"`
EffectTypes []string `json:"effectTypes"` EffectTypes []string `json:"effectTypes"`
@ -300,7 +299,6 @@ func giftFromProto(gift *walletv1.GiftConfig) giftDTO {
ChargeAssetType: gift.GetChargeAssetType(), ChargeAssetType: gift.GetChargeAssetType(),
CoinPrice: gift.GetCoinPrice(), CoinPrice: gift.GetCoinPrice(),
GiftPointAmount: gift.GetGiftPointAmount(), GiftPointAmount: gift.GetGiftPointAmount(),
HeatValue: gift.GetHeatValue(),
EffectiveFromMS: gift.GetEffectiveFromMs(), EffectiveFromMS: gift.GetEffectiveFromMs(),
EffectiveToMS: gift.GetEffectiveToMs(), EffectiveToMS: gift.GetEffectiveToMs(),
EffectTypes: gift.GetEffectTypes(), EffectTypes: gift.GetEffectTypes(),

View File

@ -135,7 +135,7 @@ type giftRequest struct {
ChargeAssetType string `json:"chargeAssetType"` ChargeAssetType string `json:"chargeAssetType"`
CoinPrice int64 `json:"coinPrice"` CoinPrice int64 `json:"coinPrice"`
GiftPointAmount int64 `json:"giftPointAmount"` GiftPointAmount int64 `json:"giftPointAmount"`
HeatValue int64 `json:"heatValue"` HeatValue int64 `json:"heatValue"` // 兼容旧后台请求;新后台不再展示或提交热度。
EffectiveAtMS int64 `json:"effectiveAtMs"` EffectiveAtMS int64 `json:"effectiveAtMs"`
EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveFromMS int64 `json:"effectiveFromMs"`
EffectiveToMS int64 `json:"effectiveToMs"` EffectiveToMS int64 `json:"effectiveToMs"`
@ -371,7 +371,7 @@ func (r giftRequest) createProto(c *gin.Context) *walletv1.CreateGiftConfigReque
ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType),
CoinPrice: r.CoinPrice, CoinPrice: r.CoinPrice,
GiftPointAmount: r.GiftPointAmount, GiftPointAmount: r.GiftPointAmount,
HeatValue: r.HeatValue, HeatValue: r.CoinPrice,
EffectiveAtMs: r.EffectiveAtMS, EffectiveAtMs: r.EffectiveAtMS,
EffectiveFromMs: r.EffectiveFromMS, EffectiveFromMs: r.EffectiveFromMS,
EffectiveToMs: r.EffectiveToMS, EffectiveToMs: r.EffectiveToMS,
@ -396,7 +396,7 @@ func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.Update
ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType),
CoinPrice: r.CoinPrice, CoinPrice: r.CoinPrice,
GiftPointAmount: r.GiftPointAmount, GiftPointAmount: r.GiftPointAmount,
HeatValue: r.HeatValue, HeatValue: r.CoinPrice,
EffectiveAtMs: r.EffectiveAtMS, EffectiveAtMs: r.EffectiveAtMS,
EffectiveFromMs: r.EffectiveFromMS, EffectiveFromMs: r.EffectiveFromMS,
EffectiveToMs: r.EffectiveToMS, EffectiveToMs: r.EffectiveToMS,

View File

@ -54,7 +54,7 @@ func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) {
t.Fatalf("DebitGift failed: %v", err) t.Fatalf("DebitGift failed: %v", err)
} }
if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 6 || receipt.HeatValue != 22 || receipt.BalanceAfter != 86 || receipt.PriceVersion != "v9" { if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 6 || receipt.HeatValue != 14 || receipt.BalanceAfter != 86 || receipt.PriceVersion != "v9" {
t.Fatalf("server price settlement mismatch: %+v", receipt) t.Fatalf("server price settlement mismatch: %+v", receipt)
} }
if receipt.TransactionID == "" || receipt.BillingReceiptID == "" { if receipt.TransactionID == "" || receipt.BillingReceiptID == "" {
@ -113,7 +113,7 @@ func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) {
t.Fatalf("BatchDebitGift failed: %v", err) t.Fatalf("BatchDebitGift failed: %v", err)
} }
if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 12 || receipt.Aggregate.HeatValue != 44 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 { if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 12 || receipt.Aggregate.HeatValue != 28 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 {
t.Fatalf("batch aggregate mismatch: %+v", receipt) t.Fatalf("batch aggregate mismatch: %+v", receipt)
} }
for _, userID := range []int64{10002, 10003} { for _, userID := range []int64{10002, 10003} {
@ -272,21 +272,22 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t
repository.SetGiftDiamondRatio(8801, resourcedomain.GiftTypeSuperLucky, "50.00") repository.SetGiftDiamondRatio(8801, resourcedomain.GiftTypeSuperLucky, "50.00")
cases := []struct { cases := []struct {
giftID string giftID string
giftType string giftType string
command string command string
senderID int64 senderID int64
heatUnit int64 coinPrice int64
wantHeat int64 heatUnit int64
wantHeat int64
}{ }{
{giftID: "normal-room-ratio-gift", giftType: resourcedomain.GiftTypeNormal, command: "cmd-normal-room-ratio", senderID: 13001, heatUnit: 100, wantHeat: 100}, {giftID: "normal-room-ratio-gift", giftType: resourcedomain.GiftTypeNormal, command: "cmd-normal-room-ratio", senderID: 13001, coinPrice: 100, heatUnit: 999, wantHeat: 100},
{giftID: "lucky-room-ratio-gift", giftType: resourcedomain.GiftTypeLucky, command: "cmd-lucky-room-ratio", senderID: 13002, heatUnit: 100, wantHeat: 10}, {giftID: "lucky-room-ratio-gift", giftType: resourcedomain.GiftTypeLucky, command: "cmd-lucky-room-ratio", senderID: 13002, coinPrice: 100, heatUnit: 999, wantHeat: 10},
{giftID: "super-lucky-room-ratio-gift", giftType: resourcedomain.GiftTypeSuperLucky, command: "cmd-super-lucky-room-ratio", senderID: 13003, heatUnit: 99, wantHeat: 0}, {giftID: "super-lucky-room-ratio-gift", giftType: resourcedomain.GiftTypeSuperLucky, command: "cmd-super-lucky-room-ratio", senderID: 13003, coinPrice: 200, heatUnit: 500, wantHeat: 2},
} }
for _, tc := range cases { for _, tc := range cases {
repository.SetBalance(tc.senderID, 100) repository.SetBalance(tc.senderID, 500)
repository.SetGiftPrice(tc.giftID, "v1", tc.heatUnit, 100, tc.heatUnit) repository.SetGiftPrice(tc.giftID, "v1", tc.coinPrice, 100, tc.heatUnit)
repository.SetGiftType(tc.giftID, tc.giftType) repository.SetGiftType(tc.giftID, tc.giftType)
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{ receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
CommandID: tc.command, CommandID: tc.command,
@ -302,7 +303,7 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t
if err != nil { if err != nil {
t.Fatalf("DebitGift %s failed: %v", tc.giftType, err) t.Fatalf("DebitGift %s failed: %v", tc.giftType, err)
} }
if receipt.HeatValue != tc.wantHeat || receipt.GiftPointAdded != 100 || receipt.CoinSpent != tc.heatUnit { if receipt.HeatValue != tc.wantHeat || receipt.GiftPointAdded != 100 || receipt.CoinSpent != tc.coinPrice {
t.Fatalf("%s room contribution ratio mismatch: %+v want heat %d", tc.giftType, receipt, tc.wantHeat) t.Fatalf("%s room contribution ratio mismatch: %+v want heat %d", tc.giftType, receipt, tc.wantHeat)
} }
} }
@ -311,7 +312,7 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t
func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue(t *testing.T) { func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue(t *testing.T) {
repository := mysqltest.NewRepository(t) repository := mysqltest.NewRepository(t)
repository.SetBalance(15001, 1000) repository.SetBalance(15001, 1000)
repository.SetGiftPrice("batch-room-ratio-gift", "v1", 100, 100, 100) repository.SetGiftPrice("batch-room-ratio-gift", "v1", 100, 100, 999)
repository.SetGiftType("batch-room-ratio-gift", resourcedomain.GiftTypeLucky) repository.SetGiftType("batch-room-ratio-gift", resourcedomain.GiftTypeLucky)
repository.SetGiftDiamondRatio(9901, resourcedomain.GiftTypeLucky, "25.00") repository.SetGiftDiamondRatio(9901, resourcedomain.GiftTypeLucky, "25.00")
svc := walletservice.New(repository) svc := walletservice.New(repository)
@ -1085,7 +1086,7 @@ func TestGetUserGiftWallAggregatesSettledGifts(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetUserGiftWall failed: %v", err) t.Fatalf("GetUserGiftWall failed: %v", err)
} }
if wall.GiftKindCount != 2 || wall.GiftTotalCount != 6 || wall.TotalValue != 135 || wall.TotalGiftPoint != 65 || wall.TotalHeatValue != 255 { if wall.GiftKindCount != 2 || wall.GiftTotalCount != 6 || wall.TotalValue != 135 || wall.TotalGiftPoint != 65 || wall.TotalHeatValue != 135 {
t.Fatalf("gift wall summary mismatch: %+v", wall) t.Fatalf("gift wall summary mismatch: %+v", wall)
} }
rose, ok := giftWallItemByID(wall.Items, "rose") rose, ok := giftWallItemByID(wall.Items, "rose")
@ -2150,7 +2151,7 @@ func TestGiftConfigMustSelectGiftResource(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("DebitGift with resource-backed gift failed: %v", err) t.Fatalf("DebitGift with resource-backed gift failed: %v", err)
} }
if receipt.CoinSpent != 10 || receipt.GiftPointAdded != 4 || receipt.HeatValue != 18 { if receipt.CoinSpent != 10 || receipt.GiftPointAdded != 4 || receipt.HeatValue != 10 {
t.Fatalf("resource-backed gift settlement mismatch: %+v", receipt) t.Fatalf("resource-backed gift settlement mismatch: %+v", receipt)
} }
} }

View File

@ -222,16 +222,12 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
if err != nil { if err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
} }
baseHeatValue, err := checkedMul(price.HeatValue, int64(command.GiftCount))
if err != nil {
return ledger.Receipt{}, err
}
roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode)
if err != nil { if err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
} }
// 房间贡献统一按全局默认礼物比例折算room-service 只消费结算后的 heat_value不再自己查钱包配置 // 房间贡献只以真实扣费 COIN 为基数;旧 gift price heat_value 不再参与结算,避免后台热度配置覆盖实际消费
heatValue, err := giftDiamondAmount(baseHeatValue, roomContributionRatio.PPM) heatValue, err := giftDiamondAmount(chargeAmount, roomContributionRatio.PPM)
if err != nil { if err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
} }
@ -301,7 +297,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
RoomContributionRatioRegionID: roomContributionRatioRegionID, RoomContributionRatioRegionID: roomContributionRatioRegionID,
CoinPrice: price.CoinPrice, CoinPrice: price.CoinPrice,
GiftPointAmount: price.GiftPointAmount, GiftPointAmount: price.GiftPointAmount,
HeatUnitValue: price.HeatValue, HeatUnitValue: price.CoinPrice,
} }
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil { if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
@ -410,16 +406,12 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
if err != nil { if err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err
} }
baseHeatValue, err := checkedMul(price.HeatValue, int64(command.GiftCount))
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode)
if err != nil { if err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err
} }
// 批量多目标仍然是每个目标各送一份同款礼物;每份房间贡献先按全局默认礼物比例折算,再由聚合回执累加。 // 批量多目标仍然是每个目标各送一份同款礼物;每份房间贡献先按真实扣费折算,再由聚合回执累加。
heatValue, err := giftDiamondAmount(baseHeatValue, roomContributionRatio.PPM) heatValue, err := giftDiamondAmount(chargeAmount, roomContributionRatio.PPM)
if err != nil { if err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err
} }
@ -549,7 +541,7 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
RoomContributionRatioRegionID: roomContributionRatioRegionID, RoomContributionRatioRegionID: roomContributionRatioRegionID,
CoinPrice: price.CoinPrice, CoinPrice: price.CoinPrice,
GiftPointAmount: price.GiftPointAmount, GiftPointAmount: price.GiftPointAmount,
HeatUnitValue: price.HeatValue, HeatUnitValue: price.CoinPrice,
} }
if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil { if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err

View File

@ -628,7 +628,7 @@ func (r *Repository) ListGiftConfigs(ctx context.Context, query resourcedomain.L
item.ChargeAssetType = price.ChargeAssetType item.ChargeAssetType = price.ChargeAssetType
item.CoinPrice = price.CoinPrice item.CoinPrice = price.CoinPrice
item.GiftPointAmount = price.GiftPointAmount item.GiftPointAmount = price.GiftPointAmount
item.HeatValue = price.HeatValue item.HeatValue = price.CoinPrice
} }
items = append(items, item) items = append(items, item)
} }
@ -1952,6 +1952,7 @@ func (r *Repository) listResourceGrantItemsWithQuery(ctx context.Context, querie
} }
func (r *Repository) upsertGiftPriceTx(ctx context.Context, tx *sql.Tx, command resourcedomain.GiftConfigCommand, nowMs int64) error { func (r *Repository) upsertGiftPriceTx(ctx context.Context, tx *sql.Tx, command resourcedomain.GiftConfigCommand, nowMs int64) error {
legacyHeatValue := command.CoinPrice
_, err := tx.ExecContext(ctx, ` _, err := tx.ExecContext(ctx, `
INSERT INTO wallet_gift_prices ( INSERT INTO wallet_gift_prices (
app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount, app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount,
@ -1966,7 +1967,7 @@ func (r *Repository) upsertGiftPriceTx(ctx context.Context, tx *sql.Tx, command
effective_at_ms = VALUES(effective_at_ms), effective_at_ms = VALUES(effective_at_ms),
updated_at_ms = VALUES(updated_at_ms)`, updated_at_ms = VALUES(updated_at_ms)`,
appcode.FromContext(ctx), command.GiftID, command.PriceVersion, command.ChargeAssetType, command.CoinPrice, appcode.FromContext(ctx), command.GiftID, command.PriceVersion, command.ChargeAssetType, command.CoinPrice,
command.GiftPointAmount, command.HeatValue, command.EffectiveAtMS, nowMs, nowMs, command.GiftPointAmount, legacyHeatValue, command.EffectiveAtMS, nowMs, nowMs,
) )
return err return err
} }
@ -2006,7 +2007,8 @@ func (r *Repository) getGiftConfig(ctx context.Context, giftID string) (resource
gift.ChargeAssetType = price.ChargeAssetType gift.ChargeAssetType = price.ChargeAssetType
gift.CoinPrice = price.CoinPrice gift.CoinPrice = price.CoinPrice
gift.GiftPointAmount = price.GiftPointAmount gift.GiftPointAmount = price.GiftPointAmount
gift.HeatValue = price.HeatValue // heat_value 是旧配置列,后台不再读写;对兼容调用方只回真实价格,房间贡献也按真实扣费结算。
gift.HeatValue = price.CoinPrice
} }
gift.RegionIDs, err = r.listGiftConfigRegionIDs(ctx, r.db, gift.GiftID) gift.RegionIDs, err = r.listGiftConfigRegionIDs(ctx, r.db, gift.GiftID)
if err != nil { if err != nil {
@ -3269,6 +3271,8 @@ func applyResourcePricingToGiftCommand(command resourcedomain.GiftConfigCommand,
command.CoinPrice = 0 command.CoinPrice = 0
command.GiftPointAmount = 0 command.GiftPointAmount = 0
} }
// 后台已取消热度配置;旧列只保留兼容写入,数值始终跟真实 COIN 价格一致。
command.HeatValue = command.CoinPrice
return command return command
} }