665 lines
23 KiB
Go
665 lines
23 KiB
Go
package order
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
"hfb_jysh/backend/internal/model"
|
|
"hfb_jysh/backend/internal/modules/rentergrowth"
|
|
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
type orderPricing struct {
|
|
RentAmountCent int64
|
|
OwnerRentAmountCent int64
|
|
PlatformFeeCent int64
|
|
PureCoinAmountCent int64
|
|
OwnerPureCoinAmountCent int64
|
|
ExtraItemAmountCent int64
|
|
OwnerExtraItemAmountCent int64
|
|
PureCoinPlatformFeeCent int64
|
|
}
|
|
|
|
type checkoutSettlement struct {
|
|
OwnerRentIncomeCent int64
|
|
DepositCompensationCent int64
|
|
OwnerIncomeCent int64
|
|
RentRefundCent int64
|
|
DepositRefundCent int64
|
|
RenterRefundCent int64
|
|
PlatformFeeCent int64
|
|
InsuranceFeeBps int
|
|
InsuranceFeeCent int64
|
|
ActualRentAmountCent int64
|
|
PureCoinAmountCent int64
|
|
PureCoinDiscountCent int64
|
|
PureCoinPayableCent int64
|
|
ExtraItemAmountCent int64
|
|
CoinConsumedM float64
|
|
OvershootAmountCent int64
|
|
ShortfallCent int64
|
|
}
|
|
|
|
// ActualPureCoinAmounts 是跨正常结账与仲裁共用的最终纯币计价结果。
|
|
type ActualPureCoinAmounts struct {
|
|
AmountCent int64
|
|
DiscountCent int64
|
|
PayableCent int64
|
|
ConsumedM float64
|
|
}
|
|
|
|
// ActualCheckoutSettlement 是正常结账与仲裁共用的实际消耗结算结果。
|
|
type ActualCheckoutSettlement struct {
|
|
OwnerRentIncomeCent int64
|
|
DepositCompensationCent int64
|
|
OwnerIncomeCent int64
|
|
RentRefundCent int64
|
|
DepositRefundCent int64
|
|
RenterRefundCent int64
|
|
PlatformFeeCent int64
|
|
InsuranceFeeBps int
|
|
InsuranceFeeCent int64
|
|
ActualRentAmountCent int64
|
|
PureCoinAmountCent int64
|
|
PureCoinDiscountCent int64
|
|
PureCoinPayableCent int64
|
|
ExtraItemAmountCent int64
|
|
CoinConsumedM float64
|
|
OvershootAmountCent int64
|
|
ShortfallCent int64
|
|
}
|
|
|
|
// CalculateActualPureCoinAmounts 按订单价格快照和实际消耗 M 计算纯币原价及等级优惠。
|
|
func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float64) ActualPureCoinAmounts {
|
|
settlement := CalculateActualCheckoutSettlement(order, 0, coinConsumedM, 0)
|
|
return ActualPureCoinAmounts{
|
|
AmountCent: settlement.PureCoinAmountCent,
|
|
DiscountCent: settlement.PureCoinDiscountCent,
|
|
PayableCent: settlement.PureCoinPayableCent,
|
|
ConsumedM: settlement.CoinConsumedM,
|
|
}
|
|
}
|
|
|
|
// CalculateActualCheckoutSettlement 按实际纯币和额外物品消耗计算完整结算。
|
|
func CalculateActualCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) ActualCheckoutSettlement {
|
|
return toActualCheckoutSettlement(calculateCheckoutSettlement(order, consumableAmountCent, coinConsumedM, depositDeductAmountCent))
|
|
}
|
|
|
|
func toActualCheckoutSettlement(settlement checkoutSettlement) ActualCheckoutSettlement {
|
|
return ActualCheckoutSettlement{
|
|
OwnerRentIncomeCent: settlement.OwnerRentIncomeCent,
|
|
DepositCompensationCent: settlement.DepositCompensationCent,
|
|
OwnerIncomeCent: settlement.OwnerIncomeCent,
|
|
RentRefundCent: settlement.RentRefundCent,
|
|
DepositRefundCent: settlement.DepositRefundCent,
|
|
RenterRefundCent: settlement.RenterRefundCent,
|
|
PlatformFeeCent: settlement.PlatformFeeCent,
|
|
InsuranceFeeBps: settlement.InsuranceFeeBps,
|
|
InsuranceFeeCent: settlement.InsuranceFeeCent,
|
|
ActualRentAmountCent: settlement.ActualRentAmountCent,
|
|
PureCoinAmountCent: settlement.PureCoinAmountCent,
|
|
PureCoinDiscountCent: settlement.PureCoinDiscountCent,
|
|
PureCoinPayableCent: settlement.PureCoinPayableCent,
|
|
ExtraItemAmountCent: settlement.ExtraItemAmountCent,
|
|
CoinConsumedM: settlement.CoinConsumedM,
|
|
OvershootAmountCent: settlement.OvershootAmountCent,
|
|
ShortfallCent: settlement.ShortfallCent,
|
|
}
|
|
}
|
|
|
|
func orderDurationHours(order model.RentalOrder) int {
|
|
if order.EstimatedDurationHours > 0 {
|
|
return order.EstimatedDurationHours
|
|
}
|
|
return internalOrderHours
|
|
}
|
|
|
|
func orderEstimatedEndAt(order model.RentalOrder) *time.Time {
|
|
if order.RentedAt == nil {
|
|
return nil
|
|
}
|
|
durationHours := orderDurationHours(order)
|
|
if durationHours <= 0 {
|
|
return nil
|
|
}
|
|
endAt := order.RentedAt.Add(time.Duration(durationHours) * time.Hour)
|
|
return &endAt
|
|
}
|
|
|
|
// orderListingTerms 用下单快照里的哈夫币和日损耗估算租期,和首页「可租 / 消耗」同一口径。
|
|
func orderListingTerms(snapshot datatypes.JSON) (days int, dailyLossM float64) {
|
|
dailyLossM = readOrderSnapshotAssetNumber(snapshot, "daily_loss_m")
|
|
coinM := readOrderSnapshotCoinM(snapshot)
|
|
if dailyLossM <= 0 || coinM <= 0 {
|
|
return 0, 0
|
|
}
|
|
days = int(math.Ceil(coinM / dailyLossM))
|
|
if days < 1 {
|
|
days = 1
|
|
}
|
|
return days, roundQuantity(dailyLossM)
|
|
}
|
|
|
|
func estimateOrderDurationHours(snapshot datatypes.JSON) int {
|
|
hafCoinM := readOrderSnapshotCoinM(snapshot)
|
|
if hafCoinM <= 0 {
|
|
return internalOrderHours
|
|
}
|
|
dailyLossM := readOrderSnapshotAssetNumber(snapshot, "daily_loss_m")
|
|
if dailyLossM <= 0 {
|
|
return internalOrderHours
|
|
}
|
|
days := hafCoinM / dailyLossM
|
|
if days <= 0 {
|
|
return internalOrderHours
|
|
}
|
|
// 不足整天按整天向上取整,例如 1.3 / 1.7 天均按 2 天计
|
|
daysCeil := int(math.Ceil(days))
|
|
if daysCeil < 1 {
|
|
daysCeil = 1
|
|
}
|
|
hours := daysCeil * 24
|
|
return hours
|
|
}
|
|
|
|
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
|
|
rentAmountCent := listing.PriceCent
|
|
ownerRentAmountCent := readSnapshotCent(account.AssetSummary, "seller_total_price")
|
|
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
|
ownerRentAmountCent = rentAmountCent
|
|
}
|
|
platformFeeCent := readSnapshotCent(account.AssetSummary, "platform_markup_amount")
|
|
if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent {
|
|
platformFeeCent = rentAmountCent - ownerRentAmountCent
|
|
}
|
|
if platformFeeCent < 0 {
|
|
platformFeeCent = 0
|
|
}
|
|
pureCoinAmountCent := readSnapshotCent(account.AssetSummary, "buyer_coin_base_price")
|
|
extraItemSnapshotCent := readSnapshotCent(account.AssetSummary, "consumable_price")
|
|
if pureCoinAmountCent <= 0 {
|
|
if extraItemSnapshotCent > 0 && extraItemSnapshotCent <= rentAmountCent {
|
|
pureCoinAmountCent = rentAmountCent - extraItemSnapshotCent
|
|
} else {
|
|
pureCoinAmountCent = rentAmountCent
|
|
}
|
|
}
|
|
if pureCoinAmountCent < 0 || pureCoinAmountCent > rentAmountCent {
|
|
pureCoinAmountCent = rentAmountCent
|
|
}
|
|
extraItemAmountCent := maxCent(rentAmountCent-pureCoinAmountCent, 0)
|
|
ownerPureCoinAmountCent := readSnapshotCent(account.AssetSummary, "seller_coin_base_price")
|
|
if ownerPureCoinAmountCent <= 0 || ownerPureCoinAmountCent > ownerRentAmountCent {
|
|
ownerPureCoinAmountCent = maxCent(ownerRentAmountCent-minCent(extraItemAmountCent, ownerRentAmountCent), 0)
|
|
}
|
|
ownerExtraItemAmountCent := maxCent(ownerRentAmountCent-ownerPureCoinAmountCent, 0)
|
|
pureCoinPlatformFeeCent := maxCent(pureCoinAmountCent-ownerPureCoinAmountCent, 0)
|
|
return orderPricing{
|
|
RentAmountCent: rentAmountCent,
|
|
OwnerRentAmountCent: ownerRentAmountCent,
|
|
PlatformFeeCent: platformFeeCent,
|
|
PureCoinAmountCent: pureCoinAmountCent,
|
|
OwnerPureCoinAmountCent: ownerPureCoinAmountCent,
|
|
ExtraItemAmountCent: extraItemAmountCent,
|
|
OwnerExtraItemAmountCent: ownerExtraItemAmountCent,
|
|
PureCoinPlatformFeeCent: pureCoinPlatformFeeCent,
|
|
}
|
|
}
|
|
|
|
func readSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
|
return readOrderSnapshotPrice(raw, key)
|
|
}
|
|
|
|
// readSnapshotCent 优先读取 price_breakdown 的整数分字段,缺失时兼容旧元字段。
|
|
func readSnapshotCent(raw datatypes.JSON, key string) int64 {
|
|
return readOrderSnapshotCent(raw, key)
|
|
}
|
|
|
|
func readOrderSnapshotCent(raw datatypes.JSON, key string) int64 {
|
|
breakdown, ok := readOrderSnapshotBreakdown(raw)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
if cent := readJSONInt64(breakdown[key+"_cent"]); cent > 0 {
|
|
return cent
|
|
}
|
|
legacy := readJSONNumber(breakdown[key])
|
|
if legacy <= 0 {
|
|
return 0
|
|
}
|
|
return int64(math.Round(legacy * 100))
|
|
}
|
|
|
|
func readOrderSnapshotBreakdown(raw datatypes.JSON) (map[string]any, bool) {
|
|
if len(raw) == 0 {
|
|
return nil, false
|
|
}
|
|
var snapshot map[string]any
|
|
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
|
return nil, false
|
|
}
|
|
if assetSummary, ok := snapshot["asset_summary"].(map[string]any); ok {
|
|
snapshot = assetSummary
|
|
}
|
|
breakdown, ok := snapshot["price_breakdown"].(map[string]any)
|
|
return breakdown, ok
|
|
}
|
|
|
|
func readJSONInt64(value any) int64 {
|
|
switch typed := value.(type) {
|
|
case float64:
|
|
return int64(typed)
|
|
case float32:
|
|
return int64(typed)
|
|
case int:
|
|
return int64(typed)
|
|
case int64:
|
|
return typed
|
|
case int32:
|
|
return int64(typed)
|
|
case json.Number:
|
|
number, err := typed.Int64()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return number
|
|
case string:
|
|
number, err := strconv.ParseInt(typed, 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return number
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func readOrderSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
|
breakdown, ok := readOrderSnapshotBreakdown(raw)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
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:
|
|
return typed
|
|
case float32:
|
|
return float64(typed)
|
|
case int:
|
|
return float64(typed)
|
|
case int64:
|
|
return float64(typed)
|
|
case json.Number:
|
|
number, err := typed.Float64()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return number
|
|
case string:
|
|
number, err := strconv.ParseFloat(typed, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return number
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmountCent int64, coinConsumedM float64, otherAmountCent int64, explicitDeductCent int64, useExplicitDeduct bool) (model.OrderCheckout, error) {
|
|
if consumableAmountCent < 0 || coinConsumedM < 0 || otherAmountCent < 0 || explicitDeductCent < 0 {
|
|
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
|
|
}
|
|
deductAmountCent := otherAmountCent
|
|
if useExplicitDeduct {
|
|
deductAmountCent = explicitDeductCent
|
|
}
|
|
settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent)
|
|
evidence, err := marshalStringList(evidenceURLS)
|
|
if err != nil {
|
|
return model.OrderCheckout{}, err
|
|
}
|
|
return model.OrderCheckout{
|
|
OrderID: order.ID,
|
|
InitiatedBy: initiatedBy,
|
|
Status: status,
|
|
RentAmountCent: settlement.ActualRentAmountCent,
|
|
OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
|
|
PlatformFeeCent: settlement.PlatformFeeCent,
|
|
InsuranceFeeBps: settlement.InsuranceFeeBps,
|
|
InsuranceFeeCent: settlement.InsuranceFeeCent,
|
|
DepositAmountCent: order.DepositAmountCent,
|
|
PureCoinAmountCent: settlement.PureCoinAmountCent,
|
|
PureCoinDiscountCent: settlement.PureCoinDiscountCent,
|
|
PureCoinPayableCent: settlement.PureCoinPayableCent,
|
|
ConsumableAmountCent: consumableAmountCent,
|
|
CoinConsumedM: roundQuantity(coinConsumedM),
|
|
OtherAmountCent: otherAmountCent,
|
|
// 存用户申报的押金赔付(损坏等),打超由结算自动计入 shortfall/overshoot
|
|
DepositDeductAmountCent: deductAmountCent,
|
|
RenterRefundAmountCent: settlement.RenterRefundCent,
|
|
OwnerIncomeAmountCent: settlement.OwnerIncomeCent,
|
|
ShortfallCent: settlement.ShortfallCent,
|
|
OvershootAmountCent: settlement.OvershootAmountCent,
|
|
Content: content,
|
|
EvidenceURLS: evidence,
|
|
}, nil
|
|
}
|
|
|
|
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
|
|
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(*checkout))
|
|
}
|
|
|
|
// effectiveCheckoutDepositDeductCent keeps legacy checkouts that only populated
|
|
// other_amount_cent from losing their deposit compensation at finalization.
|
|
func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
|
|
if checkout.DepositDeductAmountCent > 0 {
|
|
return checkout.DepositDeductAmountCent
|
|
}
|
|
return maxCent(checkout.OtherAmountCent, 0)
|
|
}
|
|
|
|
func orderConsumptionPrepaidCent(order model.RentalOrder) int64 {
|
|
return order.RentAmountCent
|
|
}
|
|
|
|
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
|
orderRentAmountCent := orderConsumptionPrepaidCent(order)
|
|
orderOwnerRentAmountCent := order.OwnerRentAmountCent
|
|
orderDepositAmountCent := order.DepositAmountCent
|
|
|
|
buyerCoinBasePriceCent := orderPureCoinOriginalAmountCent(order)
|
|
sellerCoinBasePriceCent := readOrderSnapshotCent(order.AccountSnapshot, "seller_coin_base_price")
|
|
if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent {
|
|
ownerExtraItemCent := minCent(orderExtraItemOriginalAmountCent(order), orderOwnerRentAmountCent)
|
|
sellerCoinBasePriceCent = maxCent(orderOwnerRentAmountCent-ownerExtraItemCent, 0)
|
|
}
|
|
prepaidConsumablePriceCent := orderExtraItemOriginalAmountCent(order)
|
|
prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0)
|
|
|
|
consumedM := roundQuantity(coinConsumedM)
|
|
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
|
|
buyerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "buyer_ratio")
|
|
sellerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "seller_ratio")
|
|
consumedUnits := quantityUnits(consumedM)
|
|
totalCoinUnits := quantityUnits(totalCoinM)
|
|
|
|
// 币费:比例优先按 bps 整数换算;缺失 bps 时把旧比例换算成 bps。
|
|
// 无比例时回退到预收币基价按消耗比例折算,仍使用整数除法。
|
|
var usedBuyerCoinPriceCent, usedOwnerCoinPriceCent int64
|
|
if buyerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
|
|
usedBuyerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, buyerRatioBPS)
|
|
} else {
|
|
usedBuyerCoinPriceCent = proportionalCent(buyerCoinBasePriceCent, consumedUnits, totalCoinUnits)
|
|
}
|
|
if sellerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
|
|
usedOwnerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, sellerRatioBPS)
|
|
} else {
|
|
usedOwnerCoinPriceCent = proportionalCent(sellerCoinBasePriceCent, consumedUnits, totalCoinUnits)
|
|
}
|
|
coinOvershoot := totalCoinUnits > 0 && consumedUnits > totalCoinUnits
|
|
if !coinOvershoot {
|
|
// 未打超时,比例反推只能用于折算未用完部分,不能因取整超过订单快照。
|
|
if totalCoinUnits > 0 && consumedUnits >= totalCoinUnits {
|
|
usedBuyerCoinPriceCent = buyerCoinBasePriceCent
|
|
usedOwnerCoinPriceCent = sellerCoinBasePriceCent
|
|
} else {
|
|
usedBuyerCoinPriceCent = minCent(usedBuyerCoinPriceCent, buyerCoinBasePriceCent)
|
|
usedOwnerCoinPriceCent = minCent(usedOwnerCoinPriceCent, sellerCoinBasePriceCent)
|
|
}
|
|
}
|
|
|
|
// 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比外推,超额及无预收损耗全额归号主
|
|
usedBuyerConsumablePriceCent := maxCent(consumableAmountCent, 0)
|
|
var usedOwnerConsumablePriceCent int64
|
|
if prepaidConsumablePriceCent > 0 {
|
|
if usedBuyerConsumablePriceCent <= prepaidConsumablePriceCent {
|
|
usedOwnerConsumablePriceCent = roundedDiv(
|
|
prepaidOwnerConsumablePriceCent*usedBuyerConsumablePriceCent,
|
|
prepaidConsumablePriceCent,
|
|
)
|
|
usedOwnerConsumablePriceCent = minCent(usedOwnerConsumablePriceCent, prepaidOwnerConsumablePriceCent)
|
|
} else {
|
|
// 预收范围内按号主预收基准,超出预收部分作为后置损耗全额划归号主(平台不抽成)
|
|
excessCent := usedBuyerConsumablePriceCent - prepaidConsumablePriceCent
|
|
usedOwnerConsumablePriceCent = prepaidOwnerConsumablePriceCent + excessCent
|
|
}
|
|
} else {
|
|
// 原订单无预收消耗品,后置申报的消耗/损耗金额 100% 归号主,平台不抽成
|
|
usedOwnerConsumablePriceCent = usedBuyerConsumablePriceCent
|
|
}
|
|
|
|
pureCoinPlatformFeeCent := maxCent(usedBuyerCoinPriceCent-usedOwnerCoinPriceCent, 0)
|
|
pureCoinDiscountCent := rentergrowth.CalculateDiscountCent(usedBuyerCoinPriceCent, pureCoinPlatformFeeCent, effectiveRenterDiscountBps(order))
|
|
pureCoinPayableCent := maxCent(usedBuyerCoinPriceCent-pureCoinDiscountCent, 0)
|
|
actualBuyerRentCent := pureCoinPayableCent + 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
|
|
insuranceFeeCent := calculateInsuranceFeeCent(usedOwnerCoinPriceCent, order.InsuranceFeeBps)
|
|
ownerRentIncomeCent = maxCent(ownerRentIncomeCent-insuranceFeeCent, 0)
|
|
platformFeeCent := maxCent(actualRentAmountCent-ownerRentIncomeCent, 0)
|
|
depositCompensationCent := damageAppliedCent
|
|
|
|
return checkoutSettlement{
|
|
OwnerRentIncomeCent: ownerRentIncomeCent,
|
|
DepositCompensationCent: depositCompensationCent,
|
|
OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent,
|
|
RentRefundCent: rentRefundCent,
|
|
DepositRefundCent: depositRefundCent,
|
|
RenterRefundCent: rentRefundCent + depositRefundCent,
|
|
PlatformFeeCent: platformFeeCent,
|
|
InsuranceFeeBps: order.InsuranceFeeBps,
|
|
InsuranceFeeCent: insuranceFeeCent,
|
|
ActualRentAmountCent: actualRentAmountCent,
|
|
PureCoinAmountCent: usedBuyerCoinPriceCent,
|
|
PureCoinDiscountCent: pureCoinDiscountCent,
|
|
PureCoinPayableCent: pureCoinPayableCent,
|
|
ExtraItemAmountCent: usedBuyerConsumablePriceCent,
|
|
CoinConsumedM: consumedM,
|
|
OvershootAmountCent: overshootCent,
|
|
ShortfallCent: shortfallCent,
|
|
}
|
|
}
|
|
|
|
func calculateInsuranceFeeCent(ownerRentIncomeCent int64, feeBps int) int64 {
|
|
if ownerRentIncomeCent <= 0 || feeBps <= 0 {
|
|
return 0
|
|
}
|
|
feeCent := (ownerRentIncomeCent*int64(feeBps) + 5_000) / 10_000
|
|
return minCent(feeCent, ownerRentIncomeCent)
|
|
}
|
|
|
|
func orderPureCoinOriginalAmountCent(order model.RentalOrder) int64 {
|
|
originalRentCent := effectiveRentOriginalAmountCent(order)
|
|
if order.PureCoinOriginalAmountCent > 0 {
|
|
return minCent(order.PureCoinOriginalAmountCent, originalRentCent)
|
|
}
|
|
if order.ExtraItemOriginalAmountCent > 0 {
|
|
return maxCent(originalRentCent-minCent(order.ExtraItemOriginalAmountCent, originalRentCent), 0)
|
|
}
|
|
pureCoinCent := readOrderSnapshotCent(order.AccountSnapshot, "buyer_coin_base_price")
|
|
if pureCoinCent > 0 && pureCoinCent <= originalRentCent {
|
|
return pureCoinCent
|
|
}
|
|
extraItemCent := readOrderSnapshotCent(order.AccountSnapshot, "consumable_price")
|
|
if extraItemCent > 0 && extraItemCent <= originalRentCent {
|
|
return originalRentCent - extraItemCent
|
|
}
|
|
return originalRentCent
|
|
}
|
|
|
|
func orderExtraItemOriginalAmountCent(order model.RentalOrder) int64 {
|
|
originalRentCent := effectiveRentOriginalAmountCent(order)
|
|
if order.ExtraItemOriginalAmountCent > 0 {
|
|
return minCent(order.ExtraItemOriginalAmountCent, originalRentCent)
|
|
}
|
|
pureCoinCent := orderPureCoinOriginalAmountCent(order)
|
|
return maxCent(originalRentCent-pureCoinCent, 0)
|
|
}
|
|
|
|
func readOrderSnapshotCoinM(raw datatypes.JSON) float64 {
|
|
if len(raw) == 0 {
|
|
return 0
|
|
}
|
|
var snapshot map[string]any
|
|
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
|
return 0
|
|
}
|
|
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
|
|
}
|
|
var snapshot map[string]any
|
|
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
|
return 0
|
|
}
|
|
assetSummary, ok := snapshot["asset_summary"].(map[string]any)
|
|
if !ok {
|
|
return readJSONNumber(snapshot[key])
|
|
}
|
|
if value, ok := assetSummary[key]; ok {
|
|
return readJSONNumber(value)
|
|
}
|
|
return readJSONNumber(snapshot[key])
|
|
}
|
|
|
|
// roundMoney 是订单历史链路的角精度兼容实现;新金额链路应直接使用整数分。
|
|
func roundMoney(value float64) float64 {
|
|
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return 0
|
|
}
|
|
return math.Round(value*10) / 10
|
|
}
|
|
|
|
func roundQuantity(value float64) float64 {
|
|
return math.Round(value*100) / 100
|
|
}
|
|
|
|
// minMoney 返回较小金额(角精度兼容)。
|
|
func minMoney(a float64, b float64) float64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// maxMoney 返回较大金额(角精度兼容)。
|
|
func maxMoney(a float64, b float64) float64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func minCent(a int64, b int64) int64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func maxCent(a int64, b int64) int64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func minRatio(a float64, b float64) float64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func maxRatio(a float64, b float64) float64 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|