From 71ea31ddc53375178f14bfb0e17e5e8fd51290d6 Mon Sep 17 00:00:00 2001 From: dujiao-next Date: Sun, 11 Oct 2026 12:10:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A6=81=E6=AD=A2=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E5=B7=B2=E6=94=AF=E4=BB=98=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E9=80=80=E5=9B=9E=E9=92=B1=E5=8C=85?= =?UTF-8?q?=E4=B8=8E=E9=87=8A=E6=94=BE=E5=B7=B2=E6=B6=88=E8=80=97=E5=BA=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin/src/views/admin/Orders.vue | 2 +- .../order_service_test_structure_test.go | 1 + .../order/application/order_service_child.go | 6 ++- .../application/order_service_status_test.go | 51 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/frontend/admin/src/views/admin/Orders.vue b/frontend/admin/src/views/admin/Orders.vue index a2ccb550..c9b64abb 100644 --- a/frontend/admin/src/views/admin/Orders.vue +++ b/frontend/admin/src/views/admin/Orders.vue @@ -434,7 +434,7 @@ watch( {{ t('order.status.partially_refunded') }} {{ t('order.status.delivered') }} {{ t('order.status.completed') }} - {{ t('order.status.canceled') }} + {{ t('order.status.canceled') }} {{ t('order.status.refunded') }} diff --git a/internal/architecture/order_service_test_structure_test.go b/internal/architecture/order_service_test_structure_test.go index cf8c4386..a1a08df2 100644 --- a/internal/architecture/order_service_test_structure_test.go +++ b/internal/architecture/order_service_test_structure_test.go @@ -39,6 +39,7 @@ func TestOrderServiceTestsAreSplitByResponsibility(t *testing.T) { "TestCalcParentStatus", "TestCalcParentStatusAllRefunded", "TestCalcParentStatusPartiallyRefunded", "TestExpectedRefundStatus", "TestResolvedParentStatusPrefersOwnRefund", "TestIsTransitionAllowedRefunded", "TestUpdateOrderStatusParentToPartiallyRefundedSyncsChildren", + "TestUpdateOrderStatusRejectsCancelingPaidParent", "TestUpdateOrderStatusRejectsManualPaidTransition", "TestCanCompleteParentOrder", "TestCanCompleteParentOrderRejectInvalidStatus", "TestCanCompleteParentOrderRejectInvalidChild", diff --git a/internal/modules/order/application/order_service_child.go b/internal/modules/order/application/order_service_child.go index feb97656..afd38310 100644 --- a/internal/modules/order/application/order_service_child.go +++ b/internal/modules/order/application/order_service_child.go @@ -30,7 +30,8 @@ func (s *OrderService) cancelOrderWithChildren(order *orderdomain.Order, rollbac if locked == nil { return ErrOrderNotFound } - if locked.Status != order.Status { + // 只有未支付订单可以取消:已支付订单取消会退回钱包、释放已消耗库存,必须走退款流程 + if locked.Status != order.Status || locked.Status != constants.OrderStatusPendingPayment { return ErrOrderCancelNotAllowed } updates := map[string]interface{}{ @@ -181,6 +182,9 @@ func (s *OrderService) UpdateOrderStatus(orderID uint, targetStatus string) (*or if isParent { switch target { case constants.OrderStatusCanceled: + if order.Status != constants.OrderStatusPendingPayment { + return nil, ErrOrderStatusInvalid + } if err := s.cancelOrderWithChildren(order, true); err != nil { return nil, ErrOrderUpdateFailed } diff --git a/internal/modules/order/application/order_service_status_test.go b/internal/modules/order/application/order_service_status_test.go index 40336389..d1b97a66 100644 --- a/internal/modules/order/application/order_service_status_test.go +++ b/internal/modules/order/application/order_service_status_test.go @@ -319,3 +319,54 @@ func TestCanCompleteParentOrderRejectInvalidChild(t *testing.T) { t.Fatalf("expected parent order with paid child to be rejected") } } + +func TestUpdateOrderStatusRejectsCancelingPaidParent(t *testing.T) { + dsn := fmt.Sprintf("file:order_service_reject_cancel_paid_parent_%d?mode=memory&cache=shared", time.Now().UnixNano()) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite failed: %v", err) + } + if err := db.AutoMigrate(&orderdomain.Order{}, &orderdomain.OrderItem{}, &fulfillmentdomain.Fulfillment{}); err != nil { + t.Fatalf("auto migrate failed: %v", err) + } + now := time.Now() + parent := &orderdomain.Order{ + OrderNo: "PAID-PARENT-NO-CANCEL", + Status: constants.OrderStatusDelivered, + Currency: "CNY", + TotalAmount: money.FromDecimal(decimal.NewFromInt(100)), + OriginalAmount: money.FromDecimal(decimal.NewFromInt(100)), + WalletPaidAmount: money.FromDecimal(decimal.NewFromInt(100)), + PaidAt: &now, + CreatedAt: now, + UpdatedAt: now, + } + if err := db.Create(parent).Error; err != nil { + t.Fatalf("create parent failed: %v", err) + } + child := &orderdomain.Order{ + OrderNo: "PAID-PARENT-NO-CANCEL-A", + ParentID: &parent.ID, + Status: constants.OrderStatusDelivered, + Currency: "CNY", + TotalAmount: money.FromDecimal(decimal.NewFromInt(100)), + PaidAt: &now, + CreatedAt: now, + UpdatedAt: now, + } + if err := db.Create(child).Error; err != nil { + t.Fatalf("create child failed: %v", err) + } + svc := NewOrderService(OrderServiceOptions{OrderStore: ordergormstore.New(db, "test-guest-credential-secret-with-32-bytes")}) + + if _, err := svc.UpdateOrderStatus(parent.ID, constants.OrderStatusCanceled); err != ErrOrderStatusInvalid { + t.Fatalf("cancel paid parent error = %v, want %v", err, ErrOrderStatusInvalid) + } + var stored orderdomain.Order + if err := db.First(&stored, parent.ID).Error; err != nil { + t.Fatalf("reload parent failed: %v", err) + } + if stored.Status != constants.OrderStatusDelivered { + t.Fatalf("paid parent status mutated to %s", stored.Status) + } +}