别再写一堆 if else 了:电商状态机和处理器模式这次彻底讲透
电商系统里的状态机要放数据库还是代码里?一次讲清状态机、处理器模式与状态日志表设计
大家好,我是一名有 4 年工作经验的 Java 后端开发。
最近在整理电商后台和业务系统里的状态流转设计,发现很多项目一开始状态少、动作少,写几个 if else 就能跑;但随着订单、售后、商品、优惠券这些模块越来越复杂,代码会很快失控。
这篇文章我想结合电商订单场景,系统聊一聊状态机到底该怎么落地,状态规则应该放数据库还是代码里,处理器模式怎么配合,状态日志表又该怎么设计。
🦅个人主页
🐼
文章目录
- 电商系统里的状态机要放数据库还是代码里?一次讲清状态机、处理器模式与状态日志表设计
- 一、前言
- 二、业务场景
- 2.1 订单状态
- 2.2 订单动作
- 2.3 真实业务要求
- 三、问题现象
- 3.1 状态判断散落在各层
- 3.2 状态流转规则说不清
- 3.3 一个动作不只是改状态
- 3.4 前后端判断不一致
- 四、原理分析
- 4.1 状态机解决什么?
- 4.2 处理器模式解决什么?
- 4.3 为什么这两个要组合使用?
- 规则问题
- 执行问题
- 4.4 状态机规则应该放数据库还是代码?
- 五、为什么大多数电商状态机更适合放代码里
- 5.1 核心业务规则通常不应该交给配置随便改
- 5.2 放代码里更适合排查和调试
- 5.3 动作执行逻辑本来就在代码里
- 5.4 什么时候才适合放数据库?
- 六、最推荐的落地方式
- 6.1 状态枚举
- 6.2 动作枚举
- 6.3 流转规则表
- 6.4 处理器模式
- 6.5 状态日志表
- 七、落地代码示例
- 7.1 定义订单状态枚举
- 7.2 定义订单动作枚举
- 7.3 定义状态机规则表
- 7.4 定义动作处理器接口
- 7.5 发货处理器
- 7.6 取消订单处理器
- 7.7 统一执行入口
- 八、数据库里到底该存什么?
- 8.1 当前状态要落数据库
- 8.2 状态变更日志也建议落数据库
- 8.3 核心状态规则不建议直接放数据库
- 九、什么时候适合把状态规则做成数据库配置?
- 放代码里的
- 放数据库里的
- 十、为什么很多项目上了状态机,最后还是不好维护?
- 10.1 只有状态机,没有动作处理器
- 10.2 规则写在数据库,执行逻辑写在代码,最后割裂
- 10.3 前端按钮判断和后端状态机没统一
- 10.4 没有状态日志
- 10.5 状态判断和权限判断、数据权限判断混在一起
- 十一、面试中怎么回答这个问题?
- 11.1 回答思路
- 11.2 面试官更想听到什么?
- 十二、总结
- 十三、后续可以继续展开的内容
- 十四、结尾
一、前言
很多业务系统一开始做状态判断,代码通常都长这样:
if(order.getStatus()==OrderStatus.WAIT_PAY){// 可以取消}elseif(order.getStatus()==OrderStatus.PAID){// 可以发货}elseif(order.getStatus()==OrderStatus.SHIPPED){// 可以确认收货}elseif(order.getStatus()==OrderStatus.CLOSED){// 什么都不能做}刚开始状态少的时候,这种写法还能忍。
但只要业务一复杂,很快就会出现这些问题:
- 订单状态越来越多
- 售后状态越来越多
- 商品状态、优惠券状态、活动状态都要判断
- 前端按钮判断一套,后端接口校验又一套
- 某个状态新增后,到处都要改 if else
- 状态流转规则没人能说清楚
很多人这时候会想到两个方向:
- 要不要上状态机?
- 状态机规则要不要放数据库?
再往下做一点,又会继续遇到:
- “发货”不只是改状态,还要调物流、写日志、发 MQ
- “取消订单”还要回补库存、关支付单、写操作记录
- “确认收货”还要发积分、更新结算状态
这就说明,状态流转真正要解决的,不只是“状态值怎么判断”,而是:
当前动作能不能做、做完状态变成什么、具体业务动作又该怎么执行。
这篇文章就结合电商系统真实场景,把状态机、处理器模式、状态日志表到底怎么配合,系统讲透。
二、业务场景
先假设这样一个典型的电商订单场景。
2.1 订单状态
订单状态包括:
WAIT_PAY:待付款PAID:已付款SHIPPED:已发货FINISHED:已完成CLOSED:已关闭REFUNDING:退款中
2.2 订单动作
订单支持这些动作:
- 支付
- 取消
- 发货
- 确认收货
- 申请退款
2.3 真实业务要求
这个场景下,通常需要满足:
- 不同状态下允许的动作不同
- 状态流转规则要统一
- 前端按钮显示和后端校验要一致
- 新增状态或新增动作时,不要改一堆 if else
- 每次状态变更都要留痕
- 订单动作不仅改状态,还要执行各自业务逻辑
这类问题在电商里非常普遍:
- 订单状态流转
- 售后状态流转
- 商品审核状态流转
- 优惠券状态流转
- 活动状态流转
三、问题现象
很多项目一开始不做统一设计,后面会出现这些典型问题。
3.1 状态判断散落在各层
比如:
- 前端页面里一套 if
- Controller 里一套 if
- Service 里一套 if
- 定时任务里又一套 if
- MQ 消费者里还有一套 if
最后会变成:
- 同一个状态规则到处重复
- 业务一改,改动点特别多
- 很容易漏改
3.2 状态流转规则说不清
比如你问:
- 已付款订单能不能取消?
- 已发货订单能不能退款?
- 退款中订单能不能再次发货?
如果团队回答依赖“看代码里哪里写了”,那说明规则根本没有统一收敛。
3.3 一个动作不只是改状态
比如“发货”这个动作,通常不只是:
PAID -> SHIPPED
还会伴随:
- 调物流接口
- 生成发货单
- 写订单操作日志
- 发送订单已发货消息
也就是说:
状态流转和业务执行,其实是两个不同层面的事情。
3.4 前后端判断不一致
比如前端判断:
PAID可以发货
但后端又加了条件:
- 只有
PAID且已分配仓库才能发货
结果就会出现:
- 前端显示了发货按钮
- 后端却拒绝执行
这种体验在后台系统里特别常见,也特别差。
四、原理分析
状态流转真正要解决的,不是“少写几个 if”,而是把规则和执行分开管理。
4.1 状态机解决什么?
状态机解决的是:
当前状态下,某个动作能不能做;如果能做,下一个状态是什么。
比如:
WAIT_PAY + PAY -> PAIDWAIT_PAY + CANCEL -> CLOSEDPAID + SHIP -> SHIPPEDSHIPPED + CONFIRM_RECEIVE -> FINISHED
所以状态机本质上是在管理:
- 状态
- 动作
- 流转结果
4.2 处理器模式解决什么?
处理器模式解决的是:
某个动作到底要执行哪些业务逻辑。
比如“发货”动作的处理器,可能要做:
- 校验仓库
- 调用物流系统
- 写发货记录
- 发 MQ 通知
而“取消订单”动作的处理器,可能要做:
- 校验取消条件
- 回补库存
- 关闭支付单
- 写取消日志
也就是说:
- 状态机管规则
- 处理器管执行
4.3 为什么这两个要组合使用?
因为业务里同时存在两类问题:
规则问题
- 当前状态允不允许这个动作
- 动作执行后状态变成什么
执行问题
- 这个动作具体要调哪些服务
- 要不要发消息
- 要不要写日志
如果把这两类问题都塞进 if else,代码一定会越来越乱。
4.4 状态机规则应该放数据库还是代码?
这是最关键的问题之一。
我先说结论:
对于电商里的核心业务状态流转,大多数情况下更建议放代码里,而不是直接放数据库。
原因很简单:
- 核心状态规则通常比较稳定
- 放代码里更清晰、可读、可调试
- 更适合和事务、处理器、领域逻辑一起收敛
- 有编译期约束,不容易被误改
而数据库更适合存的是:
- 当前状态
- 状态变更日志
- 某些可配置的外围规则
五、为什么大多数电商状态机更适合放代码里
这一点我建议你在设计里一定要先想清楚。
5.1 核心业务规则通常不应该交给配置随便改
比如下面这些规则:
- 待付款可以支付
- 已付款可以发货
- 已发货可以确认收货
- 已关闭不能再发货
这些其实是订单领域里的核心业务规则。
它们通常不会像运营配置那样天天变。
所以更适合:
- 枚举定义
- 规则表定义
- 代码里统一维护
5.2 放代码里更适合排查和调试
如果规则写在代码里:
- 一眼能看到所有状态流转
- 断点好打
- 改动有版本记录
- 问题定位更直接
如果规则全放数据库:
- 还要查配置
- 还要考虑缓存
- 还要防误配置
- 排障成本会高很多
5.3 动作执行逻辑本来就在代码里
就算你把状态流转规则放数据库,“发货”“取消”“退款”这些动作的真正执行逻辑还是要写代码。
比如:
- 发货要调物流
- 取消要回补库存
- 确认收货要发积分
所以很多时候你会发现:
流转规则在数据库,动作实现却在代码,最后会割裂得很严重。
5.4 什么时候才适合放数据库?
只有这些情况,我才建议考虑配置化:
- 审批流特别复杂
- 流程节点经常改
- 需要产品/运营可配置
- 需要流程可视化编排
这种更像:
- 工作流
- 审批流
- BPM
而不只是普通订单状态机。
六、最推荐的落地方式
如果你问我电商后台里最推荐的一套,我更建议这样做:
状态枚举 + 动作枚举 + 流转规则表 + 处理器模式 + 状态日志表
这套组合非常适合电商系统。
6.1 状态枚举
用于统一定义状态。
6.2 动作枚举
用于统一定义动作。
6.3 流转规则表
用于统一定义:
- 当前状态
- 当前动作
- 下一个状态
6.4 处理器模式
用于把每个动作的具体业务执行拆开。
6.5 状态日志表
用于记录每次状态变更,方便:
- 审计
- 追踪
- 排障
- 复盘
这才是一个完整闭环。
七、落地代码示例
下面给一版比较贴近实际项目思路的 Java 代码。
7.1 定义订单状态枚举
publicenumOrderStatus{WAIT_PAY,PAID,SHIPPED,FINISHED,CLOSED,REFUNDING}7.2 定义订单动作枚举
publicenumOrderAction{PAY,CANCEL,SHIP,CONFIRM_RECEIVE,APPLY_REFUND}7.3 定义状态机规则表
publicclassOrderStateMachine{privatestaticfinalMap<OrderStatus,Map<OrderAction,OrderStatus>>FLOW=newEnumMap<>(OrderStatus.class);static{Map<OrderAction,OrderStatus>waitPayMap=newEnumMap<>(OrderAction.class);waitPayMap.put(OrderAction.PAY,OrderStatus.PAID);waitPayMap.put(OrderAction.CANCEL,OrderStatus.CLOSED);Map<OrderAction,OrderStatus>paidMap=newEnumMap<>(OrderAction.class);paidMap.put(OrderAction.SHIP,OrderStatus.SHIPPED);paidMap.put(OrderAction.APPLY_REFUND,OrderStatus.REFUNDING);Map<OrderAction,OrderStatus>shippedMap=newEnumMap<>(OrderAction.class);shippedMap.put(OrderAction.CONFIRM_RECEIVE,OrderStatus.FINISHED);shippedMap.put(OrderAction.APPLY_REFUND,OrderStatus.REFUNDING);FLOW.put(OrderStatus.WAIT_PAY,waitPayMap);FLOW.put(OrderStatus.PAID,paidMap);FLOW.put(OrderStatus.SHIPPED,shippedMap);}publicstaticbooleancanDo(OrderStatuscurrentStatus,OrderActionaction){returnFLOW.containsKey(currentStatus)&&FLOW.get(currentStatus).containsKey(action);}publicstaticOrderStatusnextStatus(OrderStatuscurrentStatus,OrderActionaction){if(!canDo(currentStatus,action)){thrownewIllegalStateException("当前状态不允许执行该操作");}returnFLOW.get(currentStatus).get(action);}publicstaticSet<OrderAction>allowedActions(OrderStatuscurrentStatus){if(!FLOW.containsKey(currentStatus)){returnCollections.emptySet();}returnFLOW.get(currentStatus).keySet();}}这个类只做一件事:
- 判断某个动作是否允许
- 计算下一个状态
7.4 定义动作处理器接口
publicinterfaceOrderActionHandler{OrderActionaction();voidhandle(Orderorder);}7.5 发货处理器
@ComponentpublicclassShipOrderHandlerimplementsOrderActionHandler{@OverridepublicOrderActionaction(){returnOrderAction.SHIP;}@Overridepublicvoidhandle(Orderorder){// 1. 校验仓库// 2. 调物流接口// 3. 写发货单// 4. 发 MQ 通知}}7.6 取消订单处理器
@ComponentpublicclassCancelOrderHandlerimplementsOrderActionHandler{@OverridepublicOrderActionaction(){returnOrderAction.CANCEL;}@Overridepublicvoidhandle(Orderorder){// 1. 回补库存// 2. 关闭支付单// 3. 写取消记录}}7.7 统一执行入口
@ServicepublicclassOrderActionService{privatefinalMap<OrderAction,OrderActionHandler>handlerMap=newEnumMap<>(OrderAction.class);publicOrderActionService(List<OrderActionHandler>handlers){for(OrderActionHandlerhandler:handlers){handlerMap.put(handler.action(),handler);}}@Transactionalpublicvoidexecute(Orderorder,OrderActionaction){if(!OrderStateMachine.canDo(order.getStatus(),action)){thrownewRuntimeException("当前状态不允许执行该操作");}OrderActionHandlerhandler=handlerMap.get(action);if(handler==null){thrownewRuntimeException("未找到动作处理器");}OrderStatusfromStatus=order.getStatus();handler.handle(order);OrderStatusnextStatus=OrderStateMachine.nextStatus(fromStatus,action);order.setStatus(nextStatus);orderMapper.updateStatus(order.getId(),nextStatus);orderStatusLogService.record(order.getId(),fromStatus,nextStatus,action);}}这里你可以清楚看到:
- 状态机先判断是否合法
- 处理器执行具体业务动作
- 然后统一改状态
- 最后记录状态日志
这套结构比到处写 if else 清晰很多。
八、数据库里到底该存什么?
这部分特别关键。
8.1 当前状态要落数据库
比如order_info表里要有:
status
因为业务最终状态肯定要持久化。
8.2 状态变更日志也建议落数据库
这个非常值得做。
比如建一张:
order_status_log
字段建议:
| 字段 | 说明 |
|---|---|
id | 主键 |
order_id | 订单 ID |
from_status | 原状态 |
to_status | 新状态 |
action | 动作 |
operator_id | 操作人 |
operator_type | 操作人类型 |
remark | 备注 |
created_at | 创建时间 |
这张表的价值非常大:
- 查问题时知道订单怎么变过来的
- 审计操作轨迹
- 复盘异常流转
- 支持后台展示状态时间线
8.3 核心状态规则不建议直接放数据库
对于订单、售后、商品这些核心规则,我更建议:
- 规则写代码
- 结果落数据库
- 日志落数据库
而不是:
- 规则也放数据库
因为核心状态规则通常不该让它变成“随时可配”的。
九、什么时候适合把状态规则做成数据库配置?
这里也要说清楚,不是永远不能放数据库。
更适合数据库配置的场景通常是:
- 审批流节点经常变化
- 流程需要产品/运营配置
- 要支持可视化流程编排
- 某些外层规则变化频繁
比如:
- 商家入驻审核流
- 平台风控审核流
- 可配置的审批节点流程
这些更像“工作流”,不完全是普通状态机。
所以我的建议是:
放代码里的
- 订单状态机
- 售后状态机
- 商品状态机
- 优惠券状态机
放数据库里的
- 当前状态
- 状态变更日志
- 审批流配置
- 某些运营可调规则
也就是:
核心状态规则代码化,状态结果和状态日志数据库化。
十、为什么很多项目上了状态机,最后还是不好维护?
这也是线上项目很常见的情况。
10.1 只有状态机,没有动作处理器
结果还是把所有业务动作塞在一个大方法里,if else 只是从别处搬到了状态机类里。
10.2 规则写在数据库,执行逻辑写在代码,最后割裂
调试时特别痛苦,排查也很麻烦。
10.3 前端按钮判断和后端状态机没统一
后端已经有 allowedActions 了,前端还自己再写一套 if,这样很容易不一致。
10.4 没有状态日志
出了问题根本不知道状态是怎么流转过来的。
10.5 状态判断和权限判断、数据权限判断混在一起
比如“发货”这件事,实际上应该拆成:
- 有没有
order:ship权限 - 当前订单是不是当前仓库可操作数据
- 当前状态是不是允许发货
这三层不能混成一个 if。
十一、面试中怎么回答这个问题?
如果面试官问你:
电商系统里的状态机你会怎么设计?规则放数据库还是代码里?
你可以这样回答。
11.1 回答思路
第一,我会先区分“状态流转规则”和“动作执行逻辑”这两个层面。状态机主要负责定义当前状态下哪些动作允许执行、执行后流转到哪个状态;处理器模式则负责每个动作背后的具体业务逻辑,比如发货要调物流、取消订单要回补库存。
第二,对于订单、售后、商品这类核心业务状态机,我通常更建议把状态枚举、动作枚举和流转规则写在代码里,而不是直接放数据库。因为这些规则通常比较稳定,放代码里更清晰、可调试、可版本管理,也更适合和事务、领域逻辑放在一起维护。
第三,数据库里我会存两类东西:一类是业务当前状态,比如订单表里的status;另一类是状态变更日志,比如order_status_log,用来做审计、排障和状态时间线追踪。
第四,如果某些流程属于审批流、节点经常变化、需要运营可配置,那更适合做成数据库配置或者流程引擎,而不是和订单核心状态机混在一起。
11.2 面试官更想听到什么?
面试官真正想听的,通常不是一句“用状态机”,而是你有没有这些意识:
- 你知道状态机管规则,处理器管执行
- 你知道核心状态规则更适合代码化
- 你知道数据库更适合存状态结果和状态日志
- 你知道前后端按钮判断最好统一用 allowedActions
- 你知道权限判断、数据权限判断、状态判断要分层
如果你能把这些点讲清楚,面试官会明显觉得你做过真实业务设计,而不只是知道状态机这个概念。
十二、总结
状态流转这件事,真正难的不是“少写几个 if else”,而是如何把:
- 状态规则
- 动作执行
- 持久化状态
- 状态日志
真正拆清楚、收拢起来。
如果只记一句结论,我觉得可以记住这句:
大多数电商核心状态流转场景下,更推荐“状态规则代码化 + 动作处理器化 + 状态结果数据库化 + 状态日志持久化”。
这套方案不一定最省代码,但通常是最清晰、最好维护、最接近真实线上系统的一种设计。
十三、后续可以继续展开的内容
如果这篇你觉得还可以,后面这个系列我还可以继续写:
- 电商后台的订单状态机怎么设计
- 售后状态机怎么设计
- 状态日志表怎么和审计日志结合
- 前端按钮怎么和后端 allowedActions 统一
- 审批流什么时候该用状态机,什么时候该上流程引擎
十四、结尾
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注。
后面我会继续整理一些更偏实战的 Java 后端和电商系统设计文章。
