diff --git a/backend/internal/modules/order/admin_actions.go b/backend/internal/modules/order/admin_actions.go index 8227b08..2af2586 100644 --- a/backend/internal/modules/order/admin_actions.go +++ b/backend/internal/modules/order/admin_actions.go @@ -254,7 +254,8 @@ func (r *Repository) AdminResetHandoff(ctx context.Context, adminID uint64, orde } func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) { - if order.Status == orderStatusPendingHandoff && order.HandoffStatus == handoffStatusOwnerTimeout { + if (order.Status == orderStatusPendingHandoff || order.Status == orderStatusAbnormal) && + (order.HandoffStatus == handoffStatusOwnerTimeout || order.HandoffStatus == handoffStatusPendingOwner) { return adminResetTarget{ OrderStatus: orderStatusPendingHandoff, HandoffStatus: handoffStatusPendingOwner, @@ -267,7 +268,8 @@ func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) { RefreshStageTime: true, }, true } - if order.Status == orderStatusAbnormal && order.HandoffStatus == handoffStatusRenterConfirmTimeout { + if (order.Status == orderStatusPendingHandoff || order.Status == orderStatusAbnormal) && + (order.HandoffStatus == handoffStatusRenterConfirmTimeout || order.HandoffStatus == handoffStatusPendingRenterConfirm) { return adminResetTarget{ OrderStatus: orderStatusPendingHandoff, HandoffStatus: handoffStatusPendingRenterConfirm, @@ -280,9 +282,10 @@ func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) { RefreshStageTime: true, }, true } - if order.Status == orderStatusAbnormal && + if (order.Status == orderStatusPendingCheckoutConfirm || order.Status == orderStatusAbnormal) && (order.HandoffStatus == handoffStatusOwnerCheckoutConfirmTimeout || - order.HandoffStatus == handoffStatusOwnerReturnConfirmTimeout) { + order.HandoffStatus == handoffStatusOwnerReturnConfirmTimeout || + order.HandoffStatus == handoffStatusPendingOwnerCheckout) { return adminResetTarget{ OrderStatus: orderStatusPendingCheckoutConfirm, HandoffStatus: handoffStatusPendingOwnerCheckout, @@ -295,7 +298,8 @@ func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) { RefreshStageTime: true, }, true } - if order.Status == orderStatusOverdue && order.HandoffStatus == handoffStatusReturnOverdue { + if (order.Status == orderStatusOverdue || order.Status == orderStatusRenting || order.Status == orderStatusAbnormal) && + order.HandoffStatus == handoffStatusReturnOverdue { return adminResetTarget{ OrderStatus: orderStatusRenting, HandoffStatus: handoffStatusReceived, diff --git a/backend/internal/modules/order/handler_error.go b/backend/internal/modules/order/handler_error.go index 24d5229..a39cb48 100644 --- a/backend/internal/modules/order/handler_error.go +++ b/backend/internal/modules/order/handler_error.go @@ -35,7 +35,7 @@ func writeOrderError(c *gin.Context, err error) { case errors.Is(err, ErrOrderCannotForceHandoff): response.Error(c, http.StatusConflict, "order_cannot_force_handoff", "当前订单不能客服一键交接") case errors.Is(err, ErrOrderCannotResetHandoff): - response.Error(c, http.StatusConflict, "order_cannot_reset_handoff", "仅超时卡住的订单可重置到对应待办阶段") + response.Error(c, http.StatusConflict, "order_cannot_reset_handoff", "当前订单状态不可重置待办") case errors.Is(err, ErrOrderCannotReceive): response.Error(c, http.StatusConflict, "order_cannot_receive", "当前订单不能确认收号") case errors.Is(err, ErrOrderCannotReturn): diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go index c3b2990..2b62a42 100644 --- a/backend/internal/modules/order/repository_integration_test.go +++ b/backend/internal/modules/order/repository_integration_test.go @@ -1026,6 +1026,101 @@ func TestCancelDoesNotReleaseAssetsWhenSiblingOrderActive(t *testing.T) { } } +func TestAdminResetOwnerHandoff(t *testing.T) { + db := setupOrderTestDB(t) + repo := NewRepository(db) + + owner := model.User{Phone: "13800003009"} + renter := model.User{Phone: "13900003009"} + if err := db.Create(&owner).Error; err != nil { + t.Fatalf("create owner failed: %v", err) + } + if err := db.Create(&renter).Error; err != nil { + t.Fatalf("create renter failed: %v", err) + } + + // 1. 测试在待号主交接(pending_owner)状态下重置交接 + oldTime := time.Now().Add(-1 * time.Hour) + order := model.RentalOrder{ + OrderNo: "ORD-RESET-OWNER-001", + ListingID: 1, + AccountID: 1, + OwnerID: owner.ID, + RenterID: renter.ID, + Status: orderStatusPendingHandoff, + HandoffStatus: handoffStatusPendingOwner, + HandoffStartedAt: &oldTime, + SettlementStatus: settlementStatusPending, + } + if err := db.Create(&order).Error; err != nil { + t.Fatalf("create order failed: %v", err) + } + + actions := adminActionsForOrder(order) + if actions == nil || actions.ResetHandoff == nil || !actions.ResetHandoff.Enabled { + t.Fatalf("expected ResetHandoff action to be enabled for pending_owner, got %+v", actions) + } + + err := repo.AdminResetHandoff(t.Context(), 1, order.ID, AdminActionRequest{Reason: "客服协调重置交接时限"}, AuditMeta{}) + if err != nil { + t.Fatalf("AdminResetHandoff() error = %v", err) + } + + var saved model.RentalOrder + if err := db.First(&saved, order.ID).Error; err != nil { + t.Fatalf("load order failed: %v", err) + } + if saved.Status != orderStatusPendingHandoff { + t.Fatalf("status = %q, want %q", saved.Status, orderStatusPendingHandoff) + } + if saved.HandoffStatus != handoffStatusPendingOwner { + t.Fatalf("handoff status = %q, want %q", saved.HandoffStatus, handoffStatusPendingOwner) + } + if saved.HandoffStartedAt == nil || !saved.HandoffStartedAt.After(oldTime) { + t.Fatal("handoff started at should be refreshed to current time") + } + + // 2. 测试在号主交接超时(owner_timeout)状态下重置交接 + orderTimeout := model.RentalOrder{ + OrderNo: "ORD-RESET-OWNER-002", + ListingID: 1, + AccountID: 1, + OwnerID: owner.ID, + RenterID: renter.ID, + Status: orderStatusPendingHandoff, + HandoffStatus: handoffStatusOwnerTimeout, + HandoffStartedAt: &oldTime, + SettlementStatus: settlementStatusPending, + } + if err := db.Create(&orderTimeout).Error; err != nil { + t.Fatalf("create order failed: %v", err) + } + + actionsTimeout := adminActionsForOrder(orderTimeout) + if actionsTimeout == nil || actionsTimeout.ResetHandoff == nil || !actionsTimeout.ResetHandoff.Enabled { + t.Fatalf("expected ResetHandoff action to be enabled for owner_timeout, got %+v", actionsTimeout) + } + + err = repo.AdminResetHandoff(t.Context(), 1, orderTimeout.ID, AdminActionRequest{Reason: "超时后人工重置交接"}, AuditMeta{}) + if err != nil { + t.Fatalf("AdminResetHandoff() error = %v", err) + } + + var savedTimeout model.RentalOrder + if err := db.First(&savedTimeout, orderTimeout.ID).Error; err != nil { + t.Fatalf("load order failed: %v", err) + } + if savedTimeout.Status != orderStatusPendingHandoff { + t.Fatalf("status = %q, want %q", savedTimeout.Status, orderStatusPendingHandoff) + } + if savedTimeout.HandoffStatus != handoffStatusPendingOwner { + t.Fatalf("handoff status = %q, want %q", savedTimeout.HandoffStatus, handoffStatusPendingOwner) + } + if savedTimeout.HandoffStartedAt == nil || !savedTimeout.HandoffStartedAt.After(oldTime) { + t.Fatal("handoff started at should be refreshed to current time") + } +} + func TestAdminResetCheckoutConfirmTimeout(t *testing.T) { db := setupOrderTestDB(t) repo := NewRepository(db) diff --git a/frontend/src/features/admin/views/AdminOrderDetailView.vue b/frontend/src/features/admin/views/AdminOrderDetailView.vue index 7fcef82..ea36008 100644 --- a/frontend/src/features/admin/views/AdminOrderDetailView.vue +++ b/frontend/src/features/admin/views/AdminOrderDetailView.vue @@ -220,6 +220,7 @@ const actionTitle = computed(() => { return '标记订单异常' }) const actionConfirmButtonType = computed(() => { + if (actionType.value === 'reset') return 'success' if (['deposit_release', 'platform_checkout_confirm'].includes(actionType.value)) return 'primary' if (actionType.value === 'platform_checkout_dispute') return 'warning' return 'danger' @@ -1488,6 +1489,14 @@ function formatRatioNumber(value: number) { :closable="false" show-icon /> +