Просмотр исходного кода

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

- 数据库模型:添加cancel_reason和cancel_time字段记录取消信息
- API接口:创建cancel-order.mt.ts路由文件实现取消订单接口
- 业务逻辑:实现订单状态验证、库存恢复和退款准备逻辑
- 路由聚合:在orders.mt.ts中聚合取消订单路由
- 文档更新:更新订单取消功能实现步骤文档

♻️ refactor(order): 优化订单路由结构

- 将订单创建和取消功能拆分为独立路由文件
- 采用路由聚合方式组织相关API端点
- 完善请求验证和错误处理机制
yourname 1 месяц назад
Родитель
Сommit
177d733efe

+ 2 - 1
docs/stories/011.002.order-cancel-function.md

@@ -24,10 +24,11 @@ Draft
   - [ ] 更新订单状态为5(订单关闭),记录取消时间和原因
   - [ ] 确保多租户数据隔离
 - [ ] 添加取消订单API接口 (AC: 1, 2, 3)
-  - [ ] 在 `packages/orders-module-mt/src/routes/user/orders.mt.ts` 中添加取消订单API端点
+  - [ ] 创建 `packages/orders-module-mt/src/routes/user/cancel-order.mt.ts` 单独路由文件
   - [ ] 实现请求参数验证(订单ID、取消原因)
   - [ ] 调用 OrderMtService 的 cancelOrder 方法
   - [ ] 添加适当的错误处理和响应格式
+  - [ ] 在 `packages/orders-module-mt/src/routes/user/orders.mt.ts` 中聚合路由
 - [ ] 集成取消订单UI到订单详情页面 (AC: 1)
   - [ ] 在 `mini/src/pages/order-detail/index.tsx` 中添加取消订单按钮
   - [ ] 实现取消订单的UI交互逻辑

+ 6 - 0
packages/orders-module-mt/src/entities/order.mt.entity.ts

@@ -114,6 +114,12 @@ export class OrderMt {
   @Column({ name: 'close_time', type: 'timestamp', nullable: true, comment: '订单关闭时间' })
   closeTime!: Date | null;
 
+  @Column({ name: 'cancel_reason', type: 'varchar', length: 500, nullable: true, comment: '取消原因' })
+  cancelReason!: string | null;
+
+  @Column({ name: 'cancel_time', type: 'timestamp', nullable: true, comment: '取消时间' })
+  cancelTime!: Date | null;
+
   @Column({ name: 'remark', type: 'varchar', length: 255, nullable: true, comment: '管理员备注信息' })
   remark!: string | null;
 

+ 85 - 0
packages/orders-module-mt/src/routes/user/cancel-order.mt.ts

@@ -0,0 +1,85 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { AppDataSource } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { OrderMtService } from '../../services';
+import { CancelOrderRequestDto, CancelOrderResponseDto } from '../../schemas/cancel-order.schema';
+
+const cancelOrderRoute = createRoute({
+  method: 'post',
+  path: '/cancel-order',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CancelOrderRequestDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '订单取消成功',
+      content: {
+        'application/json': {
+          schema: CancelOrderResponseDto
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误'
+    },
+    403: {
+      description: '订单状态不允许取消'
+    },
+    404: {
+      description: '订单不存在'
+    },
+    500: {
+      description: '服务器内部错误'
+    }
+  }
+})
+
+const cancelOrderRoutes = new OpenAPIHono<AuthContext>()
+  // 取消订单路由
+  .openapi( cancelOrderRoute, async (c) => {
+      const data = c.req.valid('json');
+      const user = c.get('user');
+
+      try {
+        const orderService = new OrderMtService(AppDataSource);
+        await orderService.cancelOrder(user.tenantId, data.orderId, data.reason, user.id);
+
+        return c.json({
+          success: true,
+          message: '订单取消成功'
+        }, 200);
+      } catch (error) {
+        console.error('取消订单失败:', error);
+
+        // 根据错误类型返回不同的状态码
+        if (error instanceof Error) {
+          if (error.message === '订单不存在') {
+            return c.json(
+              { error: error.message },
+              404
+            );
+          } else if (error.message === '当前订单状态不允许取消') {
+            return c.json(
+              { error: error.message },
+              403
+            );
+          }
+        }
+
+        return c.json(
+          { error: error instanceof Error ? error.message : '取消订单失败' },
+          500
+        );
+      }
+    }
+  );
+
+export default cancelOrderRoutes;

+ 3 - 1
packages/orders-module-mt/src/routes/user/orders.mt.ts

@@ -5,6 +5,7 @@ import { UserCreateOrderDto, UserUpdateOrderDto } from '../../schemas/user-order
 import { authMiddleware } from '@d8d/auth-module-mt';
 import { OpenAPIHono } from '@hono/zod-openapi';
 import createOrderRoutes from './create-order.mt';
+import cancelOrderRoutes from './cancel-order.mt';
 
 // 多租户用户订单路由 - 有数据权限限制,只能访问自己的订单
 const userOrderCrudRoutes = createCrudRoutes({
@@ -32,6 +33,7 @@ const userOrderCrudRoutes = createCrudRoutes({
 });
 const userOrderRoutes = new OpenAPIHono()
   .route('/', createOrderRoutes)
+  .route('/', cancelOrderRoutes)
   .route('/', userOrderCrudRoutes)
-  
+
 export default userOrderRoutes;

+ 13 - 0
packages/orders-module-mt/src/schemas/cancel-order.schema.ts

@@ -0,0 +1,13 @@
+import { z } from '@hono/zod-openapi';
+
+// 取消订单请求Schema
+export const CancelOrderRequestDto = z.object({
+  orderId: z.number().int().positive().describe('订单ID'),
+  reason: z.string().min(1).max(500).describe('取消原因')
+});
+
+// 取消订单响应Schema
+export const CancelOrderResponseDto = z.object({
+  success: z.boolean().describe('操作是否成功'),
+  message: z.string().describe('操作结果消息')
+});

+ 69 - 0
packages/orders-module-mt/src/services/order.mt.service.ts

@@ -166,6 +166,75 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
     }
   }
 
+  /**
+   * 取消订单
+   * @param tenantId 租户ID
+   * @param orderId 订单ID
+   * @param reason 取消原因
+   * @param userId 用户ID
+   * @returns Promise<void>
+   */
+  async cancelOrder(tenantId: number, orderId: number, reason: string, userId: number): Promise<void> {
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    try {
+      // 查询订单信息
+      const order = await this.repository.findOne({
+        where: { id: orderId, tenantId }
+      });
+
+      if (!order) {
+        throw new Error('订单不存在');
+      }
+
+      // 验证订单状态(仅允许取消支付状态为0或2的订单)
+      if (order.payState !== 0 && order.payState !== 2) {
+        throw new Error('当前订单状态不允许取消');
+      }
+
+      // 对于已支付订单(支付状态=2),需要触发退款流程
+      if (order.payState === 2) {
+        // TODO: 调用支付模块的退款功能(将在Story 3中实现)
+        // 这里先记录日志,后续集成支付退款功能
+        console.log(`订单 ${orderId} 已支付,需要触发退款流程`);
+      }
+
+      // 更新订单状态为5(订单关闭),记录取消时间和原因
+      await queryRunner.manager.update(OrderMt, { id: orderId, tenantId }, {
+        payState: 5, // 订单关闭
+        cancelReason: reason,
+        cancelTime: new Date(),
+        updatedBy: userId,
+        updatedAt: new Date()
+      });
+
+      // 如果订单是未支付状态,需要恢复商品库存
+      if (order.payState === 0) {
+        // 查询订单商品明细
+        const orderGoods = await this.orderGoodsRepository.find({
+          where: { orderId, tenantId }
+        });
+
+        // 恢复商品库存
+        for (const item of orderGoods) {
+          await queryRunner.manager.update(GoodsMt, { id: item.goodsId, tenantId }, {
+            stock: () => `stock + ${item.num}`,
+            salesNum: () => `sales_num - ${item.num}`
+          });
+        }
+      }
+
+      await queryRunner.commitTransaction();
+    } catch (error) {
+      await queryRunner.rollbackTransaction();
+      throw error;
+    } finally {
+      await queryRunner.release();
+    }
+  }
+
   /**
    * 生成订单号
    * 格式:年月日时分秒 + 6位随机数