Skip to content

订单系统

MineShop 的订单系统采用异步下单模式,支持普通订单、秒杀订单、团购订单等多种类型,通过策略模式实现灵活扩展。

🎯 系统架构

┌─────────────────────────────────────────────────────────────┐
│                        下单流程                              │
├─────────────────────────────────────────────────────────────┤
│  同步阶段                                                    │
│  ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐     │
│  │ 参数校验 │ → │ 价格计算 │ → │ Lua扣库存│ → │ 投递队列 │     │
│  └─────────┘   └─────────┘   └─────────┘   └─────────┘     │
├─────────────────────────────────────────────────────────────┤
│  异步阶段                                                    │
│  ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐     │
│  │ 消费Job │ → │ 创建订单 │ → │ 扣优惠券 │ → │ 更新状态 │     │
│  └─────────┘   └─────────┘   └─────────┘   └─────────┘     │
└─────────────────────────────────────────────────────────────┘

✨ 核心特性

1. 异步下单

php
// DomainApiOrderCommandService.php
public function submit(OrderSubmitInput $input): OrderEntity
{
    // 1. 构建 + 校验 + 算价
    $entity = $this->buildOrder($input);
    
    // 2. 解析库存 Key
    $stockHashKey = DomainOrderStockService::resolveStockKey(
        $entity->getOrderType(), 
        $entity->getExtra('session_id') ?: $entity->getExtra('group_buy_id') ?: 0
    );

    // 3. Lua 原子扣库存
    $items = array_map(fn($item) => $item->toArray(), $entity->getItems());
    $this->stockService->reserve($items, $stockHashKey);

    // 4. 生成订单号,缓存状态
    $tradeNo = Order::generateOrderNo();
    $entity->setOrderNo($tradeNo);
    $this->pendingCacheService->markProcessing($tradeNo, $entitySnapshot);

    // 5. 投递异步 Job
    $this->driverFactory->get('default')->push(new OrderCreateJob(
        tradeNo: $tradeNo,
        entitySnapshot: $entitySnapshot,
        itemsPayload: $items,
        addressPayload: $addressPayload,
        couponUserIds: $entity->getAppliedCouponUserIds(),
        orderType: $entity->getOrderType(),
        stockHashKey: $stockHashKey,
    ));

    return $entity;
}

2. 订单类型策略

php
// OrderTypeStrategyFactory.php
public function make(string $orderType): OrderTypeStrategy
{
    return match ($orderType) {
        'normal' => $this->container->get(NormalOrderStrategy::class),
        'seckill' => $this->container->get(SeckillOrderStrategy::class),
        'group_buy' => $this->container->get(GroupBuyOrderStrategy::class),
        default => throw new \InvalidArgumentException("Unknown order type: {$orderType}"),
    };
}

3. 库存管理

php
// DomainOrderStockService.php
class DomainOrderStockService
{
    // 库存 Key 映射
    public const STOCK_HASH_KEY = 'product:stock';           // 普通商品
    public const SECKILL_STOCK_PREFIX = 'seckill:stock';     // 秒杀
    public const GROUP_BUY_STOCK_PREFIX = 'groupbuy:stock';  // 团购

    // Lua 原子扣库存脚本
    private const DEDUCT_SCRIPT = <<<'LUA'
        local stockKey = KEYS[1]
        -- 第一轮:检查库存
        for i = 1, #ARGV, 2 do
            local field = ARGV[i]
            local quantity = tonumber(ARGV[i + 1])
            local current = tonumber(redis.call('HGET', stockKey, field) or '-1')
            if current < quantity then
                return 0
            end
        end
        -- 第二轮:扣减库存
        for i = 1, #ARGV, 2 do
            local field = ARGV[i]
            local quantity = tonumber(ARGV[i + 1])
            redis.call('HINCRBY', stockKey, field, -quantity)
        end
        return 1
    LUA;

    // 预扣库存
    public function reserve(array $items, string $stockHashKey): void
    {
        $result = $this->redis()->eval(self::DEDUCT_SCRIPT, $payload, 1);
        if ((int) $result !== 1) {
            throw new \RuntimeException('库存不足或商品已下架');
        }
    }

    // 回滚库存
    public function rollback(array $items, string $stockHashKey): void
    {
        foreach ($normalized as $skuId => $quantity) {
            $this->redis()->hIncrBy($stockHashKey, (string) $skuId, $quantity);
        }
    }
}

🔄 订单状态

┌──────────┐    支付成功    ┌──────────┐    发货    ┌──────────┐
│  pending │ ───────────→ │   paid   │ ────────→ │ shipped  │
│  待支付   │              │  已支付   │           │  已发货   │
└──────────┘              └──────────┘           └──────────┘
      │                                               │
      │ 超时/取消                                      │ 确认收货
      ↓                                               ↓
┌──────────┐                                    ┌──────────┐
│ cancelled│                                    │ completed│
│  已取消   │                                    │  已完成   │
└──────────┘                                    └──────────┘

📦 订单结构

订单主表

字段类型说明
order_nostring订单号
member_idint会员 ID
order_typestring订单类型
statusstring订单状态
total_amountint商品总额(分)
freight_amountint运费(分)
coupon_amountint优惠金额(分)
pay_amountint实付金额(分)

订单商品表

字段类型说明
order_idint订单 ID
product_idint商品 ID
sku_idintSKU ID
quantityint数量
priceint单价(分)
total_amountint小计(分)

🕐 自动关单

php
// OrderAutoCloseCrontab.php
#[Crontab(
    name: 'order-auto-close',
    rule: '*/5 * * * *',  // 每5分钟执行
    callback: 'execute'
)]
class OrderAutoCloseCrontab
{
    public function execute(): void
    {
        // 获取超时未支付的订单
        $autoCloseMinutes = $this->mallSettingService->order()->autoCloseMinutes();
        $orders = $this->repository->findPendingOrdersOlderThan($autoCloseMinutes);
        
        foreach ($orders as $order) {
            try {
                // 取消订单
                $this->orderService->cancel($order->id);
                
                // 回滚库存
                $this->stockService->rollback($order->items, $stockHashKey);
                
                $this->logger->info('订单自动关闭', ['order_no' => $order->order_no]);
            } catch (\Throwable $e) {
                $this->logger->error('订单关闭失败', [
                    'order_no' => $order->order_no,
                    'error' => $e->getMessage()
                ]);
            }
        }
    }
}

💻 API 接口

前端 API

接口方法说明
/api/order/previewPOST订单预览
/api/order/submitPOST提交订单
/api/order/resultGET查询下单结果
/api/order/listGET订单列表
/api/order/{id}GET订单详情
/api/order/{id}/cancelPOST取消订单
/api/order/{id}/confirmPOST确认收货

后台 API

接口方法说明
/admin/orderGET订单列表
/admin/order/{id}GET订单详情
/admin/order/{id}/shipPOST发货
/admin/order/exportPOST导出订单

🔍 下单结果轮询

php
// 前端轮询获取下单结果
public function getSubmitResult(string $tradeNo): array
{
    return $this->pendingCacheService->getStatus($tradeNo);
    // 返回: ['status' => 'processing|created|failed', 'error' => '']
}
typescript
// 前端轮询示例
async function pollOrderResult(tradeNo: string) {
  const maxRetries = 10;
  const interval = 500; // 500ms
  
  for (let i = 0; i < maxRetries; i++) {
    const result = await api.getOrderResult(tradeNo);
    
    if (result.status === 'created') {
      return { success: true, orderNo: tradeNo };
    }
    
    if (result.status === 'failed') {
      return { success: false, error: result.error };
    }
    
    await sleep(interval);
  }
  
  return { success: false, error: '下单超时' };
}

⚠️ 注意事项

  1. 库存一致性: 使用 Lua 脚本保证原子性
  2. 超时处理: 定时任务自动关闭超时订单
  3. 幂等性: 订单号唯一,防止重复下单
  4. 降级策略: 高并发时可启用限流

📚 相关文档

基于 Apache-2.0 许可发布 | 感谢 MineAdmin 提供的优秀基础框架