引言
在复杂的应用程序设计中,尤其是那些涉及多个状态变迁和业务流程控制的场景,有限状态机(Finite State Machine, FSM
)是一种强大而有效的建模工具。Spring框架为此提供了Spring状态机(Spring State Machine
)这一组件,它允许开发者以一种声明式且结构清晰的方式来管理和控制对象的状态流转。
提起Spring状态机,可能有些小伙伴还比较陌生。当你听到状态机时,一定会联想到状态设计模式。确实,状态机是状态模式的一种实际运用,在工作流引擎、订单系统等领域有大量的应用。在介绍状态机之前,我们先来回顾一下状态模式,以便更好地理解Spring状态机的概念和应用。
状态模式
状态模式是一种行为设计模式,用于管理对象的状态以及状态之间的转换。在状态模式中,对象在不同的状态下表现出不同的行为,而状态的转换是由外部条件触发的。状态模式将每个状态封装成一个独立的类,并将状态转换的逻辑分散在这些状态类中,从而使得状态的管理和转换变得简单和灵活。
状态模式通常由以下几个要素组成:
-
上下文(Context):上下文是包含了状态的对象,它定义了当前的状态以及可以触发状态转换的接口。上下文对象在不同的状态下会调用相应状态对象的方法来执行具体的行为。
-
抽象状态(State):抽象状态是一个接口或抽象类,定义了状态对象的通用行为接口。具体的状态类需要实现这个接口,并根据不同的状态来实现具体的行为。
-
具体状态(Concrete State):具体状态是实现了抽象状态接口的具体类,它实现了在特定状态下对象的行为。每个具体状态类负责管理该状态下的行为和状态转换规则。
状态模式使得对象在不同状态下的行为更加清晰和可维护,同时也使得对象的状态转换逻辑更加灵活和可扩展。状态模式常见于需要对象根据外部条件改变行为的场景,例如订单状态(如待提交,待发货,已发货,已签收,已完结等状态)的管理、工作流引擎中的状态(例如提交,审核中,驳回,审核通过,审核失败等)管理。
我们以订单状态的流转为例:
- 首先我们定义一个订单抽象状态的接口
public interface OrderState {
void handlerOrder();
}
- 在定义具体的订单状态,以及对应的订单状态的行为
public class OrderSubmitState implements OrderState{
@Override
public void handlerOrder() {
System.out.println("订单已提交");
}
}
public class OrderOutboundState implements OrderState{
@Override
public void handlerOrder() {
System.out.println("订单已出库");
}
}
public class OrderSignedState implements OrderState{
@Override
public void handlerOrder() {
System.out.println("订单已签收");
}
}
- 在定义一个状态的上下文,用于维护当前状态对象,以及提供状态流转的方法
public class OrderContext {
private OrderState orderState;
public void setOrderState(OrderState orderState){
this.orderState = orderState;
}
public void handleOrder(){
orderState.handlerOrder();
}
}
- 编写具体业务,测试订单状态流转
public class OrderStateTest {
public static void main(String[] args) {
OrderSubmitState orderSubmitState = new OrderSubmitState();
OrderContext orderContext = new OrderContext();
orderContext.setOrderState(orderSubmitState);
orderContext.handleOrder();
OrderOutboundState orderOutboundState = new OrderOutboundState();
orderContext.setOrderState(orderOutboundState);
orderContext.handleOrder();
OrderSignedState orderSignedState = new OrderSignedState();
orderContext.setOrderState(orderSignedState);
orderContext.handleOrder();
}
}
执行结果如下:
使用状态模式中的状态类不仅能消除if-else逻辑校验,在一定程度上也增强了代码的可读性和可维护性。类似策略模式,但是状态机模式跟策略模式还有很大的区别的。
-
状态模式:
- 关注对象在不同状态下的行为和状态之间的转换。
- 通过封装每个状态为单独的类来实现状态切换,使得每个状态对象都能处理自己的行为。
- 状态之间的转换通常是通过条件判断或外部事件触发的。
-
策略模式:
- 关注对象在不同策略下的行为差异。
- 将不同的算法或策略封装成单独的类,使得它们可以互相替换,并且在运行时动态地选择不同的策略。
- 不涉及状态转换,而是更多地关注于执行特定行为时选择合适的策略。
虽然两种模式都涉及对象行为的管理,但它们的关注点和应用场景略有不同。
关于消除if-else的方案请参考:代码整洁之道(一)之优化if-else的8种方案
什么是状态机
状态机,顾名思义,是一种数学模型,它通过定义一系列有限的状态以及状态之间的转换规则来模拟现实世界或抽象系统的动态行为。每个状态代表系统可能存在的条件或阶段,而状态间的转换则是由特定的输入(即事件)触发的。例如,在电商应用中,订单状态可能会经历创建、支付、打包、发货、完成等多个状态,每个状态之间的转变都由对应的业务动作触发。
在状态机中,有以下几个基本概念:
-
状态(State):系统处于的特定状态,可以是任何抽象的状态,如有限状态机中的“开”、“关”状态,或是更具体的状态如“运行”、“暂停”、“停止”等。
-
事件(Event):导致状态转换发生的触发器或输入,例如用户的输入、外部事件等。事件触发状态之间的转换。
-
转移(Transition):描述状态之间的变化或转换,即从一个状态到另一个状态的过程。转移通常由特定的事件触发,触发特定的转移规则。
-
动作(Action):在状态转换发生时执行的动作或操作,可以是一些逻辑处理、计算、输出等。动作可以与状态转移相关联。
-
初始状态(Initial State):系统的初始状态,即系统启动时所处的状态。
-
终止状态(Final State):状态机执行完成后所达到的状态,表示整个状态机的结束。
状态机可以分为有限状态机(Finite State Machine,FSM
)和无限状态机(Infinite State Machine
)两种。有限状态机是指状态的数量是有限的,而无限状态机则可以有无限多个状态。在系统设计中,有限状态机比较常见。
Spring状态机原理
Spring状态机建立在有限状态机(FSM)的概念之上,提供了一种简洁且灵活的方式来定义、管理和执行状态机。它将状态定义为Java对象,并通过配置来定义状态之间的转换规则。状态转换通常由外部事件触发,我们可以根据业务逻辑定义不同的事件类型,并与状态转换关联。Spring状态机还提供了状态监听器,用于在状态变化时执行特定的逻辑。同时,状态机的状态可以持久化到数据库或其他存储介质中,以便在系统重启或故障恢复时保持状态的一致性。
Spring状态机核心主要包括以下三个关键元素:
-
状态(State):定义了系统可能处于的各个状态,如订单状态中的待支付、已支付等。
-
转换(Transition):描述了在何种条件下,当接收到特定事件时,系统可以从一个状态转移到另一个状态。例如,接收到“支付成功”事件时,订单状态从“待支付”转变为“已支付”。
-
事件(Event):触发状态转换的动作或者消息,它是引起状态机从当前状态迁移到新状态的原因。
接下来,我们将上述状态模式中关于订单状态的示例转换为状态机实现。
Spring状态机的使用
对于状态机,Spring中封装了一个组件spring-statemachine
,直接引入即可。
引入依赖
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
定义状态机的状态以及事件类型
在状态机(Finite State Machine, FSM
)的设计中,“定义状态”和“定义转换”是构建状态机模型的基础元素。
定义状态(States): 状态是状态机的核心组成单元,代表了系统或对象在某一时刻可能存在的条件或模式。在状态机中,每一个状态都是系统可能处于的一种明确的条件或阶段。例如,在一个简单的咖啡机状态机中,可能有的状态包括“待机”、“磨豆”、“冲泡”和“完成”。每个状态都是独一无二的,且在任何给定时间,系统只能处于其中一个状态。
定义转换(Transitions): 转换则是指状态之间的转变过程,它是状态机模型动态性的体现。当一个外部事件(如用户按下按钮、接收到信号、满足特定条件等)触发时,状态机会从当前状态转移到另一个状态。在定义转换时,需要指出触发转换的事件(Event
)以及事件发生时系统的响应,即从哪个状态(Source State
)转到哪个状态(Target State
)。
/**
*订单状态
*/
public enum OrderStatusEnum {
/**待提交*/
DRAFT,
/**待出库*/
SUBMITTED,
/**已出库*/
DELIVERING,
/**已签收*/
SIGNED,
/**已完成*/
FINISHED,
;
}
/**
* 订单状态流转事件
*/
public enum OrderStatusOperateEventEnum {
/**确认,已提交*/
CONFIRMED,
/**发货*/
DELIVERY,
/**签收*/
RECEIVED,
/**完成*/
CONFIRMED_FINISH,
;
}
定义状态机以及状态流转规则
状态机配置类是在使用Spring State Machine
或其他状态机框架时的一个重要步骤,这个类主要用于定义状态机的核心结构,包括状态(states
)、事件(events
)、状态之间的转换规则(transitions
),以及可能的状态迁移动作和决策逻辑。
在Spring State Machine
中,创建状态机配置类通常是通过继承StateMachineConfigurerAdapter
类来实现的。这个适配器类提供了几个模板方法,允许开发者重写它们来配置状态机的各种组成部分:
-
配置状态(
configureStates(StateMachineStateConfigurer)
): 在这个方法中,开发者定义状态机中所有的状态,包括初始状态(initial state
)和结束状态(final/terminal states
)。例如,定义状态A、B、C,并指定状态A作为初始状态。 -
配置转换(
configureTransitions(StateMachineTransitionConfigurer)
): 在这里,开发者描述状态之间的转换规则,也就是当某个事件(event
)发生时,状态机应如何从一个状态转移到另一个状态。例如,当事件X发生时,状态机从状态A转移到状态B。 -
配置初始状态(
configureInitialState(ConfigurableStateMachineInitializer)
): 如果需要显式指定状态机启动时的初始状态,可以在该方法中设置。
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStatusMachineConfig extends StateMachineConfigurerAdapter<OrderStatusEnum, OrderStatusOperateEventEnum> {
/**
* 设置状态机的状态
* StateMachineStateConfigurer 即 状态机状态配置
* @param states 状态机状态
* @throws Exception 异常
*/
@Override
public void configure(StateMachineStateConfigurer<OrderStatusEnum, OrderStatusOperateEventEnum> states) throws Exception {
states.withStates()
.initial(OrderStatusEnum.DRAFT)
.end(OrderStatusEnum.FINISHED)
.states(EnumSet.allOf(OrderStatusEnum.class));
}
/**
* 设置状态机与订单状态操作事件绑定
* StateMachineTransitionConfigurer
* @param transitions
* @throws Exception
*/
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatusEnum, OrderStatusOperateEventEnum> transitions) throws Exception {
transitions.withExternal().source(OrderStatusEnum.DRAFT).target(OrderStatusEnum.SUBMITTED)
.event(OrderStatusOperateEventEnum.CONFIRMED)
.and()
.withExternal().source(OrderStatusEnum.SUBMITTED).target(OrderStatusEnum.DELIVERING)
.event(OrderStatusOperateEventEnum.DELIVERY)
.and()
.withExternal().source(OrderStatusEnum.DELIVERING).target(OrderStatusEnum.SIGNED)
.event(OrderStatusOperateEventEnum.RECEIVED)
.and()
.withExternal().source(OrderStatusEnum.SIGNED).target(OrderStatusEnum.FINISHED)
.event(OrderStatusOperateEventEnum.CONFIRMED_FINISH);
}
}
配置状态机持久化
状态机持久化是指将状态机在某一时刻的状态信息存储到数据库、缓存系统等中,使得即使在系统重启、网络故障或进程终止等情况下,状态机仍能从先前保存的状态继续执行,而不是从初始状态重新开始。
在业务场景中,例如订单处理、工作流引擎、游戏进度跟踪等,状态机通常用于表示某个实体在其生命周期内的状态变迁。如果没有持久化机制,一旦发生意外情况导致系统宕机或重启,未完成的状态变迁将会丢失,这对于业务连续性和一致性是非常不利的。
状态机持久化通常涉及以下几个方面:
- 状态记录:记录当前状态机实例处于哪个状态。
- 上下文数据:除了状态外,可能还需要持久化与状态关联的上下文数据,例如触发状态变迁的事件参数、额外的状态属性等。
- 历史轨迹:某些复杂场景下可能需要记录状态机的历史变迁轨迹,以便于审计、回溯分析或错误恢复。
- 并发控制:在多线程或多节点环境下,状态机的持久化还要考虑并发访问和同步的问题。
Spring Statemachine
提供了与Redis
,MongoDB
等数据存储结合的持久化方案,可以将状态机的状态信息序列化后存储到Redis中。当状态机需要恢复时,可以从存储中读取状态信息并重新构造状态机实例,使其能够从上次中断的地方继续执行流程。
@Configuration
public class OrderPersist {
/**
* 持久化配置
* 在实际使用中,可以配合数据库或者Redis等进行持久化操作
* @return
*/
@Bean
public DefaultStateMachinePersister<OrderStatusEnum, OrderStatusOperateEventEnum, OrderDO> stateMachinePersister(){
Map<OrderDO, StateMachineContext<OrderStatusEnum, OrderStatusOperateEventEnum>> map = new HashMap();
return new DefaultStateMachinePersister<>(new StateMachinePersist<OrderStatusEnum, OrderStatusOperateEventEnum, OrderDO>() {
@Override
public void write(StateMachineContext<OrderStatusEnum, OrderStatusOperateEventEnum> context, OrderDO order) throws Exception {
//持久化操作
map.put(order, context);
}
@Override
public StateMachineContext<OrderStatusEnum, OrderStatusOperateEventEnum> read(OrderDO order) throws Exception {
//从库中或者redis中读取order的状态信息
return map.get(order);
}
});
}
}
定义状态机监听器
状态机监听器(State Machine Listener
)是一种组件,它可以监听并响应状态机在运行过程中的各种事件,例如状态变迁、进入或退出状态、转换被拒绝等。
在Spring Statemachine
中,监听器可以通过实现StateMachineListener
接口来定义。该接口提供了一系列回调方法,如transitionTriggered
、stateEntered
、stateExited
等,当状态机触发转换、进入新状态或离开旧状态时,这些方法会被调用。同时,我们也可以通过注解实现监听器。注解方式可以在类的方法上直接声明该方法应该在何种状态下被调用,简化监听器的编写和配置。例如@OnTransition
,@OnTransitionEnd
,@OnTransitionStart
等
@Component
@WithStateMachine(name = "orderStateMachine")
public class OrderStatusListener {
@OnTransition(source = "DRAFT", target = "SUBMITTED")
public boolean payTransition(Message<OrderStatusOperateEventEnum> message) {
OrderDO order = (OrderDO) message.getHeaders().get("order");
order.setOrderStatusEnum(OrderStatusEnum.SUBMITTED);
System.out.println(String.format("出库订单[%s]确认,状态机信息:%s", order.getOrderNo(), message.getHeaders()));
return true;
}
@OnTransition(source = "SUBMITTED", target = "DELIVERING")
public boolean deliverTransition(Message<OrderStatusOperateEventEnum> message) {
OrderDO order = (OrderDO) message.getHeaders().get("order");
order.setOrderStatusEnum(OrderStatusEnum.DELIVERING);
System.out.println(String.format("出库订单[%s]发货出库,状态机信息:%s", order.getOrderNo(), message.getHeaders()));
return true;
}
@OnTransition(source = "DELIVERING", target = "SIGNED")
public boolean receiveTransition(Message<OrderStatusOperateEventEnum> message){
OrderDO order = (OrderDO) message.getHeaders().get("order");
order.setOrderStatusEnum(OrderStatusEnum.SIGNED);
System.out.println(String.format("出库订单[%s]签收,状态机信息:%s", order.getOrderNo(), message.getHeaders()));
return true;
}
@OnTransition(source = "SIGNED", target = "FINISHED")
public boolean finishTransition(Message<OrderStatusOperateEventEnum> message){
OrderDO order = (OrderDO) message.getHeaders().get("order");
order.setOrderStatusEnum(OrderStatusEnum.FINISHED);
System.out.println(String.format("出库订单[%s]完成,状态机信息:%s", order.getOrderNo(), message.getHeaders()));
return true;
}
}
而监听器需要监听到状态流转的事件才会发挥他的作用,才能监听到某个状态事件之后,完成状态的变更。
@Component
public class StateEventUtil {
private StateMachine<OrderStatusEnum, OrderStatusOperateEventEnum> orderStateMachine;
private StateMachinePersister<OrderStatusEnum, OrderStatusOperateEventEnum, OrderDO> stateMachinePersister;
/**
* 发送状态转换事件
* synchronized修饰保证这个方法是线程安全的
* @param message
* @return
*/
public synchronized boolean sendEvent(Message<OrderStatusOperateEventEnum> message) {
boolean result = false;
try {
//启动状态机
orderStateMachine.start();
OrderDO order = (OrderDO) message.getHeaders().get("order");
//尝试恢复状态机状态
stateMachinePersister.restore(orderStateMachine, order);
result = orderStateMachine.sendEvent(message);
//持久化状态机状态
stateMachinePersister.persist(orderStateMachine, order);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(message)) {
OrderDO order = (OrderDO) message.getHeaders().get("order");
if (Objects.nonNull(order) && Objects.equals(order.getOrderStatusEnum(), OrderStatusEnum.FINISHED)) {
orderStateMachine.stop();
}
}
}
return result;
}
@Autowired
public void setOrderStateMachine(StateMachine<OrderStatusEnum, OrderStatusOperateEventEnum> orderStateMachine) {
this.orderStateMachine = orderStateMachine;
}
@Autowired
public void setStateMachinePersister(StateMachinePersister<OrderStatusEnum, OrderStatusOperateEventEnum, OrderDO> stateMachinePersister) {
this.stateMachinePersister = stateMachinePersister;
}
}
到这里,我们的状态机就定义好了,下面我们就可以在业务代码中使用状态机完成的订单状态的流转。
业务代码使用
@Service
public class OrderServiceImpl implements IOrderService {
private StateEventUtil stateEventUtil;
private static final AtomicInteger ID_COUNTER = new AtomicInteger(0);
private static final Map<Long, OrderDO> ORDER_MAP = new ConcurrentHashMap<>();
/**
* 创建新订单
*
* @param orderDO
*/
@Override
public Long createOrder(OrderDO orderDO) {
long orderId = ID_COUNTER.incrementAndGet();
orderDO.setOrderId(orderId);
orderDO.setOrderNo("OC20240306" + orderId);
orderDO.setOrderStatusEnum(OrderStatusEnum.DRAFT);
ORDER_MAP.put(orderId, orderDO);
System.out.println(String.format("订单[%s]创建成功:", orderDO.getOrderNo()));
return orderId;
}
/**
* 确认订单
*
* @param orderId
*/
@Override
public void confirmOrder(Long orderId) {
OrderDO order = ORDER_MAP.get(orderId);
System.out.println("确认订单,订单号:" + order.getOrderNo());
Message message = MessageBuilder.withPayload(OrderStatusOperateEventEnum.CONFIRMED).
setHeader("order", order).build();
if (!stateEventUtil.sendEvent(message)) {
System.out.println(" 确认订单失败, 状态异常,订单号:" + order.getOrderNo());
}
}
/**
* 订单发货
*
* @param orderId
*/
@Override
public void deliver(Long orderId) {
OrderDO order = ORDER_MAP.get(orderId);
System.out.println("订单出库,订单号:" + order.getOrderNo());
Message message = MessageBuilder.withPayload(OrderStatusOperateEventEnum.DELIVERY).
setHeader("order", order).build();
if (!stateEventUtil.sendEvent(message)) {
System.out.println(" 订单出库失败, 状态异常,订单号:" + order.getOrderNo());
}
}
/**
* 签收订单
*
* @param orderId
*/
@Override
public void signOrder(Long orderId) {
OrderDO order = ORDER_MAP.get(orderId);
System.out.println("订单签收,订单号:" + order.getOrderNo());
Message message = MessageBuilder.withPayload(OrderStatusOperateEventEnum.RECEIVED).
setHeader("order", order).build();
if (!stateEventUtil.sendEvent(message)) {
System.out.println(" 订单签收失败, 状态异常,订单号:" + order.getOrderNo());
}
}
/**
* 确认完成
*
* @param orderId
*/
@Override
public void finishOrder(Long orderId) {
OrderDO order = ORDER_MAP.get(orderId);
System.out.println("订单完成,订单号:" + order.getOrderNo());
Message message = MessageBuilder.withPayload(OrderStatusOperateEventEnum.CONFIRMED_FINISH).
setHeader("order", order).build();
if (!stateEventUtil.sendEvent(message)) {
System.out.println(" 订单完成失败, 状态异常,订单号:" + order.getOrderNo());
}
}
/**
* 获取所有订单信息
*/
@Override
public List<OrderDO> listOrders() {
return new ArrayList<>(ORDER_MAP.values());
}
@Autowired
public void setStateEventUtil(StateEventUtil stateEventUtil) {
this.stateEventUtil = stateEventUtil;
}
}
我们在定义一个接口,模拟订单的状态流转:
@RestController
public class OrderController {
private IOrderService orderService;
@GetMapping("testOrderStatusMachine")
public void testOrderStatusMachine(){
Long orderId1 = orderService.createOrder(new OrderDO());
Long orderId2 = orderService.createOrder(new OrderDO());
orderService.confirmOrder(orderId1);
new Thread("客户线程"){
@Override
public void run() {
orderService.deliver(orderId1);
orderService.signOrder(orderId1);
orderService.finishOrder(orderId1);
}
}.start();
orderService.confirmOrder(orderId2);
orderService.deliver(orderId2);
orderService.signOrder(orderId2);
orderService.finishOrder(orderId2);
System.out.println("全部订单状态:" + orderService.listOrders());
}
@Autowired
public void setOrderService(IOrderService orderService) {
this.orderService = orderService;
}
}
我们调用接口:
我们在日志中可以看到订单状态在状态机的控制下,流转的很丝滑。。。
注意事项
-
一致性保证:确保状态机的配置正确反映了业务逻辑,并保持其在并发环境下的状态一致性。
-
异常处理:在状态转换过程中可能出现异常情况,需要适当地捕获和处理这些异常,防止状态机进入无效状态。
-
监控与审计:在实际应用中,为了便于调试和追溯,可以考虑集成日志记录或事件监听器来记录状态机的每一次状态变迁。
-
扩展性与维护性:随着业务的发展,状态机的设计应当具有足够的灵活性,以便于新增状态或调整转换规则。
一点思考
除了直接使用如Spring状态机这样的专门状态管理工具外,还可以使用其他的哪些方法实现状态机的功能呢?比如:
-
消息队列方式
状态的变更通过发布和消费消息来驱动。每当发生状态变更所需的事件时,生产者将事件作为一个消息发布到特定的消息队列(Topic),而消费者则监听这些消息,根据消息内容和业务规则对订单状态进行更新。这种方式有利于解耦各个服务,实现异步处理,同时增强系统的伸缩性和容错能力。 -
定时任务驱动
使用定时任务定期检查系统中的订单状态,根据预设的业务规则判断是否满足状态变迁条件。比如,每隔一段时间执行一次Job,查询数据库中处于特定状态的订单,并决定是否进行状态更新。这种方法适用于具有一定时效性的状态变迁,但实时性相对较低,对于瞬时响应要求高的场景不太适用。
有关SpringBoot下几种定时任务的实现方式请参考:玩转SpringBoot:SpringBoot的几种定时任务实现方式
-
规则引擎方式
利用规则引擎(如Drools
、LiteFlow
等)实现状态机,业务团队可以直接在规则引擎中定义状态及状态之间的转换规则,当新的事实数据(如订单信息)输入到规则引擎时,引擎会自动匹配并执行相应的规则,触发状态改变。这种方式的优点在于业务规则高度集中,易于管理和修改,同时也具备较高的灵活性,能够快速应对业务规则的变化。
SpringBoot下使用LiteFlow规则引擎请参考:轻松应对复杂业务逻辑:LiteFlow-编排式规则引擎框架的优势
总结
Spring状态机提供了一种强大的工具,使得在Java应用中实现复杂的业务流程变得更为简洁和规范。不仅可以提升代码的可读性和可维护性,还能有效降低不同模块之间的耦合度,提高系统的整体稳定性与健壮性。文章来源:https://www.toymoban.com/news/detail-838884.html
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等文章来源地址https://www.toymoban.com/news/detail-838884.html
到了这里,关于Spring状态机(FSM),让订单状态流转如丝般顺滑的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!