电商系统里的状态机要放数据库还是代码里一次讲清状态机、处理器模式与状态日志表设计大家好我是一名有 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 状态判断散落在各层比如前端页面里一套 ifController 里一套 ifService 里一套 if定时任务里又一套 ifMQ 消费者里还有一套 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{privatestaticfinalMapOrderStatus,MapOrderAction,OrderStatusFLOWnewEnumMap(OrderStatus.class);static{MapOrderAction,OrderStatuswaitPayMapnewEnumMap(OrderAction.class);waitPayMap.put(OrderAction.PAY,OrderStatus.PAID);waitPayMap.put(OrderAction.CANCEL,OrderStatus.CLOSED);MapOrderAction,OrderStatuspaidMapnewEnumMap(OrderAction.class);paidMap.put(OrderAction.SHIP,OrderStatus.SHIPPED);paidMap.put(OrderAction.APPLY_REFUND,OrderStatus.REFUNDING);MapOrderAction,OrderStatusshippedMapnewEnumMap(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);}publicstaticSetOrderActionallowedActions(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{privatefinalMapOrderAction,OrderActionHandlerhandlerMapnewEnumMap(OrderAction.class);publicOrderActionService(ListOrderActionHandlerhandlers){for(OrderActionHandlerhandler:handlers){handlerMap.put(handler.action(),handler);}}Transactionalpublicvoidexecute(Orderorder,OrderActionaction){if(!OrderStateMachine.canDo(order.getStatus(),action)){thrownewRuntimeException(当前状态不允许执行该操作);}OrderActionHandlerhandlerhandlerMap.get(action);if(handlernull){thrownewRuntimeException(未找到动作处理器);}OrderStatusfromStatusorder.getStatus();handler.handle(order);OrderStatusnextStatusOrderStateMachine.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订单 IDfrom_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 后端和电商系统设计文章。