4. 订单新增事件处理

在订单状态机中,“新增”是整条生命周期的起点。新增事件处理得好不好,直接决定了后续支付、发货、撤销等环节能否建立在干净、可信的数据之上。本节围绕四个关键问题展开:新增事件在什么条件下才应被触发、订单初始状态如何构造、进入状态机前要做哪些数据校验,以及如何用幂等性设计抵御重复提交与消息重投。

一、触发条件:什么才算一次合法的“新增”

新增事件不是“收到一个请求”就触发,而是需要满足一组前置条件。典型条件包括:请求携带的订单号在系统中不存在(或符合可新建的规则)、下单用户处于可用状态、商品与价格信息有效、请求来源通过鉴权。只有这些条件同时成立,才允许生成 OrderCreated 事件并驱动状态机从“无”迁移到“待支付”。

这里要区分两类失败:一类是业务拒绝,例如用户被冻结、商品已下架,此时应返回明确的业务错误码,不产生任何状态;另一类是重复请求,订单号已存在且内容一致,这不应视为错误,而应走幂等返回路径。把“拒绝”和“重复”分开处理,是新增事件设计的第一条纪律。

二、状态初始化:初始状态要一次定准

订单创建时写入的初始状态,是后续所有迁移的基准。以常见的电商订单为例,新增成功后订单进入 CREATED(已创建/待支付),同时初始化一组伴随字段:订单版本号 version=0、支付状态 UNPAID、履约状态 NOT_SHIPPED、创建时间与过期时间。过期时间很关键,它决定了“待支付超时自动撤销”这条迁移的触发时刻。

状态初始化应遵循“一次写全、后续只迁移”的原则:新增时把订单主体、明细、金额快照、状态字段在同一事务内落库,避免出现“订单已存在但状态为空”的中间态。金额、单价等应保存下单时刻的快照,而不是运行时再去查商品表,否则后续调价会污染历史订单。

订单新增事件处理链路 下单请求 orderNo + 明细 触发条件校验 用户可用 · 商品有效 鉴权通过 · 订单号规则 幂等性判断 按 orderNo 查重 比对请求指纹 数据校验 金额 · 数量 · 快照 字段完整性 业务拒绝 返回错误码,不产生状态 幂等命中 返回已有订单,不重复创建 校验失败 拒绝并记录原因 创建订单(单事务) 主体 + 明细 + 金额快照 + 状态字段 version=0 · 过期时间写入 初始状态 CREATED UNPAID · NOT_SHIPPED OrderCreated 事件 驱动状态机迁移

三、数据校验:进入状态机前的最后一道闸

校验分三层。第一层是格式校验:订单号长度与字符集、明细数量是否在 1~200 之间、金额是否为非负整数(以分为单位存储,避免浮点误差)。第二层是业务校验:明细中的商品是否存在且可售、购买数量是否超过限购、优惠券是否属于该用户且未过期。第三层是一致性校验:明细金额之和加上运费、减去优惠后,是否等于订单应付总额;若不等,说明客户端计算或传输有误,必须拒绝。

一个常见的实现方式是在创建前计算“请求指纹”:把订单号、用户 ID、明细列表按固定顺序序列化后取哈希。这个指纹既用于幂等比对,也能在排障时快速判断两次请求是否真的相同。校验失败时应记录具体失败字段与原因,而不是笼统返回“参数错误”,否则线上问题很难定位。

四、幂等性设计:让重复请求安全落地

新增订单最容易出问题的地方就是重复创建。用户连点两次提交、网关超时后客户端重试、消息队列重投,都可能让同一笔业务被处理多次。幂等设计的核心是:以业务唯一键(通常是订单号或“用户+客户端请求号”)作为去重依据,并保证“判断—写入”这一步是原子的。

工程上有两种常用做法。其一是数据库唯一索引:在订单表对 order_no 建唯一约束,插入冲突时捕获异常,再回查已有订单返回。其二是去重表/幂等表:先以唯一键插入一条处理记录,插入成功者才继续创建订单,插入失败者直接返回已有结果。前者简单可靠,后者便于记录处理状态,适合跨服务场景。

// 伪代码:基于唯一索引的幂等创建
begin transaction;
  try {
    insert into t_order(order_no, user_id, status, version, amount, expire_at)
    values(#{orderNo}, #{userId}, 'CREATED', 0, #{amount}, #{expireAt});
    insert into t_order_item(...);   // 明细同事务写入
    commit;
    publish OrderCreatedEvent(orderNo);
  } catch (DuplicateKeyException e) {
    rollback;
    return queryOrderByNo(orderNo);  // 幂等返回已有订单
  }

需要注意两个细节。第一,幂等返回要区分“内容一致”与“内容冲突”:同一订单号但明细不同,说明客户端复用了订单号,应报冲突而非静默返回旧单。第二,事件发布要与落库解耦:推荐“本地事务表 + 异步投递”或事务消息,避免订单已入库但 OrderCreated 事件丢失,导致状态机停在初始态无人推进。

判断幂等是否真正生效,可以这样验证:用同一订单号连续提交两次,观察数据库中该订单只有一条记录、明细不重复、OrderCreated 事件只被消费一次,且第二次请求返回的订单 ID 与第一次相同。若第二次返回了新建 ID 或出现两条明细,说明去重环节存在漏洞。
不要把幂等键设成“自增主键”或“时间戳”。前者每次请求都不同,等于没有去重;后者在并发下几乎必然重复。幂等键必须由业务侧生成并在重试时保持不变,通常由客户端生成请求号随请求一起上送。

把以上四点串起来看:触发条件决定“该不该建”,状态初始化决定“建成什么样”,数据校验决定“数据可不可信”,幂等性决定“重复来了怎么办”。四者共同构成新增事件的完整防线,也为后续支付、撤销等状态迁移提供了稳定起点。