在现代电子商务系统中,订单的状态管理是一个非常重要的环节。订单从创建到最终完成或取消,通常会经历多个状态的转换。如何高效地管理这些状态流转,并在系统中灵活地扩展状态和行为,是我们在开发中需要解决的问题。
本文将详细介绍如何在Spring Boot项目中使用
Spring Statemachine
框架来实现订单状态流转控制。
Spring Statemachine
是由Spring团队提供的一个轻量级状态机框架。它为开发者提供了一种简便且强大的方式来管理复杂的状态流转逻辑,尤其适用于订单处理、工作流引擎等需要状态管理的场景。
Spring Statemachine
具有以下特点:
-
灵活的状态配置:
通过Java配置或外部配置文件定义状态和状态转换。
-
-
与Spring生态系统的良好集成:
易于与Spring Boot、Spring Security等集成。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
在一个典型的订单处理流程中,订单可能会经历以下几个状态:
-
-
-
已发货 (SHIPPED):
订单已经发货,等待收货。
-
已完成 (COMPLETED):
用户确认收货,订单完成。
-
已取消 (CANCELLED):
订单在任意状态下都可能被取消。
这些状态之间可能存在以下转换关系:
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
在
Spring Statemachine
中,状态和状态转换(Transitions)通过配置来定义。每个状态转换都由事件(Events)触发,从而使状态从一个状态流转到另一个状态。
首先,在Spring Boot项目中引入
Spring Statemachine
的依赖。
<dependency>
<groupId>org.springframework.statemachinegroupId>
<artifactId
>spring-statemachine-starterartifactId>
dependency>
接下来,我们定义订单状态和事件。
public enum OrderStates {
NEW, PAID, SHIPPED, COMPLETED, CANCELLED
}
public enum OrderEvents {
PAY, SHIP, COMPLETE, CANCEL
}
使用
StateMachineConfigurerAdapter
来配置状态机,包括状态、事件、监听器和持久化及其转换关系。
@Configuration
@EnableStateMachine
public class StateMachineConfig extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
private static final Logger log = LoggerFactory.getLogger(StateMachineConfig.class);
@Override
public void configure(StateMachineConfigurationConfigurer config)
throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(listener());
}
@Override
public void configure(StateMachineStateConfigurer states)
throws Exception {
states
.withStates()
.initial(OrderStates.NEW)
.end(OrderStates.COMPLETED)
.end(OrderStates.CANCELLED)
.states(EnumSet.allOf(OrderStates.class));
}
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.NEW).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
.withExternal()
.source(OrderStates.PAID).target(OrderStates.SHIPPED).event(OrderEvents.SHIP)
.and()
.withExternal()
.source(OrderStates.SHIPPED).target(OrderStates.COMPLETED).event(OrderEvents.COMPLETE)
.and()
.withExternal()
.source(OrderStates.NEW).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL)
.and()
.withExternal()
.source(OrderStates.PAID).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL);
}
@Bean
public StateMachineListener listener() {
return new StateMachineListenerAdapter<>() {
@Override
public void stateChanged(State from, State to) {
log.info("State change to: {}", to.getId());
}
@Override
public void stateMachineError(StateMachine stateMachine, Exception exception) {
log.error("Exception caught: {}", exception.getMessage(), exception);
}
@Override
public void eventNotAccepted(Message message) {
Order order = (Order) message.getHeaders().get("order");
log.error("Order state machine can't change state {} --> {}", Objects.requireNonNull(order).getStatus(), message.getPayload());
}
};
}
@Bean
public StateMachinePersist inMemoryStateMachinePersist() {
return new StateMachinePersist<>() {
private final Map> contexts = new HashMap<>();
@Override
public void write(StateMachineContext context, String contextObj) {
contexts.put(contextObj, context);
}
@Override
public StateMachineContext read(String contextObj) {
return contexts.get(contextObj);
}
};
}
}
使用
@WithStateMachine
来配置状态机流转后的后续逻辑,比如更新订单状态、发邮件等。
@Service
@WithStateMachine
public class OrderStateChangeHandler {
private static final Logger log = LoggerFactory.getLogger(OrderStateChangeHandler.class);
@OnTransition(source = "NEW", target = "PAID")
public void payTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
log.info("Handle Pay Order:{}", order);
// 其他业务 如保存订单状态
Objects.requireNonNull(order).setStatus(OrderStates.PAID);
//orderRepository.save(order);
}
@OnTransition(source = "PAID", target = "SHIPPED")
public void shipTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
log.info("Handle Ship Order:{}", order);
// 其他业务 如更新订单
Objects.requireNonNull(order).setStatus(OrderStates.SHIPPED);
// orderMapper.updateById(order);
}
@OnTransition(source = "SHIPPED", target = "COMPLETED")
public void completeTransition(Message message) {
Order order = (Order) message.getHeaders().get("order");
log.info("Handle Complete Order:{}", order);
// 其他业务 如更新订单
Objects.requireNonNull(order).setStatus(OrderStates.COMPLETED);
// orderMapper.updateById(order);
}
}
配置好状态机后,我们可以在业务逻辑中使用它来控制订单的状态流转。
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
private final StateMachine stateMachine;
public OrderService(StateMachine stateMachine) {
this.stateMachine = stateMachine;
}
public void payOrder(Integer id) {
log.info("Pay Order: {}", id);
Order order = new Order("12345", "Sample Order", OrderStates.NEW, new BigDecimal("99.99"));
// 模拟支付
// payService.payOrder(1);
Message message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build();
stateMachine.sendEvent(Mono.just(message)).subscribe();
}
public void shipOrder(Integer id) {
log.info("Ship Order: {}", id);
Order order = new Order("12345", "Sample Order", OrderStates.PAID, new BigDecimal("99.99"));
// 模拟发货
// tradeService.shipOrder(1);
Message message = MessageBuilder.withPayload(OrderEvents.SHIP).setHeader("order", order).build();
stateMachine.sendEvent(Mono.just(message)).subscribe();
}
public void completeOrder(Integer id) {
log.info("Complete Order: {}", id);
Order order = new Order("12345", "Sample Order", OrderStates.SHIPPED, new BigDecimal("99.99"));
Message message = MessageBuilder.withPayload(OrderEvents.COMPLETE).setHeader("order", order).build();
stateMachine.sendEvent(Mono.just(message)).subscribe();
}
}
在控制层中我们可以根据不同的请求来触发状态转换:
@GetMapping("/order/pay/{id}")
public String payOrder(@PathVariable("id") Integer id) {
orderService.payOrder(id);
return "Order paid ";
}
@GetMapping("/order/ship/{id}")
public String shipOrder(@PathVariable("id") Integer id) {
orderService.shipOrder(id);
return "Order shipped ";
}
@GetMapping("/order/complete/{id}")
public String completeOrder(@PathVariable("id") Integer id) {
orderService.completeOrder(id);
return "Order completed ";
}
Spring Statemachine
的核心是有限状态机模型,它使用状态、事件、和转换来控制业务流程。状态机会在不同的状态之间进行转换,每次转换都会触发相关的操作。状态机还支持嵌套状态、并发状态等复杂场景。
-
-
-
转换(Transition):
状态之间的变化,通常由事件驱动。
Spring Statemachine
通过定义状态机的配置,允许开发者灵活地管理状态和转换逻辑。
当我们按照上面的步骤,配置好了状态机之后,接下来我们测试一下,配置的状态机是否能达到预期的目的。
因为我们配置的订单流转规则中,
NEW
只能转换到
PAID
或
CANCELLED
,所以我们期望的是订单不能流转成功。