diff --git a/backend/internal/modules/order/pricing.go b/backend/internal/modules/order/pricing.go
index c1d4be7..f4ed584 100644
--- a/backend/internal/modules/order/pricing.go
+++ b/backend/internal/modules/order/pricing.go
@@ -340,16 +340,22 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
}
}
- // 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比线性外推
+ // 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比外推,超额及无预收损耗全额归号主
usedBuyerConsumablePriceCent := maxCent(consumableAmountCent, 0)
- consumableUseRatio := 0.0
+ var usedOwnerConsumablePriceCent int64
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)
+ 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)
diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go
index 371400e..84490ad 100644
--- a/backend/internal/modules/order/repository_integration_test.go
+++ b/backend/internal/modules/order/repository_integration_test.go
@@ -513,8 +513,8 @@ func TestBuildCheckoutSettlementWithConsumable(t *testing.T) {
settlement.DepositCompensationCent, checkout.DepositDeductAmountCent)
}
- // 验证号主总收入 = 租金 + 押金赔付
- expectedOwnerIncome := order.OwnerRentAmountCent + checkout.DepositDeductAmountCent
+ // 验证号主总收入 = 租金(含后置消耗品全额划归号主) + 押金赔付
+ expectedOwnerIncome := order.OwnerRentAmountCent + checkout.ConsumableAmountCent + checkout.DepositDeductAmountCent
if settlement.OwnerIncomeCent != expectedOwnerIncome {
t.Fatalf("OwnerIncomeCent = %d, want %d",
settlement.OwnerIncomeCent, expectedOwnerIncome)
diff --git a/backend/internal/modules/order/repository_test.go b/backend/internal/modules/order/repository_test.go
index e49aae4..6b6c07c 100644
--- a/backend/internal/modules/order/repository_test.go
+++ b/backend/internal/modules/order/repository_test.go
@@ -552,3 +552,145 @@ func TestApplyAdminOrderFiltersSmartRouting(t *testing.T) {
})
}
}
+
+func TestCalculateCheckoutSettlement_NoPrepaidConsumable_OwnerGetsFullLoss(t *testing.T) {
+ // 原订单无预收消耗品(consumable_price: 0),买家币价 263 元,卖家币价 233 元,平台加价 30 元
+ order := model.RentalOrder{
+ RentAmountCent: 26300,
+ OwnerRentAmountCent: 23300,
+ DepositAmountCent: 10000,
+ AccountSnapshot: datatypes.JSON([]byte(`{
+ "haf_coin_amount": 100000000,
+ "asset_summary": {
+ "price_breakdown": {
+ "buyer_coin_base_price": 263,
+ "seller_coin_base_price": 233,
+ "consumable_price": 0
+ }
+ }
+ }`)),
+ }
+
+ // 结账时,耗币 100M(全额使用,原价 263/233),但号主后置申报了 50 元物资损耗(5000 分)
+ settlement := calculateCheckoutSettlement(order, 5000, 100, 0)
+
+ // 1. 租客实际承担租金 = 纯币 26300 + 损耗 5000 = 31300
+ if settlement.ActualRentAmountCent != 31300 {
+ t.Fatalf("ActualRentAmountCent = %d, want 31300", settlement.ActualRentAmountCent)
+ }
+
+ // 2. 号主租金收益 = 纯币 23300 + 损耗 5000(全额划归号主) = 28300
+ if settlement.OwnerRentIncomeCent != 28300 {
+ t.Fatalf("OwnerRentIncomeCent = %d, want 28300 (号主损耗必须全额划归号主)", settlement.OwnerRentIncomeCent)
+ }
+
+ // 3. 号主总收入必须包含全部损耗
+ if settlement.OwnerIncomeCent != 28300 {
+ t.Fatalf("OwnerIncomeCent = %d, want 28300", settlement.OwnerIncomeCent)
+ }
+
+ // 4. 平台服务费必须保持原加价 3000(30 元),绝不能把号主的 50 元损耗吞掉!
+ if settlement.PlatformFeeCent != 3000 {
+ t.Fatalf("PlatformFeeCent = %d, want 3000 (平台绝不能从号主损耗中抽成)", settlement.PlatformFeeCent)
+ }
+
+ // 5. 打超金额 = 31300 - 26300 = 5000,从押金扣除
+ if settlement.OvershootAmountCent != 5000 {
+ t.Fatalf("OvershootAmountCent = %d, want 5000", settlement.OvershootAmountCent)
+ }
+ // 押金 10000 - 5000 = 5000 退给租客
+ if settlement.DepositRefundCent != 5000 {
+ t.Fatalf("DepositRefundCent = %d, want 5000", settlement.DepositRefundCent)
+ }
+}
+
+func TestCalculateCheckoutSettlement_PrepaidConsumableOvershoot_OwnerGetsExcess(t *testing.T) {
+ // 原订单有预收消耗品:买家预收 20 元,号主预收 16 元(平台抽 4 元);纯币 100 / 80
+ order := model.RentalOrder{
+ RentAmountCent: 12000,
+ OwnerRentAmountCent: 9600,
+ DepositAmountCent: 5000,
+ AccountSnapshot: datatypes.JSON([]byte(`{
+ "haf_coin_amount": 100000000,
+ "asset_summary": {
+ "price_breakdown": {
+ "buyer_coin_base_price": 100,
+ "seller_coin_base_price": 80,
+ "consumable_price": 20
+ }
+ }
+ }`)),
+ }
+
+ // 实际消耗品达到 50 元(5000 分),超出预收 30 元(3000 分)
+ settlement := calculateCheckoutSettlement(order, 5000, 100, 0)
+
+ // 号主预收部分 1600 + 超额损耗部分 3000 = 4600;纯币 8000;总号主租金 12600
+ if settlement.OwnerRentIncomeCent != 12600 {
+ t.Fatalf("OwnerRentIncomeCent = %d, want 12600 (超出预收部分必须全额划归号主)", settlement.OwnerRentIncomeCent)
+ }
+
+ // 平台服务费仍为初始纯币差额 2000 + 预收消耗品差额 400 = 2400
+ if settlement.PlatformFeeCent != 2400 {
+ t.Fatalf("PlatformFeeCent = %d, want 2400 (超额部分平台不抽成)", settlement.PlatformFeeCent)
+ }
+}
+
+func TestApplyCheckoutPriceView_RoleIsolationAndPlatformFeeHidden(t *testing.T) {
+ order := model.RentalOrder{
+ ID: 1,
+ OwnerID: 100,
+ RenterID: 200,
+ RentAmountCent: 30000,
+ OwnerRentAmountCent: 25000,
+ DepositAmountCent: 10000,
+ }
+ rentCent := int64(30000)
+ ownerRentCent := int64(25000)
+ platformCent := int64(5000)
+ renterRefundCent := int64(8000)
+ ownerIncomeCent := int64(27000)
+
+ checkout := model.OrderCheckout{
+ ID: 10,
+ OrderID: 1,
+ RentAmountCent: rentCent,
+ OwnerRentAmountCent: ownerRentCent,
+ PlatformFeeCent: platformCent,
+ RenterRefundAmountCent: renterRefundCent,
+ OwnerIncomeAmountCent: ownerIncomeCent,
+ DepositDeductAmountCent: 2000,
+ }
+
+ // 1. 号主视角
+ ownerDTO := toCheckoutAdminDTO(checkout)
+ applyCheckoutPriceView(&ownerDTO, order, 100)
+ if ownerDTO.PlatformFeeCent != nil {
+ t.Fatalf("号主视角必须屏蔽 PlatformFeeCent, 实际为: %v", *ownerDTO.PlatformFeeCent)
+ }
+ if ownerDTO.RenterRefundAmountCent != nil {
+ t.Fatalf("号主视角必须屏蔽 RenterRefundAmountCent, 实际为: %v", *ownerDTO.RenterRefundAmountCent)
+ }
+ if ownerDTO.RentAmountCent != nil {
+ t.Fatalf("号主视角必须屏蔽 RentAmountCent, 实际为: %v", *ownerDTO.RentAmountCent)
+ }
+ if ownerDTO.OwnerIncomeAmountCent == nil || *ownerDTO.OwnerIncomeAmountCent != 27000 {
+ t.Fatalf("号主视角必须展示 OwnerIncomeAmountCent, 实际为: %v", ownerDTO.OwnerIncomeAmountCent)
+ }
+
+ // 2. 租客视角
+ renterDTO := toCheckoutAdminDTO(checkout)
+ applyCheckoutPriceView(&renterDTO, order, 200)
+ if renterDTO.PlatformFeeCent != nil {
+ t.Fatalf("租客视角必须屏蔽 PlatformFeeCent, 实际为: %v", *renterDTO.PlatformFeeCent)
+ }
+ if renterDTO.OwnerRentAmountCent != nil {
+ t.Fatalf("租客视角必须屏蔽 OwnerRentAmountCent, 实际为: %v", *renterDTO.OwnerRentAmountCent)
+ }
+ if renterDTO.OwnerIncomeAmountCent != nil {
+ t.Fatalf("租客视角必须屏蔽 OwnerIncomeAmountCent, 实际为: %v", *renterDTO.OwnerIncomeAmountCent)
+ }
+ if renterDTO.RenterRefundAmountCent == nil || *renterDTO.RenterRefundAmountCent != 8000 {
+ t.Fatalf("租客视角必须展示 RenterRefundAmountCent, 实际为: %v", renterDTO.RenterRefundAmountCent)
+ }
+}
diff --git a/frontend/src/features/admin/components/AdminOrderPaymentRecords.css b/frontend/src/features/admin/components/AdminOrderPaymentRecords.css
index d1751d4..3aa947e 100644
--- a/frontend/src/features/admin/components/AdminOrderPaymentRecords.css
+++ b/frontend/src/features/admin/components/AdminOrderPaymentRecords.css
@@ -41,8 +41,8 @@
.record-meta {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: 6px 12px;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 8px 14px;
color: #52616f;
font-size: 12px;
line-height: 1.5;
diff --git a/frontend/src/features/admin/components/ProcessTimeline.vue b/frontend/src/features/admin/components/ProcessTimeline.vue
index e9826f7..f06222d 100644
--- a/frontend/src/features/admin/components/ProcessTimeline.vue
+++ b/frontend/src/features/admin/components/ProcessTimeline.vue
@@ -1,4 +1,5 @@
+
+
+