跳到主要内容

order — 订单

定位:购物车结算 / 直购下单(单事务:订单 + 订单项 + 库存扣减 + 地址快照 + 券核销 + 清购物车)、client_request_id 幂等、雪花订单号、状态机(待支付→已支付→已发货→已完成,含取消与超时取消),并向 flashsale 编排提供事务内秒杀订单能力。

实现:internal/order/

数据模型

orders

字段类型说明
order_noVARCHAR(20) PK雪花 ID 十进制字符串(避免 JS 精度丢失,JSON 按字符串序列化)
user_idBIGINT UNSIGNED FK下单用户(CASCADE)
client_request_idVARBINARY(64) NULL普通订单持久请求身份;与 user_id 联合唯一,秒杀/历史订单为 NULL
order_typeVARCHAR(16)normal / seckill(秒杀订单不使用优惠券)
statusVARCHAR(16)状态机(见下)
activity_idBIGINT UNSIGNED NULL秒杀活动(秒杀订单专属,FK RESTRICT)
purchase_slotBIGINT UNSIGNED NULL秒杀预扣分配的稳定购买槽位
total_amount / discount_amount / pay_amountBIGINT UNSIGNED总额 − 券额 = 应付(分),关系由应用预检与数据库 CHECK 双重保证
coupon_idBIGINT UNSIGNED NULL核销的用户券(FK RESTRICT)
receiver / phone / province / city / district / detailVARCHAR地址快照(下单时从地址簿固化,后续改地址不影响历史订单)
paid_at / shipped_at / completed_at / cancelled_atDATETIME(3) NULL各状态时间戳
expire_atDATETIME(3)超时取消时间(普通 15min;秒杀 10min
user_activity_keyVARCHAR(64) NULL UNIQUE秒杀槽位去重键:落单写 user_id:activity_id:purchase_slot,取消同事务置 NULL;不同槽位可并存,同槽消息重投只命中一单

状态机(model.CanTransition,仅合法跃迁,非法一律拒绝):

pending_payment(待支付) ──支付回调──▶ paid(已支付) ──后台发货──▶ shipped(已发货) ──确认收货──▶ completed(已完成)

└──用户取消 / 超时取消──▶ cancelled(已取消)

order_items

字段类型说明
idBIGINT UNSIGNED PK订单项 ID
order_noVARCHAR(20) FK所属订单(CASCADE)
sku_idBIGINT UNSIGNED FKSKU(RESTRICT:有订单历史不可删)
product_id / title / specs商品快照(标题/规格)
price / quantity / subtotal成交单价快照 / 数量 / 小计;价格上限 100,000,000 分,subtotal = price × quantity 由 CHECK 保证

接口

HTTP(handler/order_handler.go)

用户(Bearer):

方法路径说明
POST/api/orders下单 {client_request_id, address_id, coupon_id?, from_cart 或 items[]};创建 201 / 幂等命中 200 / 幂等键占用未落库 202(客户端轮询详情)
GET/api/orders我的订单(status 筛选 + 分页)
GET/api/orders/:order_no订单详情(owner 校验)
POST/api/orders/:order_no/cancel取消待支付订单;普通订单回补 SKU/券,秒杀订单转交 flashsale 编排回补活动库存、Redis 与购买槽位
POST/api/orders/:order_no/confirm确认收货(已发货 → 已完成,owner 校验)

admin(Bearer + admin):

方法路径说明
GET/api/admin/orders后台订单列表(全量跨用户,status 筛选 + 分页)
POST/api/admin/orders/:order_no/ship后台发货(已支付 → 已发货)

跨模块端口(进程内调用)

依赖(order 侧声明最小接口)ProductService(SKU 读取/条件扣减/回补)、CouponService(GetUsable/UseCoupon/RollbackCoupon)、CartService(LockItems/DeletePurchased)、UserService(GetAddress 快照)。order 不持有 flashsale service 或活动仓储。

对外端口(实现方为 order,被其他模块消费)CreateSeckillInTx(flashsale 消费者事务内建订单与订单项)、ListExpiredSeckill / CancelSeckill(flashsale 超时取消编排)、CountValidSeckill(flashsale 对账)、MarkPaid(payment 事务内条件更新)、GetDetail(payment owner 校验)、HasPurchasedSKU(social 动态分享校验)。

关键流程

普通下单(单事务 + 幂等)

POST /api/orders {client_request_id, address_id, ...}
[1] 参数校验(from_cart 与 items 互斥;直购同 SKU 多行合并,数量 1–99)
[2] 按 (user_id, client_request_id) 查询 MySQL 持久事实;命中直接返回原订单
[3] 未命中时生成雪花订单号 + 类型化缓存能力 AcquireIdempotency 原子抢占
order:idem:{uid}:{crid}(内部 SETNX + TTL 15min;已存在 → 返回既有订单号,
未落库则 202 轮询)
[4] 读地址(固化为快照)→ 组装订单项(购物车 LockItems / 直购)→ 校验券可用
[5] 单事务:
· 按 商品→SKU 固定顺序锁定并读取(GetSKUForUpdate),检查乘法与累加
· 在任何写入前验证总额、优惠额、应付额、逐项小计与订单项合计
· 条件扣减库存(stock >= N)
· 核销券(UseCoupon 条件更新 unused→used)
· 建订单 + 订单项(含地址快照;唯一键处理并发请求身份)
· 删除已结算购物车条目(DeletePurchased)
[6] 校验类失败 → 删除幂等键(允许修正后重试);基础设施失败先查订单,
未提交才释放(防瞬时故障下重试生成第二单)

取消与超时取消

Cancel(用户取消)
→ 事务:条件更新 待支付→已取消(RowsAffected=0 即状态已变,不重复回补)
· 普通订单:RestoreStock 回补 SKU 库存 + RollbackCoupon 回退券
· 秒杀订单:handler 转交 flashsale 编排,与活动库存回补和 pending_rollback 事实同事务
CancelExpired(cron 每分钟 order-timeout-cancel,批量上限 500)
→ 扫描待支付且已过 expire_at 的普通订单 → 逐个同事务取消(同上)
→ 单订单失败跳过计失败数,下个 tick 重试(不阻断整轮)
flashsale.SeckillCancellation(用户主动取消;cron 每分钟 seckill-timeout-cancel 处理超时)
→ order.ListExpiredSeckill 扫描待支付秒杀订单
→ 事务:order.CancelSeckill 条件更新取消 + flashsale 活动仓储回补 MySQL 库存
→ 事务提交后按订单槽位回补 Redis(库存按数量 INCR + 用户槽位计数 DECR 1 + 释放对应槽位键;
best-effort,失败由对账 cron 兜底)

支付状态迁移(payment 模块事务内调用)

MarkPaid(tx, orderNo, payAmount)
→ UPDATE orders SET status='paid' WHERE order_no=? AND status='pending_payment'
AND pay_amount=? AND expire_at>? ← 状态、金额与期限由同一 WHERE 原子兜底
false = 状态已变、金额不符或订单过期(支付流水整体回滚)

权威源:docs/DESIGN.md 订单状态机 / 普通订单流程、迁移 000009_orders / 000010_order_foreign_keys / 000014_seckill_repurchase / 000019_order_payment_invariants