《从零开始的毕业设计》-ELS中台快递物流调度系统(五)整合StateMachine有限状态机

372 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

本篇将采用StateMachine框架来完整一些订单状态的转换,主要是根据前面规划好的正向状态机和逆向状态机进行配置,属于锦上添花。

正向物流状态机

正向物流状态机是以卖家的角度进行定义的,商品的走向,就是整个商品到物流,再到快递,最后到达买家的手里的整一个过程。是比较好想理解的,对应的就是现始世界中的购物操作。 引用原作者的图: img

逆向物流状态机

逆向物流状态机跟正向就是反过来,是以商品从买家手里,退还给卖家,对应现实世界中的退货操作。

img

业务场景

光时看订单的状态机就可以清晰地发现一个订单中的状态是有很多种的。我们业务中的订单随时随地都会发生变化,其所在对应的数据库数据,也需要发生更新。

最直观也最简单的方式就是在每一个订单操作中更新他的订单状态

//比如1就是创建订单
order.setState(1);
service.update(order);

我们来分析一下这个实现方式的弊端,然后再引出我们今天的主角StateMachine

弊端1:繁多的状态

前面已经介绍了光是一个订单实体就有非常多的状态(state),我们项目中还会有许多实体,比如用户的实体、仓库的实体、商品的实体....都有非常多的状态。

所以当业务复杂起来之后,实体的状态维护就会显得非常繁琐。

一般我们对于实体状态这种经常被引用,并且不怎么会改变的变量,都会定义为常量,java里面就可以直接用static final进行修饰就可以。

ELS-logistics-common这个工程项目就是专门放公共资源

image.png

弊端2:状态动作的衔接

除了状态的繁多,我们还需要在每个动作中插入我们自己的状态更新代码。 这个动作是广义上的定义,比如说当刚开始创建订单的时候,这就是一个动作

还有当一个快递被签收之后,我们物流收到电商或者是快递公司传递过来的签收信息,我们需要加一个签收过程,并且在签收过程中,把订单状态改为已完成,这也是一个动作

这就意味我们需要人为地在每个状态变更的动作后面加入我们的状态更新,显得十分鸡肋。

弊端3:扩展性差

当我们想要新增一个状态变更的时候,就需要对我们原有的业务代码进行入侵式地更改,这其实是在破坏设计模式中的开闭原则,代码的扩展性非常的低。

StateMachine

官网:Spring Statemachine

对于StateMachine的介绍,大家如果感兴趣的可以去官网进行详细地查阅,因为本文不全是StateMachine的技术分享,本文关注于介绍:

  • Statemachine是个什么东西?
  • Statemachine能帮助我们干啥?
  • 如何快速使用?

简单的理解,Statemachine就是一个状态机,能帮我们完成状态变更的一个框架足矣

快速开始

在上文我们已经介绍了几个非常重要的概念:

  • 状态States
  • 状态变更的操作(事件) Events

只要我们发生了我们定义好的事件,那么状态就会进行转移,这就是Statemachine的一个最简单的工作流程。

引入依赖

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>${statemachine.version}</version>
</dependency>

定义状态和事件

我们通过枚举类型Enum来定义,状态和事件

public enum OrderEvents {
    //创建 0
    CREATE,
    //发货 10
    DELIVERY,
    //仓库接单 11
    WAREHOUSE_RECEIVED,
    //仓库出库 20
    WAREHOUSE_OUTBOUND,
    //配送接单 21
    DISTRIBUTION_RECEIVED,
    //签收 100
    SIGNED,
    //拒签
    REJECT,
}
public enum OrderStates {
    TO_CREATE(0,"创建"),
    TO_DELIVERY(10,"发货"),
    TO_WAREHOUSE_RECEIVED(11,"仓库接单"),
    TO_WAREHOUSE_OUTBOUND(20,"仓库出库"),
    TO_DISTRIBUTION_RECEIVED(21,"配送接单"),
    TO_SIGNED(100,"签收"),
    TO_REJECT(200,"签收");


    private final String display;
    @EnumValue
    private final Integer key;

    OrderStates(Integer key, String display){
        this.key = key;
        this.display = display;
    }

    public String getDisplay() {
        return display;
    }

    public Integer getKey() {
        return key;
    }

}

事件触发机制

我们这里通过注解的方式来进行事件触发的定义,统一管理比定义在StateMachineConfig那里方便很多,后期维护也非常方便。

@WithStateMachine
public class OrderSingleEventConfig {
private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 当前状态CREATE
     */
    @OnTransition(target = "TO_CREATE")
    public void toCreate() {
        logger.info("---订单预创建---");
    }

    /**
     * TO_CREATE->TO_DELIVERY 执行的动作
     */
    @OnTransition(source = "TO_CREATE", target = "TO_DELIVERY")
    public void Create() {
        logger.info("---用户完成创建,待收货---");
    }


}

配置StateMachine config

@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {

    //状态配置
    @Override
    public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception { states
                .withStates()
                //初始化创建状态
                .initial(OrderStates.TO_CREATE)
                .states(EnumSet.allOf(OrderStates.class));
    }

    //状态机配置
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
        transitions.withExternal()
                //创建 -> 发货
                .source(OrderStates.TO_CREATE).target(OrderStates.TO_DELIVERY)
                .event(OrderEvents.CREATE);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config)
            throws Exception {
        config.withConfiguration()
                .machineId("turnstileStateMachine");
    }

}

启动StateMachine

我们需要在启动类中,实现CommandLineRunner接口,在其run方法下开启StateMachine,关羽CommandLineRunner可以理解为应用启动,资源初始化的一个小口子。

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.example.elsorder.mapper")
@EnableTransactionManagement(proxyTargetClass = true)
public class ElsOrderApplication implements CommandLineRunner {


    public static void main(String[] args) {
        SpringApplication.run(ElsOrderApplication.class, args);
    }


    @LoadBalanced
    @Bean
    public RestTemplate restTemplate (){
        return new RestTemplate();
    }


    @Autowired
    public StateMachine<OrderStates, OrderEvents> stateMachine;


    @Override
    public void run(String... args) throws Exception {
        stateMachine.start();
    }
}

实现状态转移

@RestController
@RequestMapping("/logistics-order/v1")
public class LogisticsOrderController {
    @Autowired
    public LogisticsOrderServiceImpl service;

    public static final Logger logger = LoggerFactory.getLogger(LogisticsOrderController.class);

    @Autowired
    public RedisTemplate redisTemplate;

    @Autowired
    public RestTemplate restTemplate;

    @Autowired
    public TransactionTemplate transactionTemplate;


    @Autowired
    public StateMachine<OrderStates, OrderEvents> stateMachine;


    @GetMapping("/list")
    @CrossOrigin(origins = "*",maxAge = 3600)
    public resultVO test() {

        resultVO resultVO = new resultVO();
        resultVO.setCode(20000);
        orderListVO<orderDTO> orderListVO = new orderListVO();
        List<LogisticsOrder> list = service.list();
        List<orderDTO> listDTO = new ArrayList<>(list.size());

        //触发CREATE事件
        stateMachine.sendEvent(OrderEvents.CREATE);

        for(LogisticsOrder order : list){
            orderDTO dto = new orderDTO();
            BeanUtils.copyProperties(order,dto);
            listDTO.add(dto);
        }


        orderListVO.setItems(listDTO);
        orderListVO.setTotal(list.size());
        resultVO.setData(orderListVO);
        return resultVO;
        }
        
        .......
    }

效果展示

image-20220330185903490

往期文章