diff --git a/backend/internal/modules/dispute/arbitration.go b/backend/internal/modules/dispute/arbitration.go index b844a20..fdb9f8e 100644 --- a/backend/internal/modules/dispute/arbitration.go +++ b/backend/internal/modules/dispute/arbitration.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "math" "time" "hfb_sys/backend/internal/listingstatus" @@ -246,7 +247,11 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r addRenterRefund(req.AmountCent, "仲裁部分退款") // 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。 settlement.RenterDepositRefundCent = money.MinCent(req.AmountCent, depositAmountCent) - addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主") + // 号主仅拿「未退租金中的号主份额 + 未退押金」,平台加价按未退租金比例预留,避免整笔剩余进号主。 + addOwnerIncome( + partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, req.AmountCent), + "仲裁剩余金额结算给号主", + ) case "release_deposit": addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主") addRenterRefund(depositAmountCent, "仲裁释放押金给租客") @@ -273,6 +278,66 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r return settlement, nil } +// partialRefundOwnerIncomeCent 计算部分退款后号主应得。 +// 退款优先冲押金,其余冲租金;未退租金按预收结构拆分: +// 平台费 = 租客租金 − 号主租金,并按 未退租金/预收租金 比例预留,剩余才归号主。 +// 这样可避免「剩余金额整笔给号主」把平台加价分给用户。 +func partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, renterRefundCent int64) int64 { + if renterRefundCent < 0 { + renterRefundCent = 0 + } + totalCent := rentAmountCent + depositAmountCent + if renterRefundCent > totalCent { + renterRefundCent = totalCent + } + if ownerRentAmountCent < 0 { + ownerRentAmountCent = 0 + } + if ownerRentAmountCent > rentAmountCent { + ownerRentAmountCent = rentAmountCent + } + + // 退款优先视为押金退还,其余视为租金退还(与 RenterDepositRefundCent 归类一致) + depositRefunded := money.MinCent(renterRefundCent, depositAmountCent) + rentRefunded := renterRefundCent - depositRefunded + rentLeft := rentAmountCent - rentRefunded + if rentLeft < 0 { + rentLeft = 0 + } + depositLeft := depositAmountCent - depositRefunded + + platformFeeCent := rentAmountCent - ownerRentAmountCent + if platformFeeCent < 0 { + platformFeeCent = 0 + } + + // 按未退租金占比预留平台费 + platformKeepCent := int64(0) + if rentAmountCent > 0 && rentLeft > 0 && platformFeeCent > 0 { + platformKeepCent = int64(math.Round(float64(platformFeeCent) * float64(rentLeft) / float64(rentAmountCent))) + platformKeepCent = money.MinCent(platformKeepCent, rentLeft) + platformKeepCent = money.MinCent(platformKeepCent, platformFeeCent) + } + + ownerFromRent := rentLeft - platformKeepCent + if ownerFromRent > ownerRentAmountCent { + ownerFromRent = ownerRentAmountCent + } + if ownerFromRent < 0 { + ownerFromRent = 0 + } + + ownerIncome := ownerFromRent + depositLeft + remaining := totalCent - renterRefundCent + if ownerIncome > remaining { + ownerIncome = remaining + } + if ownerIncome < 0 { + return 0 + } + return ownerIncome +} + func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) { if amountCent <= 0 { return nil, nil diff --git a/backend/internal/modules/dispute/repository_test.go b/backend/internal/modules/dispute/repository_test.go index cd38ad7..174c44f 100644 --- a/backend/internal/modules/dispute/repository_test.go +++ b/backend/internal/modules/dispute/repository_test.go @@ -261,3 +261,78 @@ func TestBuildArbitrationHandoffContent(t *testing.T) { } } } + +// 复现线上单:预收 94.7(号主 66.7 + 平台 28),部分退 50 时,号主不应吃掉平台加价。 +func TestPartialRefundReservesPlatformFee(t *testing.T) { + order := model.RentalOrder{ + ID: 30, + OrderNo: "RO-ARBIT-PARTIAL-001", + RenterID: 9233, + OwnerID: 7463, + RentAmountCent: 9470, + OwnerRentAmountCent: 6670, + DepositAmountCent: 0, + } + + settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ + Result: "partial_refund", + AmountCent: 5000, + Remark: "没打完 号主登录不上", + }, 0) + if err != nil { + t.Fatalf("buildArbitrationSettlement() error = %v", err) + } + if settlement.RenterRefundAmountCent != 5000 { + t.Fatalf("RenterRefundAmountCent = %d, want 5000", settlement.RenterRefundAmountCent) + } + + // 未退租金 44.7:平台按比例预留 round(28 * 44.7/94.7)=13.22 → 号主 31.48 + wantOwner := partialRefundOwnerIncomeCent(9470, 6670, 0, 5000) + if settlement.OwnerIncomeAmountCent != wantOwner { + t.Fatalf("OwnerIncomeAmountCent = %d, want %d", settlement.OwnerIncomeAmountCent, wantOwner) + } + if settlement.OwnerIncomeAmountCent >= 4470 { + t.Fatalf("号主收入 %d 不应吞掉整笔剩余 4470(含平台费)", settlement.OwnerIncomeAmountCent) + } + platformKeep := 9470 - settlement.RenterRefundAmountCent - settlement.OwnerIncomeAmountCent + if platformKeep <= 0 { + t.Fatalf("平台留存 = %d, 部分退款后应保留平台加价份额", platformKeep) + } +} + +func TestPartialRefundFullRentRetainedGivesOwnerFullOwnerRent(t *testing.T) { + // 只退押金:号主拿满预收号主租金,平台留满预收平台费 + order := model.RentalOrder{ + RentAmountCent: 9470, + OwnerRentAmountCent: 6670, + DepositAmountCent: 20000, + } + settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ + Result: "partial_refund", + AmountCent: 20000, + }, 0) + if err != nil { + t.Fatalf("buildArbitrationSettlement() error = %v", err) + } + if settlement.OwnerIncomeAmountCent != 6670 { + t.Fatalf("OwnerIncomeAmountCent = %d, want 6670", settlement.OwnerIncomeAmountCent) + } + if settlement.RenterRefundAmountCent != 20000 { + t.Fatalf("RenterRefundAmountCent = %d, want 20000", settlement.RenterRefundAmountCent) + } + platformKeep := 9470 + 20000 - settlement.RenterRefundAmountCent - settlement.OwnerIncomeAmountCent + if platformKeep != 2800 { + t.Fatalf("platformKeep = %d, want 2800", platformKeep) + } +} + +func TestPartialRefundOwnerIncomeCentUnit(t *testing.T) { + // 无平台费时,未退租金全部给号主 + if got := partialRefundOwnerIncomeCent(10000, 10000, 0, 3000); got != 7000 { + t.Fatalf("no platform fee: got %d, want 7000", got) + } + // 全额退:号主 0 + if got := partialRefundOwnerIncomeCent(9470, 6670, 0, 9470); got != 0 { + t.Fatalf("full refund: got %d, want 0", got) + } +}