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
/>
+