这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice
状态机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):订单当前所处阶段,如
INIT、PAY_WAIT、PAYING、PAY_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);
// ...
}
}
五、收益:解决了什么问题
| 问题 | 状态机方案 |
|---|---|
| 状态跃迁不可控 | 每个事件明确定义前置状态,非法转换直接报错 |
| 难以追溯 | 事件名即业务动作,日志一看就知道发生了什么 |
| 并发隐患 | 乐观锁保证同一时刻只有一个线程能成功更新 |
| 维护噩梦 | 加新状态只需在枚举里加一行,所有转换规则一目了然 |
六、任何有状态流转的场景都可以复用本状态机方案
状态机不是银弹,但它让状态流转变得:可控、可追溯、可维护。
核心思想:
- 事件驱动:每个事件有明确的
preState和targetState - 前置校验:转换前断言当前状态是否合法
- 乐观锁更新:WHERE 条件带上前置状态,防止并发覆盖
- 失败告警:更新失败必有异常,必须告警
任何有状态流转的场景都可以复用本状态机方案
| 场景 | 状态示例 |
|---|---|
| 退款订单 | INIT → REFUNDING → REFUND_SUCCESS/REFUND_FAIL |
| 任务订单 | DRAFT → PUBLISHED → ACCEPTED → COMPLETED |
| 审批流程 | PENDING → APPROVED/REJECTED |
再来总结一下复用步骤:
- 定义状态枚举,实现
SMState - 定义状态机枚举,实现
StateMachine<StateEnum> - 业务Manager统一一个更新状态的方法,并将状态机作为入参。(依赖
StateMachineDbUpdater.updateStateById)
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/22224710
浙公网安备 33010602011771号