Browse Source

✨ feat(orders): 实现订单创建功能

- 添加订单创建API接口(/create-order/create)
- 实现订单创建服务层逻辑,包括事务处理
- 添加订单请求/响应DTO和数据验证
- 实现订单号生成算法
- 添加订单商品关系处理
- 集成用户认证中间件
- 优化订单路由聚合结构
yourname 4 months ago
parent
commit
4388236727

+ 77 - 0
src/server/api/orders/create-order.ts

@@ -0,0 +1,77 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { CreateOrderRequestDto, CreateOrderResponseDto } from '@/server/modules/orders/dto/create-order-request.dto';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { OrderService } from '@/server/modules/orders/order.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'post',
+  path: '/create',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateOrderRequestDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '订单创建成功',
+      content: {
+        'application/json': {
+          schema: CreateOrderResponseDto
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const data = await c.req.json();
+    const user = c.get('user');
+    
+    if (!user?.id) {
+      return c.json({ code: 401, message: '用户未登录' }, 401);
+    }
+
+    const service = new OrderService(AppDataSource);
+    const result = await service.createOrder(data, user.id);
+
+    return c.json({
+      success: true,
+      orderId: result.orderId,
+      orderNo: result.orderNo,
+      amount: result.amount,
+      payAmount: result.payAmount,
+      message: '订单创建成功'
+    }, 200);
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '创建订单失败';
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+export default app;

+ 11 - 1
src/server/api/orders/index.ts

@@ -1,8 +1,10 @@
 import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
 import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OpenAPIHono } from '@hono/zod-openapi';
 import { Order } from '@/server/modules/orders/order.entity';
 import { Order } from '@/server/modules/orders/order.entity';
 import { OrderSchema, CreateOrderDto, UpdateOrderDto } from '@/server/modules/orders/order.schema';
 import { OrderSchema, CreateOrderDto, UpdateOrderDto } from '@/server/modules/orders/order.schema';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
 
 
+// 基础CRUD路由
 const orderRoutes = createCrudRoutes({
 const orderRoutes = createCrudRoutes({
   entity: Order,
   entity: Order,
   createSchema: CreateOrderDto,
   createSchema: CreateOrderDto,
@@ -18,4 +20,12 @@ const orderRoutes = createCrudRoutes({
   }
   }
 });
 });
 
 
-export default orderRoutes;
+// 导入扩展路由
+import createOrderRoute from './create-order';
+
+// 聚合所有路由
+const app = new OpenAPIHono()
+  .route('/', orderRoutes)  // 基础CRUD路由
+  .route('/create-order', createOrderRoute);  // 创建订单专用路由
+
+export default app;

+ 66 - 0
src/server/modules/orders/dto/create-order-request.dto.ts

@@ -0,0 +1,66 @@
+import { z } from '@hono/zod-openapi';
+
+// 订单商品项DTO
+export const OrderItemDto = z.object({
+  id: z.number().int().positive('商品ID必须是正整数').openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  num: z.number().int().min(1, '购买数量必须大于0').openapi({
+    description: '购买数量',
+    example: 2
+  })
+});
+
+// 创建订单请求DTO
+export const CreateOrderRequestDto = z.object({
+  addressId: z.number().int().positive('收货地址ID必须是正整数').optional().openapi({
+    description: '收货地址ID',
+    example: 1
+  }),
+  productOwn: z.string().min(1, '商品所属方不能为空').max(50, '商品所属方最多50个字符').openapi({
+    description: '商品所属方(如:自营、第三方等)',
+    example: '自营'
+  }),
+  consumeFrom: z.string().min(1, '消费来源不能为空').max(50, '消费来源最多50个字符').openapi({
+    description: '消费来源(如:积分兑换、礼券兑换等)',
+    example: '积分兑换'
+  }),
+  products: z.array(OrderItemDto).min(1, '商品列表不能为空').openapi({
+    description: '商品列表',
+    example: [{ id: 1, num: 2 }, { id: 3, num: 1 }]
+  })
+});
+
+// 创建订单响应DTO
+export const CreateOrderResponseDto = z.object({
+  success: z.boolean().openapi({
+    description: '是否成功',
+    example: true
+  }),
+  orderId: z.number().int().positive().openapi({
+    description: '订单ID',
+    example: 1001
+  }),
+  orderNo: z.string().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  amount: z.number().openapi({
+    description: '订单总金额',
+    example: 199.98
+  }),
+  payAmount: z.number().openapi({
+    description: '实际支付金额',
+    example: 199.98
+  }),
+  message: z.string().openapi({
+    description: '提示信息',
+    example: '订单创建成功'
+  })
+});
+
+// 导出类型
+export type CreateOrderRequest = z.infer<typeof CreateOrderRequestDto>;
+export type CreateOrderResponse = z.infer<typeof CreateOrderResponseDto>;
+export type OrderItem = z.infer<typeof OrderItemDto>;

+ 170 - 1
src/server/modules/orders/order.service.ts

@@ -1,9 +1,178 @@
 import { GenericCrudService } from '@/server/utils/generic-crud.service';
 import { GenericCrudService } from '@/server/utils/generic-crud.service';
-import { DataSource } from 'typeorm';
+import { DataSource, Repository } from 'typeorm';
 import { Order } from './order.entity';
 import { Order } from './order.entity';
+import { OrderGoods } from './order-goods.entity';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { DeliveryAddress } from '@/server/modules/delivery-address/delivery-address.entity';
+import type { CreateOrderRequest } from './dto/create-order-request.dto';
 
 
 export class OrderService extends GenericCrudService<Order> {
 export class OrderService extends GenericCrudService<Order> {
+  private orderGoodsRepository: Repository<OrderGoods>;
+  private goodsRepository: Repository<Goods>;
+  private deliveryAddressRepository: Repository<DeliveryAddress>;
+
   constructor(dataSource: DataSource) {
   constructor(dataSource: DataSource) {
     super(dataSource, Order);
     super(dataSource, Order);
+    this.orderGoodsRepository = dataSource.getRepository(OrderGoods);
+    this.goodsRepository = dataSource.getRepository(Goods);
+    this.deliveryAddressRepository = dataSource.getRepository(DeliveryAddress);
+  }
+
+  /**
+   * 创建订单
+   * @param data 创建订单请求数据
+   * @param userId 用户ID
+   * @returns 创建的订单信息
+   */
+  async createOrder(data: CreateOrderRequest, userId: number): Promise<{
+    orderId: number;
+    orderNo: string;
+    amount: number;
+    payAmount: number;
+  }> {
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    try {
+      const { addressId, productOwn, consumeFrom, products } = data;
+
+      // 验证商品信息并计算总价
+      let totalAmount = 0;
+      let totalCostAmount = 0;
+      const goodsInfo = [];
+
+      for (const item of products) {
+        const goods = await this.goodsRepository.findOne({
+          where: { id: item.id }
+        });
+
+        if (!goods) {
+          throw new Error(`商品ID ${item.id} 不存在`);
+        }
+
+        if (goods.stock < item.num) {
+          throw new Error(`商品 ${goods.name} 库存不足`);
+        }
+
+        const itemAmount = goods.price * item.num;
+        const itemCostAmount = goods.costPrice * item.num;
+
+        totalAmount += itemAmount;
+        totalCostAmount += itemCostAmount;
+
+        goodsInfo.push({
+          goods,
+          quantity: item.num,
+          amount: itemAmount,
+          costAmount: itemCostAmount
+        });
+      }
+
+      // 获取收货地址信息
+      let deliveryAddress = null;
+      if (addressId) {
+        deliveryAddress = await this.deliveryAddressRepository.findOne({
+          where: { id: addressId, userId }
+        });
+
+        if (!deliveryAddress) {
+          throw new Error('收货地址不存在');
+        }
+      }
+
+      // 生成订单号
+      const orderNo = this.generateOrderNo();
+
+      // 创建订单
+      const order = this.repository.create({
+        orderNo,
+        userId,
+        amount: totalAmount,
+        costAmount: totalCostAmount,
+        payAmount: totalAmount, // 这里可以根据优惠规则计算实际支付金额
+        orderType: 1, // 实物订单
+        payType: consumeFrom === '积分兑换' ? 1 : 2, // 根据消费来源设置支付类型
+        payState: 0, // 未支付
+        state: 0, // 未发货
+        addressId: addressId || 0,
+        merchantId: goodsInfo[0]?.goods.merchantId || 0,
+        supplierId: goodsInfo[0]?.goods.supplierId || 0,
+        createdBy: userId,
+        updatedBy: userId,
+        // 设置收货地址信息(如果提供)
+        ...(deliveryAddress && {
+          receiverMobile: deliveryAddress.phone,
+          recevierName: deliveryAddress.name,
+          recevierProvince: deliveryAddress.provinceId,
+          recevierCity: deliveryAddress.cityId,
+          recevierDistrict: deliveryAddress.districtId,
+          recevierTown: deliveryAddress.townId,
+          address: deliveryAddress.address
+        })
+      });
+
+      const savedOrder = await queryRunner.manager.save(order);
+
+      // 创建订单商品明细
+      const orderGoodsList = goodsInfo.map(info => ({
+        orderId: savedOrder.id,
+        orderNo,
+        goodsId: info.goods.id,
+        goodsName: info.goods.name,
+        goodsImage: info.goods.image,
+        goodsType: info.goods.goodsType,
+        supplierId: info.goods.supplierId,
+        costPrice: info.goods.costPrice,
+        price: info.goods.price,
+        num: info.quantity,
+        freightAmount: 0,
+        state: 0,
+        createdBy: userId,
+        updatedBy: userId
+      }));
+
+      await queryRunner.manager.save(OrderGoods, orderGoodsList);
+
+      // 更新商品库存
+      for (const item of goodsInfo) {
+        await queryRunner.manager.update(Goods, item.goods.id, {
+          stock: () => `stock - ${item.quantity}`,
+          sales: () => `sales + ${item.quantity}`
+        });
+      }
+
+      await queryRunner.commitTransaction();
+
+      return {
+        orderId: savedOrder.id,
+        orderNo: savedOrder.orderNo,
+        amount: savedOrder.amount,
+        payAmount: savedOrder.payAmount
+      };
+
+    } catch (error) {
+      await queryRunner.rollbackTransaction();
+      throw error;
+    } finally {
+      await queryRunner.release();
+    }
+  }
+
+  /**
+   * 生成订单号
+   * 格式:年月日时分秒 + 6位随机数
+   */
+  private generateOrderNo(): string {
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, '0');
+    const day = String(now.getDate()).padStart(2, '0');
+    const hour = String(now.getHours()).padStart(2, '0');
+    const minute = String(now.getMinutes()).padStart(2, '0');
+    const second = String(now.getSeconds()).padStart(2, '0');
+    const random = String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
+    
+    return `ORD${year}${month}${day}${hour}${minute}${second}${random}`;
   }
   }
 }
 }