4. 订单新增事件处理
在订单状态机中,“新增”是整条生命周期的起点。新增事件处理得好不好,直接决定了后续支付、发货、撤销等环节能否建立在干净、可信的数据之上。本节围绕四个关键问题展开:新增事件在什么条件下才应被触发、订单初始状态如何构造、进入状态机前要做哪些数据校验,以及如何用幂等性设计抵御重复提交与消息重投。
一、触发条件:什么才算一次合法的“新增”
新增事件不是“收到一个请求”就触发,而是需要满足一组前置条件。典型条件包括:请求携带的订单号在系统中不存在(或符合可新建的规则)、下单用户处于可用状态、商品与价格信息有效、请求来源通过鉴权。只有这些条件同时成立,才允许生成 OrderCreated 事件并驱动状态机从“无”迁移到“待支付”。
这里要区分两类失败:一类是业务拒绝,例如用户被冻结、商品已下架,此时应返回明确的业务错误码,不产生任何状态;另一类是重复请求,订单号已存在且内容一致,这不应视为错误,而应走幂等返回路径。把“拒绝”和“重复”分开处理,是新增事件设计的第一条纪律。
二、状态初始化:初始状态要一次定准
订单创建时写入的初始状态,是后续所有迁移的基准。以常见的电商订单为例,新增成功后订单进入 CREATED(已创建/待支付),同时初始化一组伴随字段:订单版本号 version=0、支付状态 UNPAID、履约状态 NOT_SHIPPED、创建时间与过期时间。过期时间很关键,它决定了“待支付超时自动撤销”这条迁移的触发时刻。
状态初始化应遵循“一次写全、后续只迁移”的原则:新增时把订单主体、明细、金额快照、状态字段在同一事务内落库,避免出现“订单已存在但状态为空”的中间态。金额、单价等应保存下单时刻的快照,而不是运行时再去查商品表,否则后续调价会污染历史订单。
三、数据校验:进入状态机前的最后一道闸
校验分三层。第一层是格式校验:订单号长度与字符集、明细数量是否在 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 或出现两条明细,说明去重环节存在漏洞。把以上四点串起来看:触发条件决定“该不该建”,状态初始化决定“建成什么样”,数据校验决定“数据可不可信”,幂等性决定“重复来了怎么办”。四者共同构成新增事件的完整防线,也为后续支付、撤销等状态迁移提供了稳定起点。