支持结账多轮协商与按实际消耗双比例结算

允许双方最多 6 轮修改结账方案;哈夫币按实际消耗与 buyer/seller 比例计价,打超从押金扣,押金不足禁止自动完结并引导人工争议。同步收紧结账明细展示密度。
This commit is contained in:
yml2213
2026-07-18 20:35:16 +08:00
parent 98772867a7
commit 1a4776bba0
19 changed files with 713 additions and 140 deletions
+68 -18
View File
@@ -27,6 +27,8 @@ type checkoutSettlement struct {
RenterRefundCent int64
PlatformFeeCent int64
ActualRentAmountCent int64
OvershootAmountCent int64
ShortfallCent int64
}
func orderDurationHours(order model.RentalOrder) int {
@@ -145,9 +147,6 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
if useExplicitDeduct {
deductAmountCent = explicitDeductCent
}
if deductAmountCent > order.DepositAmountCent {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
}
settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent)
evidence, err := marshalStringList(evidenceURLS)
if err != nil {
@@ -164,9 +163,12 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
ConsumableAmountCent: consumableAmountCent,
CoinConsumedM: roundQuantity(coinConsumedM),
OtherAmountCent: otherAmountCent,
DepositDeductAmountCent: settlement.DepositCompensationCent,
// 存用户申报的押金赔付(损坏等),打超由结算自动计入 shortfall/overshoot
DepositDeductAmountCent: deductAmountCent,
RenterRefundAmountCent: settlement.RenterRefundCent,
OwnerIncomeAmountCent: settlement.OwnerIncomeCent,
ShortfallCent: settlement.ShortfallCent,
OvershootAmountCent: settlement.OvershootAmountCent,
Content: content,
EvidenceURLS: evidence,
}, nil
@@ -194,24 +196,61 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0)
}
prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0)
consumedM := roundQuantity(coinConsumedM)
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
coinUseRatio := 1.0
if totalCoinM > 0 {
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
buyerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_ratio")
sellerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "seller_ratio")
// 币费:优先按各自比例 × 实际消耗;无比例时回退到预收币基价 × (消耗/发布量),允许 >1(打超)
// 无发布量快照时保持旧行为:按全额使用(ratio=1)
coinUseRatio := coinConsumptionRatio(consumedM, totalCoinM)
var usedBuyerCoinPriceCent, usedOwnerCoinPriceCent int64
if buyerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
// 价(元) = 消耗M×100 / ratio;分 = 元×100
usedBuyerCoinPriceCent = int64(math.Round(consumedM * 10000 / buyerRatio))
} else {
usedBuyerCoinPriceCent = int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
}
usedBuyerCoinPriceCent := int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
usedOwnerCoinPriceCent := int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
usedBuyerConsumablePriceCent := minCent(consumableAmountCent, prepaidConsumablePriceCent)
consumableUseRatio := 1.0
if sellerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
usedOwnerCoinPriceCent = int64(math.Round(consumedM * 10000 / sellerRatio))
} else {
usedOwnerCoinPriceCent = int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
}
// 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比线性外推
usedBuyerConsumablePriceCent := maxCent(consumableAmountCent, 0)
consumableUseRatio := 0.0
if prepaidConsumablePriceCent > 0 {
consumableUseRatio = minRatio(maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0), 1)
consumableUseRatio = maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0)
}
usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
actualRentAmountCent := minCent(usedBuyerCoinPriceCent+usedBuyerConsumablePriceCent, orderRentAmountCent)
ownerRentIncomeCent := minCent(usedOwnerCoinPriceCent+usedOwnerConsumablePriceCent, orderOwnerRentAmountCent)
depositCompensationCent := minCent(depositDeductAmountCent, orderDepositAmountCent)
rentRefundCent := maxCent(orderRentAmountCent-actualRentAmountCent, 0)
depositRefundCent := maxCent(orderDepositAmountCent-depositCompensationCent, 0)
actualBuyerRentCent := usedBuyerCoinPriceCent + usedBuyerConsumablePriceCent
actualOwnerRentCent := usedOwnerCoinPriceCent + usedOwnerConsumablePriceCent
if actualOwnerRentCent > actualBuyerRentCent {
actualOwnerRentCent = actualBuyerRentCent
}
// 打超:实际租客侧费用超出预收租金的部分,从押金扣
overshootCent := maxCent(actualBuyerRentCent-orderRentAmountCent, 0)
damageRequestCent := maxCent(depositDeductAmountCent, 0)
// 押金先覆盖损坏赔付,再覆盖打超;不足记 shortfall,禁止自动完结
damageAppliedCent := minCent(damageRequestCent, orderDepositAmountCent)
depositLeftAfterDamage := maxCent(orderDepositAmountCent-damageAppliedCent, 0)
overshootCoveredCent := minCent(overshootCent, depositLeftAfterDamage)
shortfallCent := maxCent(damageRequestCent-damageAppliedCent, 0) + maxCent(overshootCent-overshootCoveredCent, 0)
rentFromPrepaidCent := minCent(actualBuyerRentCent, orderRentAmountCent)
rentRefundCent := maxCent(orderRentAmountCent-rentFromPrepaidCent, 0)
depositUsedCent := damageAppliedCent + overshootCoveredCent
depositRefundCent := maxCent(orderDepositAmountCent-depositUsedCent, 0)
// 可完结时租客实际承担的租金向金额;有 shortfall 时仍展示完整应付便于协商
actualRentAmountCent := actualBuyerRentCent
ownerRentIncomeCent := actualOwnerRentCent
platformFeeCent := maxCent(actualRentAmountCent-ownerRentIncomeCent, 0)
depositCompensationCent := damageAppliedCent
return checkoutSettlement{
OwnerRentIncomeCent: ownerRentIncomeCent,
@@ -220,8 +259,10 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
RentRefundCent: rentRefundCent,
DepositRefundCent: depositRefundCent,
RenterRefundCent: rentRefundCent + depositRefundCent,
PlatformFeeCent: maxCent(actualRentAmountCent-ownerRentIncomeCent, 0),
PlatformFeeCent: platformFeeCent,
ActualRentAmountCent: actualRentAmountCent,
OvershootAmountCent: overshootCent,
ShortfallCent: shortfallCent,
}
}
@@ -236,6 +277,15 @@ func readOrderSnapshotCoinM(raw datatypes.JSON) float64 {
return roundQuantity(readJSONNumber(snapshot["haf_coin_amount"]) / 1000000)
}
// coinConsumptionRatio 实际消耗相对发布量的比例,允许 >1 表示打超。
// publishedM<=0 时视为无快照,回退为全额使用(兼容旧单测/无币价场景)。
func coinConsumptionRatio(consumedM, publishedM float64) float64 {
if publishedM > 0 {
return maxRatio(roundQuantity(consumedM)/publishedM, 0)
}
return 1
}
func readOrderSnapshotAssetNumber(raw datatypes.JSON, key string) float64 {
if len(raw) == 0 {
return 0