package order import ( "encoding/json" "math" "strconv" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/rentergrowth" "hfb_sys/backend/pkg/money" "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 := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100)) if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent { ownerRentAmountCent = rentAmountCent } platformFeeCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "platform_markup_amount") * 100)) if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent { platformFeeCent = rentAmountCent - ownerRentAmountCent } if platformFeeCent < 0 { platformFeeCent = 0 } pureCoinAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "buyer_coin_base_price") * 100)) extraItemSnapshotCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "consumable_price") * 100)) 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 := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_coin_base_price") * 100)) 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) } func readOrderSnapshotPrice(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 } if assetSummary, ok := snapshot["asset_summary"].(map[string]any); ok { snapshot = assetSummary } breakdown, ok := snapshot["price_breakdown"].(map[string]any) if !ok { return 0 } return readJSONNumber(breakdown[key]) } 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 := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100)) 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) 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)) } if sellerRatio > 0 && (totalCoinM > 0 || consumedM > 0) { usedOwnerCoinPriceCent = int64(math.Round(consumedM * 10000 / sellerRatio)) } else { usedOwnerCoinPriceCent = int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio)) } coinOvershoot := totalCoinM > 0 && consumedM > totalCoinM if !coinOvershoot { // 未打超时,比例反推只能用于折算未用完部分,不能因四舍五入超过订单快照。 if totalCoinM > 0 && consumedM >= totalCoinM { 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 { consumableUseRatio := maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0) usedOwnerConsumablePriceCent = int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio)) 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 := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100)) if pureCoinCent > 0 && pureCoinCent <= originalRentCent { return pureCoinCent } extraItemCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100)) 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 使用统一的角精度(0.1元) func roundMoney(value float64) float64 { return money.Round(value) } func roundQuantity(value float64) float64 { return math.Round(value*100) / 100 } // minMoney 返回较小金额(角精度) func minMoney(a float64, b float64) float64 { return money.Min(a, b) } // maxMoney 返回较大金额(角精度) func maxMoney(a float64, b float64) float64 { return money.Max(a, 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 }