370 lines
13 KiB
Go
370 lines
13 KiB
Go
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))
|
||
}
|
||
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)
|
||
}
|
||
|
||
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
|
||
}
|