为什么要单独设计订单状态机
很多同学写订单模块,第一反应是用一个 Integer 字段存状态,然后在业务代码里写一堆 if-else 判断“当前状态能不能执行这个操作”。订单少时没问题,一旦状态变多、角色变多(用户、司机、系统定时任务都能改状态),这种写法很快会失控:非法状态跳转难以排查,新加一个状态要改动十几处。
本文分享一套开源代驾系统的订单状态机设计。
源码地址: gitee.com/zhoujian666…
代驾订单有哪些状态
代驾业务的完整生命周期:
- 待接单:用户已下单,等待附近司机抢单
- 已接单:司机抢单成功,正前往用户位置
- 司机已到达:司机抵达上车点
- 行程中:用户上车,开始计费
- 待支付:行程结束,等待用户付款
- 已支付:支付成功(若有垫付/余额支付可能直接流转)
- 待评价:等待用户评价
- 已完成:订单闭环
- 已取消:下单后未接单可取消;接单后取消涉及违约金判断
核心设计:状态 + 事件 + 转移
状态机三要素:
- State(状态):枚举,列出所有合法状态
- Event(事件):触发状态变更的动作,如“司机接单”“到达上车点”“支付成功”“用户取消”
- Transition(转移):定义“当前状态 + 事件 → 目标状态”的合法映射
关键思想是:把“能不能跳转”的规则集中声明,而不是散落在业务代码里。
落地方式
1. 状态枚举集中管理
用枚举定义状态码、状态描述,避免魔法数字。订单实体里的状态字段只允许取枚举值。
2. 事件与转移表
用一张转移映射表声明规则,例如:
- 待接单 + 接单事件 → 已接单
- 已接单 + 到达事件 → 司机已到达
- 司机已到达 + 开始行程事件 → 行程中
- 行程中 + 结束行程事件 → 待支付
- 待支付 + 支付成功事件 → 待评价
- 任意未完成态 + 取消事件 → 已取消(带条件校验)
3. 统一的状态变更入口
所有状态修改走同一个方法:传入订单、事件,方法内部查转移表,找不到合法映射就抛异常,找到则更新状态并记录变更日志。
这样非法跳转在入口就被拦截,不会污染数据。
事件驱动:状态变更后要做什么
状态变更本身和“变更后的副作用”要解耦。例如订单变为“已接单”后要:
- 推送通知给用户
- 通知其他抢单司机该单已被接
- 给接单侧写一条数据
这些副作用不写在状态机里,而是状态变更成功后发布领域事件,由各自的监听者处理(推送走 Netty,异步任务走 RabbitMQ)。状态机保持纯粹,只负责“状态对不对”。
取消场景的特殊处理
取消是最复杂的事件:
- 待接单时取消:直接关闭,无费用
- 已接单后取消:根据司机是否已到达、等待时长判断是否收空驶费/违约金
- 行程中一般不允许取消,需走异常订单流程
这类带业务条件的判断放在事件处理层,状态机只接收“已经算好的、合法的取消结果”。
状态流转的可观测性
每次状态变更写一条流转日志(订单 ID、原状态、新状态、触发事件、操作人、时间)。线上排查“订单怎么变成这个状态了”时,这张表是最重要的依据,也方便后台给客服展示订单时间轴。
项目其他模块
除订单状态机外,系统还包含:智能派单、Netty WebSocket 实时推送、计价引擎、Redis 在线司机管理、微信/支付宝支付、分润对账。后端 + 管理后台已开源(Java 17 + Spring Boot 2.7 + Vue),可直接部署。
欢迎评论区交流状态机设计,觉得有用欢迎 Star。