From 2487a2d83fbe67aed9a881ef57f21e54293bcc0f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 29 Aug 2026 01:31:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9B=B6=E5=8F=B7=E4=B8=BB?= =?UTF-8?q?=E6=94=B6=E5=85=A5=E7=BB=93=E7=AE=97=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/order/dto.go | 3 +- .../internal/modules/order/handler_error.go | 2 + .../modules/order/platform_managed.go | 4 ++ .../order/repository_integration_test.go | 46 +++++++++++++++++++ backend/internal/modules/order/service.go | 1 + .../admin/views/AdminOrderDetailView.vue | 42 ++++++++++++++++- frontend/src/features/orders/api/orders.ts | 8 +++- 7 files changed, 101 insertions(+), 5 deletions(-) diff --git a/backend/internal/modules/order/dto.go b/backend/internal/modules/order/dto.go index 7848044..ef3ca76 100644 --- a/backend/internal/modules/order/dto.go +++ b/backend/internal/modules/order/dto.go @@ -133,7 +133,8 @@ type CounterCheckoutRequest struct { } type AdminActionRequest struct { - Reason string `json:"reason" binding:"required"` + Reason string `json:"reason" binding:"required"` + ConfirmZeroOwnerIncome bool `json:"confirm_zero_owner_income"` } type PlatformHandoffRequest struct { diff --git a/backend/internal/modules/order/handler_error.go b/backend/internal/modules/order/handler_error.go index f8a8c02..acbb653 100644 --- a/backend/internal/modules/order/handler_error.go +++ b/backend/internal/modules/order/handler_error.go @@ -44,6 +44,8 @@ func writeOrderError(c *gin.Context, err error) { response.Error(c, http.StatusConflict, "checkout_cannot_submit", "当前订单不能发起结账") case errors.Is(err, ErrCheckoutCannotConfirm): response.Error(c, http.StatusConflict, "checkout_cannot_confirm", "当前结账不能确认") + case errors.Is(err, ErrZeroOwnerIncomeConfirmation): + response.Error(c, http.StatusConflict, "zero_owner_income_confirmation_required", "号主收入为 0,请确认后再结算") case errors.Is(err, ErrCheckoutCannotCounter): response.Error(c, http.StatusConflict, "checkout_cannot_counter", "当前结账不能修改") case errors.Is(err, ErrCheckoutMaxRounds): diff --git a/backend/internal/modules/order/platform_managed.go b/backend/internal/modules/order/platform_managed.go index 5d478de..963876e 100644 --- a/backend/internal/modules/order/platform_managed.go +++ b/backend/internal/modules/order/platform_managed.go @@ -159,6 +159,9 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u if normalizeCheckoutTurn(checkout) != checkoutTurnOwner { return ErrCheckoutCannotConfirm } + if checkout.OwnerIncomeAmountCent == 0 && !req.ConfirmZeroOwnerIncome { + return ErrZeroOwnerIncomeConfirmation + } if err := ensureCheckoutCompletable(order, checkout); err != nil { return err } @@ -196,6 +199,7 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u "after_settlement_status": order.SettlementStatus, "offline_settlement_status": order.OfflineSettlementStatus, "offline_settlement_amount_cent": order.OfflineSettlementAmountCent, + "confirm_zero_owner_income": req.ConfirmZeroOwnerIncome, }) }) if err != nil { diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go index c939399..da8d2e2 100644 --- a/backend/internal/modules/order/repository_integration_test.go +++ b/backend/internal/modules/order/repository_integration_test.go @@ -2,6 +2,7 @@ package order import ( "context" + "errors" "testing" "time" @@ -1794,3 +1795,48 @@ func createOpenPlatformCheckout(t *testing.T, db *gorm.DB, order model.RentalOrd } return checkout } + +func TestAdminPlatformCheckoutConfirmRequiresExplicitConfirmationForZeroOwnerIncome(t *testing.T) { + db := setupOrderTestDB(t) + repo := NewRepository(db, Dependencies{ + RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) { + return "pending", nil + }), + }) + const adminID uint64 = 7001 + _, renter, order := createPlatformManagedCheckoutOrder(t, db, adminID) + checkout := createOpenPlatformCheckout(t, db, order, renter.ID) + checkout.OwnerIncomeAmountCent = 0 + if err := db.Save(&checkout).Error; err != nil { + t.Fatalf("将号主收入调整为零失败: %v", err) + } + + err := repo.AdminPlatformCheckoutConfirm(t.Context(), *order.ManagedAdminID, order.ID, AdminActionRequest{ + Reason: "客服核对结算", + }, AuditMeta{}) + if !errors.Is(err, ErrZeroOwnerIncomeConfirmation) { + t.Fatalf("未确认零收入时 error = %v, want %v", err, ErrZeroOwnerIncomeConfirmation) + } + + var pending model.RentalOrder + if err := db.First(&pending, order.ID).Error; err != nil { + t.Fatalf("读取订单失败: %v", err) + } + if pending.Status != orderStatusPendingCheckoutConfirm { + t.Fatalf("未确认零收入后订单状态 = %s, want %s", pending.Status, orderStatusPendingCheckoutConfirm) + } + + if err := repo.AdminPlatformCheckoutConfirm(t.Context(), *order.ManagedAdminID, order.ID, AdminActionRequest{ + Reason: "已确认号主收入为零", + ConfirmZeroOwnerIncome: true, + }, AuditMeta{}); err != nil { + t.Fatalf("确认零收入后结算失败: %v", err) + } + var completed model.RentalOrder + if err := db.First(&completed, order.ID).Error; err != nil { + t.Fatalf("读取完成订单失败: %v", err) + } + if completed.Status != orderStatusCompleted { + t.Fatalf("确认零收入后订单状态 = %s, want %s", completed.Status, orderStatusCompleted) + } +} diff --git a/backend/internal/modules/order/service.go b/backend/internal/modules/order/service.go index c08605c..74b7269 100644 --- a/backend/internal/modules/order/service.go +++ b/backend/internal/modules/order/service.go @@ -22,6 +22,7 @@ var ( ErrOrderCannotComplete = errors.New("order cannot complete") ErrCheckoutCannotSubmit = errors.New("checkout cannot submit") ErrCheckoutCannotConfirm = errors.New("checkout cannot confirm") + ErrZeroOwnerIncomeConfirmation = errors.New("zero owner income confirmation required") ErrCheckoutCannotCounter = errors.New("checkout cannot counter") ErrCheckoutMaxRounds = errors.New("checkout max rounds reached") ErrCheckoutDepositShortfall = errors.New("checkout deposit shortfall") diff --git a/frontend/src/features/admin/views/AdminOrderDetailView.vue b/frontend/src/features/admin/views/AdminOrderDetailView.vue index fcb18d1..01d4aec 100644 --- a/frontend/src/features/admin/views/AdminOrderDetailView.vue +++ b/frontend/src/features/admin/views/AdminOrderDetailView.vue @@ -68,6 +68,7 @@ type OrderActionType = | '' const actionType = ref('') const reason = ref('') +const zeroOwnerIncomeConfirmed = ref(false) const refundStatus = ref(null) const platformHandoffVisible = ref(false) const platformHandoffContent = ref('') @@ -476,7 +477,11 @@ async function submitAction() { await adminResetHandoff(order.value.id, reason.value) ElMessage.success(`${resetActionLabel.value}成功`) } else if (actionType.value === 'platform_checkout_confirm') { - await adminPlatformCheckoutConfirm(order.value.id, reason.value) + await adminPlatformCheckoutConfirm( + order.value.id, + reason.value, + zeroOwnerIncomeConfirmed.value + ) ElMessage.success('结账已确认') } else if (actionType.value === 'platform_checkout_dispute') { await adminPlatformCheckoutDispute(order.value.id, reason.value) @@ -500,6 +505,28 @@ async function submitAction() { } } +async function confirmPlatformCheckout() { + if (!order.value) return + zeroOwnerIncomeConfirmed.value = false + if (Number(order.value.checkout?.owner_income_amount_cent ?? 0) === 0) { + try { + await ElMessageBox.confirm( + '本次结算的号主收入为 ¥0.0。确认后将完成结算,请再次确认是否继续。', + '号主收入为零', + { + confirmButtonText: '仍确认结算', + cancelButtonText: '取消', + type: 'warning', + } + ) + } catch { + return + } + zeroOwnerIncomeConfirmed.value = true + } + openAction('platform_checkout_confirm') +} + function openPlatformHandoff() { platformHandoffContent.value = '' platformHandoffReason.value = '' @@ -952,7 +979,7 @@ function paymentPaidAt(record: AdminPayment) { {{ platformCheckoutConfirmAction?.label || '客服确认结账' }} @@ -1500,6 +1527,17 @@ function paymentPaidAt(record: AdminPayment) {

{{ order.order_no }} · 商品编号 {{ listingCode }} · {{ order.title }}

+ >( `/admin/orders/${id}/platform-checkout/confirm`, - { reason } + { reason, confirm_zero_owner_income: confirmZeroOwnerIncome } ) return data.data }