基于 Spring 狀態機實現電商訂單狀態流轉管理
作者:一安
傳統的if-else分支判斷在狀態較多、轉移邏輯復雜時會變得難以維護,而Spring狀態機結合狀態模式可以很好地解決這個問題,讓狀態轉移邏輯更加清晰、可擴展。
引言
在電商系統中,訂單狀態的流轉是核心業務流程之一。從用戶下單、支付、商家發貨到用戶收貨、售后等環節,每個環節對應不同的訂單狀態,且狀態之間的轉移需要滿足嚴格的業務規則。
傳統的if-else分支判斷在狀態較多、轉移邏輯復雜時會變得難以維護,而Spring狀態機結合狀態模式可以很好地解決這個問題,讓狀態轉移邏輯更加清晰、可擴展。
實現
圖片
圖片
案例代碼
訂單狀態枚舉
定義訂單的所有狀態,每個狀態對應業務中的一個環節:
@WithStateMachine
public enum OrderStatus {
WAIT_PAY("待支付"),
WAIT_DELIVER("待發貨"),
WAIT_RECEIVE("待收貨"),
COMPLETED("已完成"),
CLOSED("已關閉"),
AFTER_SALE("售后中");
@Getter
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
}訂單事件枚舉
事件是觸發狀態轉移的動作,每個事件對應一次狀態變更的觸發條件:
public enum OrderEvent {
PAY_SUCCESS("支付成功"),
DELIVER("倉庫發貨"),
RECEIVE("確認收貨"),
CANCEL("用戶取消/超時"),
APPLY_REFUND("審核退款"),
APPLY_AFTER_SALE("申請售后");
@Getter
private final String desc;
OrderEvent(String desc) {
this.desc = desc;
}
}狀態機配置
配置狀態機的狀態、事件、轉移邏輯及監聽器:
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatus, OrderEvent> {
/**
* 配置狀態機的“狀態集合”和“初始狀態”
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states) throws Exception {
states.withStates()
.initial(OrderStatus.WAIT_PAY) // 初始狀態為“待支付”
.states(EnumSet.allOf(OrderStatus.class)); // 注冊所有狀態
}
/**
* 配置狀態機的“轉移規則”(事件觸發狀態變更)
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) throws Exception {
transitions
// 待支付 → 待發貨:支付成功
.withExternal()
.source(OrderStatus.WAIT_PAY).target(OrderStatus.WAIT_DELIVER)
.event(OrderEvent.PAY_SUCCESS)
.and()
// 待支付 → 已關閉:用戶取消/超時
.withExternal()
.source(OrderStatus.WAIT_PAY).target(OrderStatus.CLOSED)
.event(OrderEvent.CANCEL)
.and()
// 待發貨 → 待收貨:倉庫發貨
.withExternal()
.source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE)
.event(OrderEvent.DELIVER)
.and()
// 待發貨 → 已關閉:審核退款
.withExternal()
.source(OrderStatus.WAIT_DELIVER).target(OrderStatus.CLOSED)
.event(OrderEvent.APPLY_REFUND)
.and()
// 待收貨 → 已完成:確認收貨
.withExternal()
.source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.COMPLETED)
.event(OrderEvent.RECEIVE)
.and()
// 已完成 → 售后中:申請售后
.withExternal()
.source(OrderStatus.COMPLETED).target(OrderStatus.AFTER_SALE)
.event(OrderEvent.APPLY_AFTER_SALE);
}
/**
* 配置狀態機的“全局配置”(如監聽器)
*/
@Override
public void configure(StateMachineConfigurationConfigurer<OrderStatus, OrderEvent> config) throws Exception {
config.withConfiguration()
.listener(new OrderStateMachineListener()); // 注冊狀態變更監聽器
}
}@Slf4j
@Component
public class OrderStateMachineListener extends StateMachineListenerAdapter<OrderStatus, OrderEvent> {
@Override
public void stateChanged(State<OrderStatus, OrderEvent> from, State<OrderStatus, OrderEvent> to) {
if (from != null) {
log.info("訂單狀態從: " + from.getId().getDesc() + " 變更為: " + to.getId().getDesc());
} else {
log.info("訂單初始狀態: " + to.getId().getDesc());
}
}
}業務邏輯層實現
封裝狀態機觸發邏輯與數據庫操作,保證事務一致性:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StateMachine<OrderStatus, OrderEvent> stateMachine;
@Autowired
private StateMachinePersister<OrderStatus, OrderEvent, Order> stateMachineMemPersister;
/**
* 創建訂單(初始狀態為待支付)
*/
@Transactional
public Order createOrder(Order order) {
order.setStatus(OrderStatus.WAIT_PAY);
orderMapper.insert(order);
return order;
}
/**
* 觸發狀態事件,更新訂單狀態
*/
@Transactional
public boolean triggerEvent(Long orderId, OrderEvent event) {
// 查詢訂單
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("訂單不存在:" + orderId);
}
boolean result = false;
try {
// 從訂單中恢復狀態機當前狀態
stateMachineMemPersister.restore(stateMachine, order);
// 發送事件觸發狀態變更
Message<OrderEvent> message = MessageBuilder.withPayload(event)
.setHeader("orderId", orderId)
.build();
result = stateMachine.sendEvent(message);
// 狀態轉移成功,更新數據庫中的訂單狀態
if (result) {
// 持久化狀態機最新狀態
stateMachineMemPersister.persist(stateMachine, order);
// 更新訂單實體狀態
order.setStatus(stateMachine.getState().getId());
orderMapper.updateById(order);
}
} catch (Exception e) {
throw new RuntimeException("狀態變更失敗:" + e.getMessage(), e);
}
return result;
}
/**
* 查詢訂單(用于測試)
*/
public Order getOrder(Long orderId) {
return orderMapper.selectById(orderId);
}
}接口層實現
對外暴露HTTP接口,用于創建訂單和觸發狀態事件:
@CrossOrigin
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderService orderService;
/**
* 創建訂單
*/
@PostMapping("/create")
public Order createOrder(
@RequestParam String userId,
@RequestParam String productId,
@RequestParam BigDecimal amount) {
Order order = new Order();
order.setId(userId);
order.setProductName(productId);
order.setAmount(amount);
order.setStatus(OrderStatus.WAIT_PAY);
return orderService.createOrder(order);
}
/**
* 觸發訂單狀態事件
*/
@PostMapping("/event/{orderId}/{event}")
public boolean triggerEvent(
@PathVariable Long orderId,
@PathVariable String event) {
OrderEvent orderEvent = OrderEvent.valueOf(event);
return orderService.triggerEvent(orderId, orderEvent);
}
@GetMapping("/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
Order order = orderService.getOrder(orderId);
if (order == null) {
throw new RuntimeException("訂單不存在");
}
return order;
}
}持久化(可選)
@Slf4j
@Configuration
public class StateMachinePersistConfig {
/**
* 內存持久化(基于HashMap)
* 適合單體應用,服務重啟后狀態會丟失
*/
@Bean(name = "stateMachineMemPersister")
public StateMachinePersister<OrderStatus, OrderEvent, Order> stateMachineMemPersister() {
// 實現StateMachinePersist接口,定義狀態讀寫邏輯
StateMachinePersist<OrderStatus, OrderEvent, Order> persist = new StateMachinePersist<OrderStatus, OrderEvent, Order>() {
// 用HashMap存儲訂單ID與狀態機上下文的映射
private final Map<String, StateMachineContext<OrderStatus, OrderEvent>> stateMap = new HashMap<>();
@Override
public void write(StateMachineContext<OrderStatus, OrderEvent> context, Order order) throws Exception {
log.info("內存持久化狀態機 - 寫入,訂單ID: {}, 狀態上下文: {}", order.getId(), JSON.toJSONString(context));
stateMap.put(order.getId(), context);
}
@Override
public StateMachineContext<OrderStatus, OrderEvent> read(Order order) throws Exception {
StateMachineContext<OrderStatus, OrderEvent> context = stateMap.get(order.getId());
log.info("內存持久化狀態機 - 讀取,訂單ID: {}, 狀態上下文: {}", order.getId(), JSON.toJSONString(context));
return context;
}
};
// 使用Spring提供的DefaultStateMachinePersister包裝
return new DefaultStateMachinePersister<>(persist);
}
}Redis持久化配置案例:
@Configuration
public class StateMachinePersistConfig {
/**
* Redis持久化(分布式系統適用)
* 狀態機上下文存儲在Redis中,支持多實例共享狀態
*/
@Bean(name = "stateMachineRedisPersister")
public StateMachinePersister<OrderStatus, OrderEvent, Long> stateMachineRedisPersister(
RedisConnectionFactory redisConnectionFactory) {
// 創建Redis狀態機上下文倉庫
RedisStateMachineContextRepository<OrderStatus, OrderEvent> repository =
new RedisStateMachineContextRepository<>(redisConnectionFactory);
// 基于倉庫實現持久化邏輯
RepositoryStateMachinePersist<OrderStatus, OrderEvent, Long> persist =
new RepositoryStateMachinePersist<>(repository);
// 包裝為RedisStateMachinePersister
return new RedisStateMachinePersister<>(persist);
}
}注意事項
- 狀態機上下文結構:
Spring State Machine的StateMachineContext包含當前狀態、歷史狀態、擴展變量等信息,持久化時會完整存儲這些內容,確保狀態恢復的準確性。 Redis鍵設計:Redis持久化默認鍵格式為STATE_MACHINE_CONTEXT:{orderId},可通過自定義RedisStateMachineContextRepository修改鍵前綴,避免與其他業務鍵沖突。- 過期策略:對于
Redis持久化,可設置鍵過期時間(如訂單完成后24小時),避免無效數據占用內存。 - 分布式鎖:在
Redis持久化的分布式場景中,建議為triggerEvent方法添加分布式鎖(如Redisson),防止并發狀態修改導致的數據不一致。
責任編輯:武曉燕
來源:
一安未來





























