修复结账金额和后台类型

This commit is contained in:
yml
2026-05-27 10:28:05 +08:00
parent 6fb5de204c
commit c54e3936ab
7 changed files with 311 additions and 48 deletions
+174 -28
View File
@@ -40,6 +40,17 @@ type orderPricing struct {
PlatformFee float64
}
type checkoutSettlement struct {
OwnerRentIncome float64
DepositCompensation float64
OwnerIncome float64
RentRefund float64
DepositRefund float64
RenterRefund float64
PlatformFee float64
ActualRentAmount float64
}
func orderDurationHours(order model.RentalOrder) int {
if order.EstimatedDurationHours > 0 {
return order.EstimatedDurationHours
@@ -68,14 +79,21 @@ func buildOrderPricing(listing model.RentalListing, account model.GameAccount) o
}
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 summary map[string]any
if err := json.Unmarshal(raw, &summary); err != nil {
var snapshot map[string]any
if err := json.Unmarshal(raw, &snapshot); err != nil {
return 0
}
breakdown, ok := summary["price_breakdown"].(map[string]any)
if assetSummary, ok := snapshot["asset_summary"].(map[string]any); ok {
snapshot = assetSummary
}
breakdown, ok := snapshot["price_breakdown"].(map[string]any)
if !ok {
return 0
}
@@ -927,7 +945,8 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
listing.InTransaction = false
account.Status = "published"
orderID := order.ID
if err := wallet.AppendEntries(tx,
settlement := buildCheckoutSettlement(*order, checkout)
entries := []wallet.Entry{
wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
@@ -938,29 +957,63 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
BizNo: order.OrderNo,
Remark: "订单结账释放冻结金额",
},
wallet.Entry{
}
if settlement.OwnerRentIncome > 0 {
entries = append(entries, wallet.Entry{
UserID: order.OwnerID,
OrderID: &orderID,
Direction: "in",
Amount: checkout.OwnerIncomeAmount,
Amount: settlement.OwnerRentIncome,
BalanceType: "available",
BizType: "owner_income",
BizNo: order.OrderNo,
Remark: "订单结账收入",
},
wallet.Entry{
Remark: "订单结账租金收入",
})
}
if settlement.DepositCompensation > 0 {
entries = append(entries, wallet.Entry{
UserID: order.OwnerID,
OrderID: &orderID,
Direction: "in",
Amount: settlement.DepositCompensation,
BalanceType: "available",
BizType: "deposit_compensation",
BizNo: order.OrderNo,
Remark: "订单结账押金赔付",
})
}
if settlement.RentRefund > 0 {
entries = append(entries, wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "in",
Amount: checkout.RenterRefundAmount,
Amount: settlement.RentRefund,
BalanceType: "available",
BizType: "rent_refund",
BizNo: order.OrderNo,
Remark: "订单结账退回未使用租金",
})
}
if settlement.DepositRefund > 0 {
entries = append(entries, wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "in",
Amount: settlement.DepositRefund,
BalanceType: "available",
BizType: "deposit_release",
BizNo: order.OrderNo,
Remark: "订单结账退回押金",
},
); err != nil {
})
}
if err := wallet.AppendEntries(tx, entries...); err != nil {
return err
}
checkout.RentAmount = settlement.ActualRentAmount
checkout.OwnerRentAmount = settlement.OwnerRentIncome
checkout.PlatformFee = settlement.PlatformFee
checkout.RenterRefundAmount = settlement.RenterRefund
checkout.OwnerIncomeAmount = settlement.OwnerIncome
if err := notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
@@ -1049,7 +1102,7 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
if deductAmount > order.DepositAmount {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
}
renterRefund := order.DepositAmount - deductAmount
settlement := calculateCheckoutSettlement(order, consumableAmount, roundQuantity(coinConsumedM), deductAmount)
evidence, err := marshalStringList(evidenceURLS)
if err != nil {
return model.OrderCheckout{}, err
@@ -1058,21 +1111,80 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
OrderID: order.ID,
InitiatedBy: initiatedBy,
Status: status,
RentAmount: order.RentAmount,
OwnerRentAmount: order.OwnerRentAmount,
PlatformFee: order.PlatformFee,
RentAmount: settlement.ActualRentAmount,
OwnerRentAmount: settlement.OwnerRentIncome,
PlatformFee: settlement.PlatformFee,
DepositAmount: order.DepositAmount,
ConsumableAmount: consumableAmount,
CoinConsumedM: roundQuantity(coinConsumedM),
OtherAmount: otherAmount,
DepositDeductAmount: roundMoney(deductAmount),
RenterRefundAmount: roundMoney(renterRefund),
OwnerIncomeAmount: roundMoney(order.OwnerRentAmount + deductAmount),
RenterRefundAmount: settlement.RenterRefund,
OwnerIncomeAmount: settlement.OwnerIncome,
Content: content,
EvidenceURLS: evidence,
}, nil
}
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
return calculateCheckoutSettlement(order, checkout.ConsumableAmount, checkout.CoinConsumedM, checkout.DepositDeductAmount)
}
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmount float64, coinConsumedM float64, depositDeductAmount float64) checkoutSettlement {
buyerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price")
if buyerCoinBasePrice <= 0 || buyerCoinBasePrice > order.RentAmount {
buyerCoinBasePrice = order.RentAmount
}
sellerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price")
if sellerCoinBasePrice <= 0 || sellerCoinBasePrice > order.OwnerRentAmount {
sellerCoinBasePrice = order.OwnerRentAmount
}
prepaidConsumablePrice := readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price")
if prepaidConsumablePrice <= 0 || prepaidConsumablePrice > order.RentAmount-buyerCoinBasePrice {
prepaidConsumablePrice = maxMoney(order.RentAmount-buyerCoinBasePrice, 0)
}
prepaidOwnerConsumablePrice := maxMoney(order.OwnerRentAmount-sellerCoinBasePrice, 0)
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
coinUseRatio := 1.0
if totalCoinM > 0 {
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
}
usedBuyerCoinPrice := roundMoney(buyerCoinBasePrice * coinUseRatio)
usedOwnerCoinPrice := roundMoney(sellerCoinBasePrice * coinUseRatio)
usedBuyerConsumablePrice := minMoney(roundMoney(consumableAmount), prepaidConsumablePrice)
consumableUseRatio := 1.0
if prepaidConsumablePrice > 0 {
consumableUseRatio = minRatio(maxRatio(usedBuyerConsumablePrice/prepaidConsumablePrice, 0), 1)
}
usedOwnerConsumablePrice := roundMoney(prepaidOwnerConsumablePrice * consumableUseRatio)
actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), order.RentAmount)
ownerRentIncome := minMoney(roundMoney(usedOwnerCoinPrice+usedOwnerConsumablePrice), order.OwnerRentAmount)
depositCompensation := minMoney(roundMoney(depositDeductAmount), order.DepositAmount)
rentRefund := maxMoney(order.RentAmount-actualRentAmount, 0)
depositRefund := maxMoney(order.DepositAmount-depositCompensation, 0)
return checkoutSettlement{
OwnerRentIncome: ownerRentIncome,
DepositCompensation: depositCompensation,
OwnerIncome: roundMoney(ownerRentIncome + depositCompensation),
RentRefund: rentRefund,
DepositRefund: depositRefund,
RenterRefund: roundMoney(rentRefund + depositRefund),
PlatformFee: maxMoney(actualRentAmount-ownerRentIncome, 0),
ActualRentAmount: actualRentAmount,
}
}
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)
}
func marshalStringList(items []string) (datatypes.JSON, error) {
if items == nil {
items = []string{}
@@ -1100,6 +1212,34 @@ func roundQuantity(value float64) float64 {
return math.Round(value*100) / 100
}
func minMoney(a float64, b float64) float64 {
if a < b {
return roundMoney(a)
}
return roundMoney(b)
}
func maxMoney(a float64, b float64) float64 {
if a > b {
return roundMoney(a)
}
return roundMoney(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
}
func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
var count int64
err := tx.Model(&model.OrderCheckout{}).
@@ -1288,27 +1428,33 @@ func applyCheckoutPriceView(dto *CheckoutDTO, order model.RentalOrder, userID ui
if dto == nil {
return
}
ownerAmount := 0.0
if dto.OwnerRentAmount != nil {
ownerAmount = *dto.OwnerRentAmount
}
ownerIncomeAmount := 0.0
if dto.OwnerIncomeAmount != nil {
ownerIncomeAmount = *dto.OwnerIncomeAmount
}
rentAmount := 0.0
if dto.RentAmount != nil {
rentAmount = *dto.RentAmount
}
renterRefundAmount := 0.0
if dto.RenterRefundAmount != nil {
renterRefundAmount = *dto.RenterRefundAmount
}
dto.PlatformFee = nil
dto.RenterRefundAmount = nil
dto.OwnerIncomeAmount = nil
switch {
case userID == order.OwnerID:
ownerAmount := order.OwnerRentAmount
if ownerAmount <= 0 {
ownerAmount = order.RentAmount
}
ownerIncomeAmount := dto.DepositDeductAmount + ownerAmount
dto.PriceRole = "owner"
dto.DisplayAmount = ownerAmount
dto.RentAmount = nil
dto.OwnerRentAmount = &ownerAmount
dto.OwnerIncomeAmount = &ownerIncomeAmount
case userID == order.RenterID:
rentAmount := order.RentAmount
renterRefundAmount := order.DepositAmount - dto.DepositDeductAmount
if renterRefundAmount < 0 {
renterRefundAmount = 0
}
dto.PriceRole = "renter"
dto.DisplayAmount = rentAmount
dto.RentAmount = &rentAmount
@@ -0,0 +1,114 @@
package order
import (
"testing"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
)
func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
order := model.RentalOrder{
RentAmount: 383,
OwnerRentAmount: 353,
DepositAmount: 150,
AccountSnapshot: datatypes.JSON([]byte(`{
"haf_coin_amount": 100000000,
"asset_summary": {
"price_breakdown": {
"buyer_coin_base_price": 263,
"seller_coin_base_price": 233,
"consumable_price": 120
}
}
}`)),
}
settlement := calculateCheckoutSettlement(order, 7, 90, 0)
if settlement.ActualRentAmount != 244 {
t.Fatalf("ActualRentAmount = %.2f, want 244.00", settlement.ActualRentAmount)
}
if settlement.OwnerRentIncome != 217 {
t.Fatalf("OwnerRentIncome = %.2f, want 217.00", settlement.OwnerRentIncome)
}
if settlement.PlatformFee != 27 {
t.Fatalf("PlatformFee = %.2f, want 27.00", settlement.PlatformFee)
}
if settlement.RentRefund != 139 {
t.Fatalf("RentRefund = %.2f, want 139.00", settlement.RentRefund)
}
if settlement.DepositRefund != 150 {
t.Fatalf("DepositRefund = %.2f, want 150.00", settlement.DepositRefund)
}
if settlement.RenterRefund != 289 {
t.Fatalf("RenterRefund = %.2f, want 289.00", settlement.RenterRefund)
}
}
func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testing.T) {
order := model.RentalOrder{
RentAmount: 383,
OwnerRentAmount: 353,
DepositAmount: 150,
AccountSnapshot: datatypes.JSON([]byte(`{
"haf_coin_amount": 100000000,
"asset_summary": {
"price_breakdown": {
"buyer_coin_base_price": 263,
"seller_coin_base_price": 233,
"consumable_price": 120
}
}
}`)),
}
settlement := calculateCheckoutSettlement(order, 0, 50, 0)
if settlement.ActualRentAmount != 132 {
t.Fatalf("租客侧实际租金 = %.2f, want 132.00", settlement.ActualRentAmount)
}
if settlement.OwnerRentIncome != 117 {
t.Fatalf("卖家侧租金收入 = %.2f, want 117.00", settlement.OwnerRentIncome)
}
if settlement.PlatformFee != 15 {
t.Fatalf("平台差价 = %.2f, want 15.00", settlement.PlatformFee)
}
}
func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
order := model.RentalOrder{
RentAmount: 383,
OwnerRentAmount: 353,
DepositAmount: 150,
AccountSnapshot: datatypes.JSON([]byte(`{
"haf_coin_amount": 100000000,
"asset_summary": {
"price_breakdown": {
"buyer_coin_base_price": 263,
"seller_coin_base_price": 233,
"consumable_price": 120
}
}
}`)),
}
settlement := calculateCheckoutSettlement(order, 120, 100, 30)
if settlement.ActualRentAmount != 383 {
t.Fatalf("ActualRentAmount = %.2f, want 383.00", settlement.ActualRentAmount)
}
if settlement.OwnerRentIncome != 353 {
t.Fatalf("OwnerRentIncome = %.2f, want 353.00", settlement.OwnerRentIncome)
}
if settlement.DepositCompensation != 30 {
t.Fatalf("DepositCompensation = %.2f, want 30.00", settlement.DepositCompensation)
}
if settlement.OwnerIncome != 383 {
t.Fatalf("OwnerIncome = %.2f, want 383.00", settlement.OwnerIncome)
}
if settlement.RenterRefund != 120 {
t.Fatalf("RenterRefund = %.2f, want 120.00", settlement.RenterRefund)
}
}