当前位置: 首页 > news >正文

别再写一堆 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 -> PAID
  • WAIT_PAY + CANCEL -> CLOSED
  • PAID + SHIP -> SHIPPED
  • SHIPPED + 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 后端和电商系统设计文章。

http://www.cnnetsun.cn/news/1861341.html

相关文章:

  • 如何在3分钟内让Mac通过Android手机获得有线网络连接:HoRNDIS终极指南
  • 自然语言生成代码(使用千问AI助手)
  • 从自动驾驶到AR建模:PointTransformer实战,在Open3D和PyTorch中快速搭建你的第一个点云分类模型
  • 【2026年最新600套毕设项目分享】基于微信小程序的网上商城(30045)
  • 通达信缠论分析插件完整教程:三分钟掌握缠论技术分析终极指南
  • 如何快速掌握跨平台文献管理:WPS-Zotero完整使用指南
  • Nanbeige像素冒险聊天终端5分钟快速部署:复古游戏风AI对话一键搭建
  • 如何高效使用lilToon:打造惊艳卡通角色的Unity着色器完整指南
  • [特殊字符] mPLUG-Owl3-2B轻量部署案例:科研实验室私有图像数据集零外泄分析平台
  • HEIF Utility:解决Windows平台HEIF图片兼容性的完整指南
  • DAMOYOLO实战:实时手机检测-通用模型部署与效果展示
  • Blender3mfFormat:让Blender成为专业3D打印设计的得力助手
  • 高性能内存管理与算法优化框架:Performance-Fish实现400%游戏帧率提升的技术解析
  • AssetStudio完整指南:Unity资源提取终极解决方案
  • 数据恢复新境界:用智能算法修复损坏的视频文件
  • 如何深度移除Windows Defender:高级权限工具配置指南
  • FireRed-OCR Studio部署教程:阿里云ECS+GPU实例一键部署全流程
  • Stable Yogi Leather-Dress-Collection 融合SolidWorks概念:生成3D建模参考图
  • douyin-downloader:基于智能解析引擎的抖音视频批量下载技术实现与架构解析
  • 保姆级教程:SenseVoice语音识别镜像快速上手,10秒音频70ms识别
  • 如何实现40+平台直播自动录制?开源工具DouyinLiveRecorder给你答案
  • obs studio软件、直播、视频录制笔记
  • [特殊字符]HistoXGAN有没有人复现过这个[特殊字符]
  • Cosmos-Reason1-7B免配置环境:WebUI预置Supervisor服务管理脚本
  • 实测Qwen3字幕生成:上传MP3,1分钟输出带时间戳的SRT文件
  • Llama Factory问题解决:常见微调错误排查与优化指南
  • 前端技术思考
  • 微信聊天记录永久保存指南:如何用WeChatMsg一键导出并生成年度报告?
  • ESP32-S3 + TB6600驱动42步进电机:从基础接线到AccelStepper库平滑加减速实战
  • AutoGen Studio快速体验:开箱即用的AI智能体开发平台