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 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 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, 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 } 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, 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, checkout.DepositDeductAmountCent) } func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement { orderRentAmountCent := order.RentAmountCent 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) consumableUseRatio := 0.0 if prepaidConsumablePriceCent > 0 { consumableUseRatio = maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0) } usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio)) if usedBuyerConsumablePriceCent <= prepaidConsumablePriceCent { usedBuyerConsumablePriceCent = minCent(usedBuyerConsumablePriceCent, prepaidConsumablePriceCent) usedOwnerConsumablePriceCent = minCent(usedOwnerConsumablePriceCent, prepaidOwnerConsumablePriceCent) } 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 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, ActualRentAmountCent: actualRentAmountCent, PureCoinAmountCent: usedBuyerCoinPriceCent, PureCoinDiscountCent: pureCoinDiscountCent, PureCoinPayableCent: pureCoinPayableCent, ExtraItemAmountCent: usedBuyerConsumablePriceCent, CoinConsumedM: consumedM, OvershootAmountCent: overshootCent, ShortfallCent: shortfallCent, } } 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 }