Appearance
订单系统
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_no | string | 订单号 |
member_id | int | 会员 ID |
order_type | string | 订单类型 |
status | string | 订单状态 |
total_amount | int | 商品总额(分) |
freight_amount | int | 运费(分) |
coupon_amount | int | 优惠金额(分) |
pay_amount | int | 实付金额(分) |
订单商品表
| 字段 | 类型 | 说明 |
|---|---|---|
order_id | int | 订单 ID |
product_id | int | 商品 ID |
sku_id | int | SKU ID |
quantity | int | 数量 |
price | int | 单价(分) |
total_amount | int | 小计(分) |
🕐 自动关单
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/preview | POST | 订单预览 |
/api/order/submit | POST | 提交订单 |
/api/order/result | GET | 查询下单结果 |
/api/order/list | GET | 订单列表 |
/api/order/{id} | GET | 订单详情 |
/api/order/{id}/cancel | POST | 取消订单 |
/api/order/{id}/confirm | POST | 确认收货 |
后台 API
| 接口 | 方法 | 说明 |
|---|---|---|
/admin/order | GET | 订单列表 |
/admin/order/{id} | GET | 订单详情 |
/admin/order/{id}/ship | POST | 发货 |
/admin/order/export | POST | 导出订单 |
🔍 下单结果轮询
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: '下单超时' };
}⚠️ 注意事项
- 库存一致性: 使用 Lua 脚本保证原子性
- 超时处理: 定时任务自动关闭超时订单
- 幂等性: 订单号唯一,防止重复下单
- 降级策略: 高并发时可启用限流