diff --git a/backend/internal/modules/dispute/arbitration.go b/backend/internal/modules/dispute/arbitration.go index 5c6234a..158171e 100644 --- a/backend/internal/modules/dispute/arbitration.go +++ b/backend/internal/modules/dispute/arbitration.go @@ -224,7 +224,7 @@ func buildArbitrationDecision(tx *gorm.DB, row model.Dispute, order model.Rental func arbitrationUsesActualCheckout(result string) bool { switch result { - case "release_deposit", "deduct_deposit", "compensate_owner": + case "actual_settlement", "release_deposit", "deduct_deposit", "compensate_owner": return true default: return false @@ -301,6 +301,14 @@ func arbitrationConsumableAmountCent(order model.RentalOrder, checkout *model.Or func arbitrationDepositDeductAmountCent(order model.RentalOrder, req ArbitrateRequest) (int64, error) { switch req.Result { + case "actual_settlement": + if req.AmountCent <= 0 { + return 0, nil + } + if req.AmountCent > order.DepositAmountCent { + return 0, ErrInvalidDispute + } + return req.AmountCent, nil case "release_deposit": return 0, nil case "deduct_deposit", "compensate_owner": @@ -628,6 +636,8 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration func arbitrationResultLabel(result string) string { switch result { + case "actual_settlement": + return "按实际消耗结算" case "full_refund": return "全额退款" case "partial_refund": diff --git a/backend/internal/modules/dispute/repository_test.go b/backend/internal/modules/dispute/repository_test.go index 575e1b7..68fad02 100644 --- a/backend/internal/modules/dispute/repository_test.go +++ b/backend/internal/modules/dispute/repository_test.go @@ -337,7 +337,7 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) { } _, err = repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{ - Result: "release_deposit", + Result: "actual_settlement", Remark: "确认完成订单", }, AuditMeta{}) if !errors.Is(err, ErrInvalidDispute) { @@ -347,7 +347,7 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) { actualCoinConsumedM := 50.0 actualConsumableAmountCent := int64(12000) _, err = repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{ - Result: "release_deposit", + Result: "actual_settlement", Remark: "确认实际消耗 50M", ActualCoinConsumedM: &actualCoinConsumedM, }, AuditMeta{}) @@ -356,7 +356,7 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) { } if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{ - Result: "release_deposit", + Result: "actual_settlement", Remark: "确认实际消耗 50M 和额外物品", ActualCoinConsumedM: &actualCoinConsumedM, ActualConsumableAmountCent: &actualConsumableAmountCent, @@ -372,6 +372,40 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) { } } +func TestActualSettlementArbitrationUsesOwnerCompensationAmount(t *testing.T) { + order := disputePureCoinOrder() + actualCoinConsumedM := 50.0 + actualConsumableAmountCent := int64(12000) + + settlement, actualSettlement, _, depositDeductAmountCent, err := buildActualCheckoutArbitrationSettlement(nil, model.Dispute{}, order, ArbitrateRequest{ + Result: "actual_settlement", + Remark: "按实际消耗结算", + ActualCoinConsumedM: &actualCoinConsumedM, + ActualConsumableAmountCent: &actualConsumableAmountCent, + }, 0) + if err != nil { + t.Fatalf("不填押金赔付的实际结算失败: %v", err) + } + if depositDeductAmountCent != 0 || actualSettlement.DepositCompensationCent != 0 || settlement.DepositDeductAmountCent != 0 { + t.Fatalf("不填押金赔付不应默认扣全押金: input=%d actual=%d settlement=%d", depositDeductAmountCent, actualSettlement.DepositCompensationCent, settlement.DepositDeductAmountCent) + } + + compensationCent := int64(3000) + settlement, actualSettlement, _, depositDeductAmountCent, err = buildActualCheckoutArbitrationSettlement(nil, model.Dispute{}, order, ArbitrateRequest{ + Result: "actual_settlement", + Remark: "赔付号主", + AmountCent: compensationCent, + ActualCoinConsumedM: &actualCoinConsumedM, + ActualConsumableAmountCent: &actualConsumableAmountCent, + }, 0) + if err != nil { + t.Fatalf("填写押金赔付的实际结算失败: %v", err) + } + if depositDeductAmountCent != compensationCent || actualSettlement.DepositCompensationCent != compensationCent || settlement.DepositDeductAmountCent != compensationCent { + t.Fatalf("押金赔付 = %d/%d/%d, want %d", depositDeductAmountCent, actualSettlement.DepositCompensationCent, settlement.DepositDeductAmountCent, compensationCent) + } +} + func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) { db := setupDisputeTestDB(t) repo := NewRepository(db, Dependencies{RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) { diff --git a/frontend/src/features/admin/views/AdminDisputesView.vue b/frontend/src/features/admin/views/AdminDisputesView.vue index 9c8be2e..e1375e1 100644 --- a/frontend/src/features/admin/views/AdminDisputesView.vue +++ b/frontend/src/features/admin/views/AdminDisputesView.vue @@ -23,7 +23,8 @@ const submitting = ref(false) const disputes = ref([]) const activeDispute = ref(null) const evidenceDispute = ref(null) -const result = ref('release_deposit') +const actualSettlementResult = 'actual_settlement' +const result = ref(actualSettlementResult) const remark = ref('') const amount = ref() const actualCoinConsumedM = ref() @@ -34,9 +35,15 @@ const total = ref(0) const keywordFilter = ref('') const statusFilter = ref('') const typeFilter = ref('') -const isPartialRefund = computed(() => result.value === 'partial_refund') const isCompletedResult = computed(() => - ['deduct_deposit', 'release_deposit', 'compensate_owner'].includes(result.value) + [actualSettlementResult, 'deduct_deposit', 'release_deposit', 'compensate_owner'].includes( + result.value + ) +) +const hasValidOwnerCompensation = computed( + () => + amount.value === undefined || + (typeof amount.value === 'number' && Number.isFinite(amount.value) && amount.value >= 0) ) const hasValidActualCoinConsumedM = computed( () => @@ -62,7 +69,7 @@ const cancelledCount = computed( const canSubmitArbitration = computed(() => { if (submitting.value || !activeDispute.value) return false if (!result.value || !remark.value.trim()) return false - if (isPartialRefund.value && (!amount.value || amount.value <= 0)) return false + if (isCompletedResult.value && !hasValidOwnerCompensation.value) return false if ( isCompletedResult.value && !activeDispute.value.checkout_id && @@ -93,6 +100,7 @@ function disputeTypeLabel(type: string) { function arbitrationResultLabel(value: string) { const map: Record = { + actual_settlement: '按实际消耗结算', full_refund: '全额退款', partial_refund: '部分退款', deduct_deposit: '扣押金', @@ -104,6 +112,13 @@ function arbitrationResultLabel(value: string) { return map[value] || value || '-' } +function editableArbitrationResult(value: string) { + if (['partial_refund', 'deduct_deposit', 'release_deposit', 'compensate_owner'].includes(value)) { + return actualSettlementResult + } + return value || actualSettlementResult +} + function disputeStatusTone(status: string) { const map: Record = { open: 'warning', @@ -167,7 +182,7 @@ async function queryDisputes() { function openArbitration(row: Dispute) { if (!canArbitrate(row)) return activeDispute.value = row - result.value = row.arbitration_result || 'release_deposit' + result.value = editableArbitrationResult(row.arbitration_result) remark.value = row.arbitration_remark || '' amount.value = undefined actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined @@ -300,8 +315,8 @@ async function handleArbitrate() { ElMessage.warning('请填写客服裁决说明') return } - if (isPartialRefund.value && (!amount.value || amount.value <= 0)) { - ElMessage.warning('请填写部分退款金额') + if (isCompletedResult.value && !hasValidOwnerCompensation.value) { + ElMessage.warning('请填写有效的押金赔付金额') return } if ( @@ -321,7 +336,8 @@ async function handleArbitrate() { await arbitrateDispute(activeDispute.value.id, { result: result.value, remark: remark.value.trim(), - amount_cent: amount.value ? yuanToCent(amount.value) : undefined, + amount_cent: + isCompletedResult.value && amount.value !== undefined ? yuanToCent(amount.value) : undefined, actual_coin_consumed_m: isCompletedResult.value ? actualCoinConsumedM.value : undefined, actual_consumable_amount_cent: isCompletedResult.value ? yuanToCent(actualConsumableAmountYuan.value) @@ -512,27 +528,23 @@ async function handleArbitrate() {

{{ activeDispute.description }}

- - - - + - -

- 部分退款金额表示退给租客的金额,剩余冻结金额结算给号主。 -

-

- 金额表示从押金中赔付给号主的部分;不填则默认处理全额押金。 +

+ 押金赔付给号主(元) + +
+

+ 金额表示从押金中赔付给号主的部分;不填或填 0 表示不扣押金赔付。

最终实际哈夫币消耗(M)