订单状态应使用带行为的Java枚举实现状态机,避免硬编码和if-else;每个状态自行定义合法操作,新增状态只需扩展枚举项;订单通过语义化方法(如pay())驱动状态转移,数据库存稳定字符串标识符。

订单状态变更为什么总出bug?
因为硬编码状态值 + if-else 判断,导致新增状态要改七八个地方,测试漏掉一个分支就引发支付成功但库存没扣、发货后还能取消等线上事故。OrderStatus 不能是 String 或 int,必须是可枚举、可扩展、自带行为的对象。
用Java枚举实现带行为的状态机
Java 枚举天然支持字段、构造、方法,比抽象类+子类更轻量,且线程安全、不可变。关键不是“定义状态”,而是让每个状态自己决定“能做什么”“不能做什么”。
public enum OrderStatus {
CREATED("已创建") {
@Override
public OrderStatus cancel() { return CANCELLED; }
@Override
public OrderStatus pay() { return PAID; }
},
PAID("已支付") {
@Override
public OrderStatus ship() { return SHIPPED; }
@Override
public OrderStatus refund() { return REFUNDED; }
},
SHIPPED("已发货") {
@Override
public OrderStatus complete() { return COMPLETED; }
},
CANCELLED("已取消"),
REFUNDED("已退款"),
COMPLETED("已完成");
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
// 默认抛异常,只在允许转移的状态里重写
public OrderStatus cancel() { throw new IllegalStateException("不可取消: " + this); }
public OrderStatus pay() { throw new IllegalStateException("不可支付: " + this); }
public OrderStatus ship() { throw new IllegalStateException("不可发货: " + this); }
public OrderStatus complete() { throw new IllegalStateException("不可完成: " + this); }
public OrderStatus refund() { throw new IllegalStateException("不可退款: " + this); }
public String getDesc() { return desc; }
}
-
CREATED只重写cancel()和pay(),其他调用直接抛IllegalStateException - 状态转移逻辑内聚在枚举项内部,新增状态只需加枚举常量 + 重写对应方法,不碰已有代码
- 避免用
switch(status)分支判断——那又退回到硬编码老路
订单实体如何安全触发状态变更
订单对象不能暴露 setStatus() 方法,否则外部可随意设成任意状态。必须通过明确语义的动作方法(如 pay())驱动,由动作委托给当前状态执行校验和转移。
public class Order {
private Long id;
private OrderStatus status = OrderStatus.CREATED;
private BigDecimal amount;
public void pay() {
this.status = this.status.pay(); // 转移由状态自己控制
// 这里可追加:发MQ、更新库存、记录日志...
}
public void ship() {
this.status = this.status.ship();
// 发货逻辑...
}
public OrderStatus getStatus() {
return status;
}
}
- 调用
order.pay()时,如果当前是CANCELLED,会直接在OrderStatus.CANCELLED.pay()抛异常,不走后续逻辑 - 所有业务副作用(如扣库存、发消息)必须放在状态变更之后,避免状态已变但下游失败导致不一致
- 不要在枚举方法里做耗时操作(如远程调用),状态对象应保持纯内存、无副作用
数据库怎么存枚举状态才不翻车
别存中文描述或序号(ordinal()),一改枚举顺序全崩。用稳定字符串标识符,比如数据库字段类型为 VARCHAR(20),值为 "CREATED"、"PAID"。
立即学习“Java免费学习笔记(深入)”;
- JPA 用户:用
@Enumerated(EnumType.STRING),确保映射到枚举名而非序号 - MyBatis 用户:配置
TypeHandler将OrderStatus自动转成/从字符串读取 - SQL 查询时,WHERE 条件写
status = 'PAID',而不是status = 1—— 后者耦合了枚举顺序 - 上线前检查历史数据是否包含未知字符串值,防止反序列化失败










