惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
月光博客
月光博客
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
美团技术团队
Y
Y Combinator Blog
T
Tailwind CSS Blog
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
B
Blog RSS Feed
V
Visual Studio Blog
MyScale Blog
MyScale Blog

博客园 - buguge

邮件正文不该进log日志:通过【lombok注解语法糖】实现从 整块屏蔽 到 安全预览 交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 还在用ArrayList?用HashSet吧!--性能对比
这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这...
buguge · 2026-08-04 · via 博客园 - buguge

状态机good-practice:让订单状态流转可控、可追溯、可维护

作者:buguge
2026-04-07


一、问题背景:状态流转的"野路子"

在我们的企业应用系统中,业务数据的状态流转是最常见的场景之一。以支付订单为例,状态可能经历:

初始 → 待付款 → 付款中 → 付款成功/付款失败

1.1 常见的"野路子"写法

// 方式一:直接硬编码
order.setStatus("PAY_SUCCESS");


// 方式二:用枚举,但“随便”赋值
order.setStatus(PaymentOrderStatusEnum.PAY_SUCCESS);


// 方式三:if-else 大杂烩
if (order.getStatus().equals("INIT")) {
    order.setStatus("PAY_WAIT");
} else if (order.getStatus().equals("PAY_WAIT")) {
    order.setStatus("PAYING");
}
// ... 继续套娃

1.2 这些写法的问题

问题 说明
状态跃迁不可控 哪儿都能改状态,INIT → PAY_SUCCESS 直接跳过中间态,业务逻辑断裂
难以追溯 状态怎么变的?哪儿改的(谁改的)?为什么改?——全靠日志“猜”
并发隐患 两个线程同时改状态,一个改成 PAYING,一个改成 PAY_SUCCESS,数据不一致bug
维护噩梦 加一个新状态,所有 if-else 都要改,漏改就会出 bug

二、状态机是什么?一句话解释

状态机 = 状态 + 事件 + 转换规则

  • 状态(State):订单当前所处阶段,如 INITPAY_WAITPAYINGPAY_SUCCESS
  • 事件(Event):触发状态转换的业务动作,如"风控通过"、"付款请求成功"
  • 转换规则:定义哪个事件能从哪个前置状态转换到哪个目标状态

核心约束:每个事件有明确的前置状态和目标状态,不能随意跃迁。


三、我们的最佳实践:基于状态机进行分层协作

1. ★★状态机(PaymentStateMachine 枚举)
   - 定义事件与状态转换规则(preState → targetState)
   - 前置状态校验(assertCurrentState)

2. ★★★持久层(PaymentOrderManager)提供统一的修改状态的 API:updateStateById
   - 该方法明确将状态机作为入参
   - 依赖 StateMachineDbUpdater 实现乐观锁更新

3. ★上层应用服务(PaymentTransService 等)
   - 约定禁止直接修改状态。改状态需调用 PaymentOrderManager#updateStateById
   - 必须指定具体的状态机事件,如:PaymentStateMachine.PAY_SUCCESS

四、核心代码解析

4.0 核心类清单

路径 职责
SMState sby-libs/.../sm/SMState.java (底层公共)可流转状态标记接口
StateMachine sby-libs/.../sm/StateMachine.java (底层公共)状态机接口
PaymentOrderStatusEnum monorepo/.../pay/PaymentOrderStatusEnum.java (应用层)支付订单状态枚举
PaymentStateMachine monorepo/.../pay/PaymentStateMachine.java (应用层)支付状态机
StateMachineDbUpdater sby-libs/.../sm/StateMachineDbUpdater.java (底层公共)持久化更新器
PaymentOrderManager monorepo/.../pay/manager/PaymentOrderManager.java (应用层)支付订单业务持久层
PaymentTransService monorepo/.../pay/service/PaymentTransService.java (应用层)支付订单业务编排service

4.1 (应用层)状态枚举:PaymentOrderStatusEnum

public enum PaymentOrderStatusEnum implements SMState {
    INIT("初始"),
    RISKING("风控中"),
    PAY_WAIT("待支付"),
    PAYING("付款中"),
    PAY_SUCCESS("付款成功"),
    PAY_FAIL("付款失败"),
    PAY_CANCEL("付款撤销");
}

设计点:

  • 实现 SMState 接口,标记这是一个"可流转的业务状态"
  • 状态即文档,一眼能看懂订单生命周期

4.2 (底层公共)状态机接口:StateMachine<State>

public interface StateMachine<State extends SMState> {
    
    /** 获取事件的前置状态 */
    State getPreState();
    
    /** 获取事件的目标状态 */
    State getTargetState();
    
    /** 断言当前状态是否允许转换 */
    default void assertCurrentState(State current) {
        if (!current.equals(getPreState())) {
            throw new IllegalStateException("当前状态不正确,请检查状态机配置");
        }
    }
}

设计点:

  • 泛型设计,可复用于任何业务场景
  • assertCurrentState 提供前置状态校验,状态跃迁前先断言

4.3 (应用层)支付状态机:PaymentStateMachine

@Getter
@AllArgsConstructor
public enum PaymentStateMachine implements StateMachine<PaymentOrderStatusEnum> {

    // 保存数据
    SAVE_DATA(null, PaymentOrderStatusEnum.INIT),
    SAVE_DATA_SCENE(null, PaymentOrderStatusEnum.PAY_WAIT),

    // 风控相关事件
    RISK_VERIFY_RETURN_SUCCESS(PaymentOrderStatusEnum.INIT, PaymentOrderStatusEnum.PAY_WAIT),
    RISK_VERIFY_RETURN_FAIL(PaymentOrderStatusEnum.INIT, PaymentOrderStatusEnum.PAY_FAIL),
    RISK_VERIFY_RETURN_ONGOING(PaymentOrderStatusEnum.INIT, PaymentOrderStatusEnum.RISKING),
    RISK_VERIFY_ASYNC_SUCCESS(PaymentOrderStatusEnum.RISKING, PaymentOrderStatusEnum.PAY_WAIT),
    RISK_VERIFY_ASYNC_FAIL(PaymentOrderStatusEnum.RISKING, PaymentOrderStatusEnum.PAY_FAIL),

    // 付款相关事件
    PAY_REQUEST_RETURN_NORMAL(PaymentOrderStatusEnum.PAY_WAIT, PaymentOrderStatusEnum.PAYING),
    PAY_REQUEST_RETURN_PAY_FAIL(PaymentOrderStatusEnum.PAY_WAIT, PaymentOrderStatusEnum.PAY_FAIL),
    PAY_SUCCESS(PaymentOrderStatusEnum.PAYING, PaymentOrderStatusEnum.PAY_SUCCESS),
    PAY_FAIL(PaymentOrderStatusEnum.PAYING, PaymentOrderStatusEnum.PAY_FAIL),
    PAY_CANCEL(PaymentOrderStatusEnum.PAYING, PaymentOrderStatusEnum.PAY_CANCEL),
    ;

    private final PaymentOrderStatusEnum preState;
    private final PaymentOrderStatusEnum targetState;
}

每个枚举值代表一个状态机事件。从这个枚举类可以清晰看到下面的 状态流转图:

                    ┌─────────┐
                    │  INIT   │
                    └────┬────┘
                         │
        ┌────────────────┼────────────────┐
        │                │                │
        ▼                ▼                ▼
   ┌─────────┐     ┌─────────┐     ┌─────────┐
   │ PAY_WAIT│     │ RISKING │     │ PAY_FAIL│
   └────┬────┘     └─────┬───┘     └─────────┘
        │                │
        │         ┌──────┴──────┐
        │         │             │
        │         ▼             ▼
        │    ┌─────────┐   ┌─────────┐
        │    │ PAY_WAIT│   │ PAY_FAIL│
        │    └─────────┘   └─────────┘
        │
        ▼
   ┌─────────┐
   │  PAYING │
   └────┬────┘
        │
   ┌────┴────┬───────────────┐
   │         │               │
   ▼         ▼               ▼
┌─────────┐┌────────────┐┌──────────┐
│PAY_FAIL ││PAY_SUCCUES ││PAY_CANCEL│
└─────────┘└────────────┘└──────────┘

设计点:

  • 每个事件名直接体现业务含义:RISK_VERIFY_RETURN_SUCCESS = "风控校验返回成功"
  • preState = null 表示允许从任意状态转换(用于初始创建)
  • 一眼能看出所有合法的状态转换路径

4.4 (底层公共)持久化更新器:StateMachineDbUpdater

public static <Entity, StateEnum extends SMState> int updateStateById(
        Entity entity, BaseMapper<Entity> mapper,
        StateMachine<StateEnum> stateMachine, 
        SFunction<Entity, StateEnum> stateField,
        SFunction<Entity, ?>... otherUpdateFields) {
    
    // 1. 设置目标状态
    setTargetState(entity, stateMachine, stateField);
    
    // 2. 构建更新条件(乐观锁)
    UpdateWrapper<Entity> where = new UpdateWrapper<>();
    where.eq(pkColumn, pkValue)           // 主键
          .eq(stateField, stateMachine.getPreState())  // 前置状态作为条件
          .set(stateField, stateMachine.getTargetState());  // 目标状态
    
    // 3. 设置其他业务字段
    applyFieldValuesToUpdateWrapper(entity, where, otherUpdateFields);
    
    // 4. 执行更新
    return mapper.update(null, where);
}

核心机制:乐观锁(CAS 思想)

UPDATE trans_payment_order 
SET status = 'PAY_SUCCESS', update_time = NOW()
WHERE id = 12345 
  AND status = 'PAYING';  -- 关键:前置状态作为条件
  • 如果 status 已经被其他线程改成 PAY_FAIL,WHERE 条件不匹配,updateCount = 0
  • 天然防止并发覆盖

4.5 (应用层)业务持久层:PaymentOrderManager.updateStateById

public void updateStateById(
        PaymentStateMachine event, 
        PaymentOrder paymentOrder, 
        CodeMsgVO<String> codeMsgVO,
        SFunction<PaymentOrder, ?>... updateFields) {
    
    // 1. 前置状态断言
    event.assertCurrentState(paymentOrder.getStatus());
    
    // 2. 设置目标状态和业务字段
    paymentOrder.setStatus(event.getTargetState());
    if (codeMsgVO != null) {
        paymentOrder.setResultCode(codeMsgVO.getCode());
        paymentOrder.setResultMsg(codeMsgVO.getMsg());
    }
    
    // 3. 调用底层公共能力(利用乐观锁实现持久化更新)
    int updateCount = StateMachineDbUpdater.updateStateById(
        paymentOrder, paymentOrderMapper, event, 
        PaymentOrder::getStatus, updateFields);
    
    // 4. 更新失败处理
    if (updateCount != 1) {
        // 发送告警 + 抛异常
        WechatMessageSend.sendWechatWarningMsg(...);
        throw BizException.build("订单状态变更失败");
    }
}

4.5 (应用层)上层PaymentTransService

@Service
public class PaymentTransService {
    
    public void handlePaymentNotify(PaymentNotifyVO paymentNotifyVO) {
        // ...
        if (!"S".equals(paymentNotifyVO.getPayState)) {
            log.warn("支付结果通知,非成功态,中止处理");
            return;
        }
        
        PaymentOrder paymentOrder = paymentOrderManager.selectByOrderId(paymentNotifyVO.getOrderId());
        // ....
        PaymentStateMachine event = PaymentStateMachine.PAY_SUCCESS;
        // 前置状态断言(可选,下层Manager里也有这个断言)
        event.assertCurrentState(paymentOrder.getStatus());
        
        paymentOrder.setPaymentFinishTime(paymentNotifyVO.getPaymentFinishTime());
        
        // 付款成功,持久化更新
        paymentOrderManager.updateStateById(
                event,    // 事件
                paymentOrder,                        // 订单实体
                CodeMsgVO.build("200", "付款成功"),   // 结果码
                PaymentOrder::getPaymentFinishTime   // 额外更新字段
        );
        
        // 订单完成的处理
        paySuccessMqProducer.produce(paymentOrder.getOrderId);
        // ...
    }
}


五、收益:解决了什么问题

问题 状态机方案
状态跃迁不可控 每个事件明确定义前置状态,非法转换直接报错
难以追溯 事件名即业务动作,日志一看就知道发生了什么
并发隐患 乐观锁保证同一时刻只有一个线程能成功更新
维护噩梦 加新状态只需在枚举里加一行,所有转换规则一目了然

六、任何有状态流转的场景都可以复用本状态机方案

状态机不是银弹,但它让状态流转变得:可控、可追溯、可维护。

核心思想:

  1. 事件驱动:每个事件有明确的 preStatetargetState
  2. 前置校验:转换前断言当前状态是否合法
  3. 乐观锁更新:WHERE 条件带上前置状态,防止并发覆盖
  4. 失败告警:更新失败必有异常,必须告警

任何有状态流转的场景都可以复用本状态机方案

场景 状态示例
退款订单 INIT → REFUNDING → REFUND_SUCCESS/REFUND_FAIL
任务订单 DRAFT → PUBLISHED → ACCEPTED → COMPLETED
审批流程 PENDING → APPROVED/REJECTED

再来总结一下复用步骤

  1. 定义状态枚举,实现 SMState
  2. 定义状态机枚举,实现 StateMachine<StateEnum>
  3. 业务Manager统一一个更新状态的方法,并将状态机作为入参。(依赖StateMachineDbUpdater.updateStateById