소스 검색

✨ feat(order): 实现订单取消功能

- 在OrderMtService中添加cancelOrder方法,支持订单状态验证、库存恢复和退款流程
- 创建单独的取消订单路由文件cancel-order.mt.ts,包含参数验证和错误处理
- 在订单详情页面集成取消订单UI,包括取消按钮和原因输入对话框
- 编写完整集成测试,覆盖各种场景和权限验证
- 新增取消订单相关Schema和实体字段

📝 docs(story): 更新订单取消功能文档

- 添加Agent Model和Debug Log引用信息
- 完善Completion Notes List和File List
- 记录详细的变更日志和实现细节
yourname 1 개월 전
부모
커밋
8846cf03da

+ 44 - 0
docs/stories/011.002.order-cancel-function.md

@@ -105,12 +105,56 @@ Draft
 *This section is populated by the development agent during implementation*
 
 ### Agent Model Used
+Claude Sonnet 4.5 (d8d-model)
 
 ### Debug Log References
+- 修复了订单实体缺少取消相关字段的问题
+- 修复了小程序API调用参数错误
+- 遵循了现有路由结构模式(单独文件+聚合)
 
 ### Completion Notes List
+1. ✅ 在 OrderMtService 中添加了完整的 cancelOrder 方法,包含:
+   - 订单状态验证(仅允许取消支付状态为0或2的订单)
+   - 已支付订单的退款流程占位符
+   - 订单状态更新为5(订单关闭)
+   - 取消原因和时间记录
+   - 未支付订单的商品库存恢复
+
+2. ✅ 创建了单独的取消订单路由文件 `cancel-order.mt.ts`,包含:
+   - 完整的请求参数验证Schema
+   - 适当的错误处理和状态码返回
+   - 多租户数据隔离验证
+
+3. ✅ 在订单详情页面集成了取消订单UI,包含:
+   - 未支付订单的取消按钮
+   - 已支付订单的取消按钮(带退款提示)
+   - 取消原因输入对话框
+   - 完整的用户交互反馈
+
+4. ✅ 编写了完整的集成测试,包含:
+   - 成功取消未支付订单
+   - 成功取消已支付订单
+   - 拒绝不允许的订单状态
+   - 拒绝不存在的订单
+   - 跨租户和跨用户权限验证
 
 ### File List
+- **新增文件**:
+  - `packages/orders-module-mt/src/schemas/cancel-order.schema.ts`
+  - `packages/orders-module-mt/src/routes/user/cancel-order.mt.ts`
+
+- **修改文件**:
+  - `packages/orders-module-mt/src/services/order.mt.service.ts`
+  - `packages/orders-module-mt/src/entities/order.mt.entity.ts`
+  - `packages/orders-module-mt/src/routes/user/orders.mt.ts`
+  - `mini/src/pages/order-detail/index.tsx`
+  - `packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts`
+
+### Change Log
+| Date | Version | Description | Author |
+|------|---------|-------------|---------|
+| 2025-11-21 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+| 2025-11-21 | 1.1 | 实现订单取消功能 | James (Developer) |
 
 ## QA Results
 *Results from QA Agent QA review of the completed story implementation*

+ 70 - 5
mini/src/pages/order-detail/index.tsx

@@ -1,5 +1,5 @@
 import { View, ScrollView, Text } from '@tarojs/components'
-import { useQuery } from '@tanstack/react-query'
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
 import Taro from '@tarojs/taro'
 import { orderClient } from '@/api'
 import { InferResponseType } from 'hono'
@@ -13,6 +13,7 @@ export default function OrderDetailPage() {
   // 获取订单ID
   const params = Taro.getCurrentInstance().router?.params
   const orderId = params?.id ? parseInt(params.id) : 0
+  const queryClient = useQueryClient()
 
   const { data: order, isLoading } = useQuery({
     queryKey: ['order', orderId],
@@ -29,6 +30,36 @@ export default function OrderDetailPage() {
     staleTime: 5 * 60 * 1000,
   })
 
+  // 取消订单mutation
+  const cancelOrderMutation = useMutation({
+    mutationFn: async (reason: string) => {
+      const response = await orderClient.cancelOrder.$post({
+        json: {
+          orderId,
+          reason
+        }
+      })
+      if (response.status !== 200) {
+        throw new Error('取消订单失败')
+      }
+      return response.json()
+    },
+    onSuccess: () => {
+      // 取消成功后刷新订单数据
+      queryClient.invalidateQueries({ queryKey: ['order', orderId] })
+      Taro.showToast({
+        title: '订单取消成功',
+        icon: 'success'
+      })
+    },
+    onError: (error) => {
+      Taro.showToast({
+        title: error.message,
+        icon: 'error'
+      })
+    }
+  })
+
   // 解析商品详情
   const parseGoodsDetail = (goodsDetail: string | null) => {
     try {
@@ -182,10 +213,17 @@ export default function OrderDetailPage() {
                   content: '确定要取消订单吗?',
                   success: (res) => {
                     if (res.confirm) {
-                      // 调用取消订单API
-                      Taro.showToast({
-                        title: '已取消订单',
-                        icon: 'success'
+                      // 显示输入取消原因的对话框
+                      Taro.showModal({
+                        title: '取消原因',
+                        content: '',
+                        editable: true,
+                        placeholderText: '请输入取消原因...',
+                        success: (reasonRes) => {
+                          if (reasonRes.confirm && reasonRes.content) {
+                            cancelOrderMutation.mutate(reasonRes.content)
+                          }
+                        }
                       })
                     }
                   }
@@ -203,6 +241,33 @@ export default function OrderDetailPage() {
             </>
           )}
           
+          {order.payState === 2 && order.state === 0 && (
+            <Button variant="outline" onClick={() => {
+              Taro.showModal({
+                title: '取消订单',
+                content: '确定要取消订单吗?(已支付订单将触发退款流程)',
+                success: (res) => {
+                  if (res.confirm) {
+                    // 显示输入取消原因的对话框
+                    Taro.showModal({
+                      title: '取消原因',
+                      content: '',
+                      editable: true,
+                      placeholderText: '请输入取消原因...',
+                      success: (reasonRes) => {
+                        if (reasonRes.confirm && reasonRes.content) {
+                          cancelOrderMutation.mutate(reasonRes.content)
+                        }
+                      }
+                    })
+                  }
+                }
+              })
+            }}>
+              取消订单
+            </Button>
+          )}
+
           {order.state === 1 && (
             <Button onClick={() => {
               Taro.showModal({

+ 181 - 0
packages/orders-module-mt/tests/integration/user-orders-routes.integration.test.ts

@@ -185,4 +185,185 @@ describe('多租户用户订单管理API集成测试', () => {
       expect(createdOrder.userId).toBe(testUser.id);
     });
   });
+
+  describe('取消订单功能验证', () => {
+    it('应该成功取消未支付订单', async () => {
+      // 创建未支付订单
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 0, // 未支付
+        state: 0
+      });
+
+      const cancelData = {
+        orderId: order.id,
+        reason: '用户主动取消'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+
+      expect(result.success).toBe(true);
+      expect(result.message).toBe('订单取消成功');
+
+      // 验证订单状态已更新
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder?.payState).toBe(5); // 订单关闭
+      expect(updatedOrder?.cancelReason).toBe('用户主动取消');
+      expect(updatedOrder?.cancelTime).toBeInstanceOf(Date);
+    });
+
+    it('应该成功取消已支付订单', async () => {
+      // 创建已支付订单
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 2, // 支付成功
+        state: 0
+      });
+
+      const cancelData = {
+        orderId: order.id,
+        reason: '用户主动取消(已支付)'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+
+      expect(result.success).toBe(true);
+      expect(result.message).toBe('订单取消成功');
+
+      // 验证订单状态已更新
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const updatedOrder = await dataSource.getRepository(OrderMt).findOne({
+        where: { id: order.id, tenantId: 1 }
+      });
+
+      expect(updatedOrder?.payState).toBe(5); // 订单关闭
+      expect(updatedOrder?.cancelReason).toBe('用户主动取消(已支付)');
+      expect(updatedOrder?.cancelTime).toBeInstanceOf(Date);
+    });
+
+    it('应该拒绝取消不允许的订单状态', async () => {
+      // 创建已发货订单(支付状态=2,订单状态=1)
+      const order = await testFactory.createTestOrder(testUser.id, {
+        tenantId: 1,
+        payState: 2, // 支付成功
+        state: 1 // 已发货
+      });
+
+      const cancelData = {
+        orderId: order.id,
+        reason: '尝试取消已发货订单'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      // 应该返回403,因为已发货订单不允许取消
+      expect(response.status).toBe(403);
+      const result = await response.json();
+
+      expect(result.error).toBe('当前订单状态不允许取消');
+    });
+
+    it('应该拒绝取消不存在的订单', async () => {
+      const cancelData = {
+        orderId: 99999, // 不存在的订单ID
+        reason: '取消不存在的订单'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(response.status).toBe(404);
+      const result = await response.json();
+
+      expect(result.error).toBe('订单不存在');
+    });
+
+    it('应该拒绝跨租户取消订单', async () => {
+      // 创建租户2的订单
+      const otherTenantOrder = await testFactory.createTestOrder(otherTenantUser.id, {
+        tenantId: 2,
+        payState: 0
+      });
+
+      const cancelData = {
+        orderId: otherTenantOrder.id,
+        reason: '跨租户取消尝试'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      // 应该返回404,因为订单不在当前租户
+      expect(response.status).toBe(404);
+      const result = await response.json();
+
+      expect(result.error).toBe('订单不存在');
+    });
+
+    it('应该拒绝跨用户取消订单', async () => {
+      // 创建其他用户的订单(同一租户)
+      const otherUserOrder = await testFactory.createTestOrder(otherUser.id, {
+        tenantId: 1,
+        payState: 0
+      });
+
+      const cancelData = {
+        orderId: otherUserOrder.id,
+        reason: '跨用户取消尝试'
+      };
+
+      const response = await client.cancelOrder.$post({
+        json: cancelData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${userToken}`
+        }
+      });
+
+      // 应该返回404,因为无权访问其他用户的订单
+      expect(response.status).toBe(404);
+      const result = await response.json();
+
+      expect(result.error).toBe('订单不存在');
+    });
+  });
 });