feat(distribution): 收口账号同步与支付配置并优化结算

This commit is contained in:
yml2213
2026-10-30 16:03:30 +08:00
parent d37cbad323
commit 81a19431fa
11 changed files with 1122 additions and 26 deletions
+65 -17
View File
@@ -279,6 +279,52 @@ func readOrderSnapshotPrice(raw datatypes.JSON, key string) float64 {
return readJSONNumber(breakdown[key])
}
// readOrderSnapshotBPS 优先读取整数基点字段;旧快照只有 float 比例时换算为 bps。
func readOrderSnapshotBPS(raw datatypes.JSON, key string) int64 {
breakdown, ok := readOrderSnapshotBreakdown(raw)
if !ok {
return 0
}
if bps := readJSONNumber(breakdown[key+"_bps"]); bps > 0 {
return int64(math.Round(bps))
}
legacy := readJSONNumber(breakdown[key])
if legacy <= 0 {
return 0
}
return int64(math.Round(legacy * 10000))
}
// quantityUnits 把 M 数量转成百分之一 M 的整数单位,避免比例计算依赖浮点。
func quantityUnits(value float64) int64 {
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return int64(math.Round(value * 100))
}
// roundedDiv 对非负整数做四舍五入除法。
func roundedDiv(numerator, denominator int64) int64 {
if numerator <= 0 || denominator <= 0 {
return 0
}
return (numerator + denominator/2) / denominator
}
// proportionalCent 按 part/total 折算金额;total<=0 时保持旧行为取全额。
func proportionalCent(baseCent, partUnits, totalUnits int64) int64 {
if baseCent <= 0 {
return 0
}
if totalUnits <= 0 {
return baseCent
}
if partUnits <= 0 {
return 0
}
return roundedDiv(baseCent*partUnits, totalUnits)
}
func readJSONNumber(value any) float64 {
switch typed := value.(type) {
case float64:
@@ -377,28 +423,28 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
consumedM := roundQuantity(coinConsumedM)
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
buyerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_ratio")
sellerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "seller_ratio")
buyerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "buyer_ratio")
sellerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "seller_ratio")
consumedUnits := quantityUnits(consumedM)
totalCoinUnits := quantityUnits(totalCoinM)
// 币费:优先按各自比例 × 实际消耗;无比例时回退到预收币基价 × (消耗/发布量),允许 >1(打超)
// 无发布量快照时保持旧行为:按全额使用(ratio=1)
coinUseRatio := coinConsumptionRatio(consumedM, totalCoinM)
// 币费:比例优先按 bps 整数换算;缺失 bps 时把旧比例换算成 bps。
// 无比例时回退到预收币基价按消耗比例折算,仍使用整数除法。
var usedBuyerCoinPriceCent, usedOwnerCoinPriceCent int64
if buyerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
// 价(元) = 消耗M×100 / ratio;分 = 元×100
usedBuyerCoinPriceCent = int64(math.Round(consumedM * 10000 / buyerRatio))
if buyerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
usedBuyerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, buyerRatioBPS)
} else {
usedBuyerCoinPriceCent = int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
usedBuyerCoinPriceCent = proportionalCent(buyerCoinBasePriceCent, consumedUnits, totalCoinUnits)
}
if sellerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
usedOwnerCoinPriceCent = int64(math.Round(consumedM * 10000 / sellerRatio))
if sellerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
usedOwnerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, sellerRatioBPS)
} else {
usedOwnerCoinPriceCent = int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
usedOwnerCoinPriceCent = proportionalCent(sellerCoinBasePriceCent, consumedUnits, totalCoinUnits)
}
coinOvershoot := totalCoinM > 0 && consumedM > totalCoinM
coinOvershoot := totalCoinUnits > 0 && consumedUnits > totalCoinUnits
if !coinOvershoot {
// 未打超时,比例反推只能用于折算未用完部分,不能因四舍五入超过订单快照。
if totalCoinM > 0 && consumedM >= totalCoinM {
// 未打超时,比例反推只能用于折算未用完部分,不能因取整超过订单快照。
if totalCoinUnits > 0 && consumedUnits >= totalCoinUnits {
usedBuyerCoinPriceCent = buyerCoinBasePriceCent
usedOwnerCoinPriceCent = sellerCoinBasePriceCent
} else {
@@ -412,8 +458,10 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
var usedOwnerConsumablePriceCent int64
if prepaidConsumablePriceCent > 0 {
if usedBuyerConsumablePriceCent <= prepaidConsumablePriceCent {
consumableUseRatio := maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0)
usedOwnerConsumablePriceCent = int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
usedOwnerConsumablePriceCent = roundedDiv(
prepaidOwnerConsumablePriceCent*usedBuyerConsumablePriceCent,
prepaidConsumablePriceCent,
)
usedOwnerConsumablePriceCent = minCent(usedOwnerConsumablePriceCent, prepaidOwnerConsumablePriceCent)
} else {
// 预收范围内按号主预收基准,超出预收部分作为后置损耗全额划归号主(平台不抽成)
@@ -66,3 +66,37 @@ func TestBuildOrderPricingUsesCentBreakdown(t *testing.T) {
t.Fatalf("OwnerPureCoinAmountCent = %d, want 23315", pricing.OwnerPureCoinAmountCent)
}
}
func TestCalculateCheckoutSettlementPrefersRatioBPS(t *testing.T) {
// bps 必须优先于旧 float 比例:buyer 1:40、seller 1:45,消耗 180M。
order := model.RentalOrder{
RentAmountCent: 25000,
OwnerRentAmountCent: 22222,
DepositAmountCent: 50000,
AccountSnapshot: datatypes.JSON([]byte(`{
"haf_coin_amount": 100000000,
"asset_summary": {
"price_breakdown": {
"buyer_ratio": 1,
"seller_ratio": 1,
"buyer_ratio_bps": 400000,
"seller_ratio_bps": 450000,
"buyer_coin_base_price": 250,
"seller_coin_base_price": 222.22,
"consumable_price": 0
}
}
}`)),
}
settlement := calculateCheckoutSettlement(order, 0, 180, 0)
if settlement.ActualRentAmountCent != 45000 {
t.Fatalf("ActualRentAmountCent = %d, want 45000", settlement.ActualRentAmountCent)
}
if settlement.OwnerRentIncomeCent != 40000 {
t.Fatalf("OwnerRentIncomeCent = %d, want 40000", settlement.OwnerRentIncomeCent)
}
if settlement.PlatformFeeCent != 5000 {
t.Fatalf("PlatformFeeCent = %d, want 5000", settlement.PlatformFeeCent)
}
}