fix: 禁止后台取消已支付订单,避免退回钱包与释放已消耗库存

This commit is contained in:
dujiao-next committed 2026-10-11 12:11:18 +08:00
1 parent f627efce94
commit 71ea31ddc5
4 files changed
+58 -2

No files matched your search

+1 -1
View File
@@ -434,7 +434,7 @@ watch(
<SelectItem value="partially_refunded">{{ t('order.status.partially_refunded') }}</SelectItem>
<SelectItem value="delivered">{{ t('order.status.delivered') }}</SelectItem>
<SelectItem value="completed">{{ t('order.status.completed') }}</SelectItem>
<SelectItem value="canceled">{{ t('order.status.canceled') }}</SelectItem>
<SelectItem v-if="order.status === 'pending_payment'" value="canceled">{{ t('order.status.canceled') }}</SelectItem>
<SelectItem value="refunded">{{ t('order.status.refunded') }}</SelectItem>
</SelectContent>
</Select>
@@ -39,6 +39,7 @@ func TestOrderServiceTestsAreSplitByResponsibility(t *testing.T) {
"TestCalcParentStatus", "TestCalcParentStatusAllRefunded", "TestCalcParentStatusPartiallyRefunded",
"TestExpectedRefundStatus", "TestResolvedParentStatusPrefersOwnRefund",
"TestIsTransitionAllowedRefunded", "TestUpdateOrderStatusParentToPartiallyRefundedSyncsChildren",
"TestUpdateOrderStatusRejectsCancelingPaidParent",
"TestUpdateOrderStatusRejectsManualPaidTransition",
"TestCanCompleteParentOrder", "TestCanCompleteParentOrderRejectInvalidStatus",
"TestCanCompleteParentOrderRejectInvalidChild",
@@ -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
}
@@ -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)
}
}