Files
hfb_sys/backend/internal/modules/order/pricing.go
T
yml2213 0ef6a481f5 租期天数改为向上取整到整天
不足一天按一天计(如 1.3/1.7 均计 2 天),前后端展示与预计时长计算保持一致。
2026-07-20 22:14:03 +08:00

355 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package order
import (
"encoding/json"
"math"
"strconv"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/pkg/money"
"gorm.io/datatypes"
)
type orderPricing struct {
RentAmountCent int64
OwnerRentAmountCent int64
PlatformFeeCent int64
}
type checkoutSettlement struct {
OwnerRentIncomeCent int64
DepositCompensationCent int64
OwnerIncomeCent int64
RentRefundCent int64
DepositRefundCent int64
RenterRefundCent int64
PlatformFeeCent int64
ActualRentAmountCent int64
OvershootAmountCent int64
ShortfallCent int64
}
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
}
return orderPricing{
RentAmountCent: rentAmountCent,
OwnerRentAmountCent: ownerRentAmountCent,
PlatformFeeCent: platformFeeCent,
}
}
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,
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 := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100))
if buyerCoinBasePriceCent <= 0 || buyerCoinBasePriceCent > orderRentAmountCent {
buyerCoinBasePriceCent = orderRentAmountCent
}
sellerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100))
if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent {
sellerCoinBasePriceCent = orderOwnerRentAmountCent
}
prepaidConsumablePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100))
if prepaidConsumablePriceCent <= 0 || prepaidConsumablePriceCent > orderRentAmountCent-buyerCoinBasePriceCent {
prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0)
}
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))
}
// 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比线性外推
usedBuyerConsumablePriceCent := maxCent(consumableAmountCent, 0)
consumableUseRatio := 0.0
if prepaidConsumablePriceCent > 0 {
consumableUseRatio = maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0)
}
usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
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,
DepositCompensationCent: depositCompensationCent,
OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent,
RentRefundCent: rentRefundCent,
DepositRefundCent: depositRefundCent,
RenterRefundCent: rentRefundCent + depositRefundCent,
PlatformFeeCent: platformFeeCent,
ActualRentAmountCent: actualRentAmountCent,
OvershootAmountCent: overshootCent,
ShortfallCent: shortfallCent,
}
}
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
}