如何基于状态机对订单状态实现统一管理?

1、问题分析

当前有一个预约下单 得场景,在该模块设计订单状态共有7种:

目前我们使用了待支付、派单中两种状态,在代码中我们发现存在对订单状态进行硬编码的情况:

java 复制代码
public void paySuccess(TradeStatusMsg tradeStatusMsg) {
  ...
  //订单状态为待支付时当支付成功更新为派单中
    if (ObjectUtil.equal(0, orders.getOrdersStatus())) {
       ...
    }

}

随着开发的深入这种代码会越来越多,比如在实现对订单进行关闭时代码会写成如下的形式:

java 复制代码
1
  //运营人员在订单完成时取消订单
  //执行此场景下的业务逻辑
  //更新订单状态为派单中
  update(id,已关闭)
)
if(订单状态==服务中){
  //运营人员在服务中时取消订单
  //执行此场景下的业务逻辑
  //更新订单状态为已关闭
  update(id,已关闭)
)
...

存在问题:

  • 在业务代码中对订单状态进行 硬编码 如果有一天更改了业务逻辑就需要更改代码,不方便进行系统扩展和维护。

  • 另外对订单状态的管理是散落在很多地方不方便对订单状态进行统一管理和维护。

2、使用状态机解决问题

我们可以使用状态机对订单状态进行统一管理

2.1、什么是状态机?

上图在UML中叫状态图(又叫状态机图),UML是软件开发中的一种建模语言,用来辅助进行软件设计,常用的如:类图、对象、状态图、序列图等,注意状态机图并不是状态机,状态机是一种数学模型,应用在自动化控制、 计算机科学 、通信等很多领域,简单理解状态机就是对状态进行统一管理的数学模型。

我们画的状态图是状态机在 计算机科学 中的应用方法,还有状态机设计模式也是状态机在软件领域的应用方法。

状态机设计模式是状态机在软件中的应用,状态机设计模式描述了一个对象在内部状态发生变化时如何改变其行为,将状态之间的变更定义为事件,将事件暴露出去,通过执行状态变更事件去更改状态,这是状态机设计模式的核心内容。

理解状态机设计模式需要理解四个要素:现态事件动作次态

1、现态:是指当前所处的状态。

2、事件:当一个条件被满足,状态会由现态变为新的状态,事件发生会触发一个动作,或者执行一次状态的迁移。

3、动作:发生事件执行的动作,动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。

4、次态:条件满足后要迁往的新状态。

待支付状态到派单中状态举例:

现态:订单当前处于待支付状态那么现态为待支付

事件:用户支付成功为事件,支付成功是条件,当条件满足进行状态迁移

动作:将订单状态由待支付更改为派单中

次态:派单中

2.2、使用状态机优化代码

支付成功更改订单状态的代码优化如下:

java 复制代码
if(支付状态==支付成功){
    //调用状态机执行支付成功事件
    orderStateMachine.changeStatus(id,支付成功事件);
}

订单取消的代码优化如下:

java 复制代码
orderStateMachine.changeStatus(id,订单完成时取消订单事件);

我们发现使用状态机的代码并没有对订单状态进行硬编码,只是指定了订单id和事件名称,执行changeStatus方法后自动更改订单的状态。

3、实现订单状态机

3.1、添加组件依赖

xml 复制代码
<dependency>
    <groupId>com.jzo2o</groupId>
    <artifactId>jzo2o-statemachine</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3.2、订单状态枚举类

阅读订单状态枚举类,它实现了StatusDefine 状态接口,不论是现态还是次态都需要实现状态接口

定义每个枚举需要注意见名知意,比如:NO_PAY(0, "待支付", "NO_PAY")表示待支付状态。

java 复制代码
package com.jzo2o.orders.base.enums;

import com.jzo2o.statemachine.core.StatusDefine;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author itcast
 */
@Getter
@AllArgsConstructor
public enum OrderStatusEnum implements StatusDefine {
    NO_PAY(0, "待支付", "NO_PAY"),
    DISPATCHING(100, "派单中", "DISPATCHING"),
    NO_SERVE(200, "待服务", "NO_SERVE"),
    SERVING(300, "服务中", "SERVING"),
    FINISHED(500, "已完成", "FINISHED"),
    CANCELED(600, "已取消", "CANCELED"),
    CLOSED(700, "已关闭", "CLOSED");

    private final Integer status;
    private final String desc;
    private final String code;

    /**
     * 根据状态值获得对应枚举
     *
     * @param status 状态
     * @return 状态对应枚举
     */
    public static OrderStatusEnum codeOf(Integer status) {
        for (OrderStatusEnum orderStatusEnum : values()) {
            if (orderStatusEnum.status.equals(status)) {
                return orderStatusEnum;
            }
        }
        return null;
    }
}

3.3、状态变更事件枚举类

所有状态之间存在的变更都需要定义状态变更事件 ,它实现了StatusChangeEvent 状态变更事件接口,事件对应状态机四要素的事件

PAYED(OrderStatusEnum.NO_PAY, OrderStatusEnum.DISPATCHING, "支付成功", "payed")表示由NO_PAY(未支付)状态变化为DISPATCHING(派单中)状态,事件名称为"支付成功"(payed)。

java 复制代码
package com.jzo2o.orders.base.enums;

import com.jzo2o.statemachine.core.StatusChangeEvent;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author itcast
 */
@Getter
@AllArgsConstructor
public enum OrderStatusChangeEventEnum implements StatusChangeEvent {
    PAYED(OrderStatusEnum.NO_PAY, OrderStatusEnum.DISPATCHING, "支付成功", "payed"),
    DISPATCH(OrderStatusEnum.DISPATCHING, OrderStatusEnum.NO_SERVE, "接单/抢单成功", "dispatch"),
    START_SERVE(OrderStatusEnum.NO_SERVE, OrderStatusEnum.SERVING, "开始服务", "start_serve"),
    COMPLETE_SERVE(OrderStatusEnum.SERVING, OrderStatusEnum.FINISHED, "完成服务", "complete_serve"),
//    EVALUATE(OrderStatusEnum.NO_EVALUATION, OrderStatusEnum.FINISHED, "评价完成", "evaluate"),
CANCEL(OrderStatusEnum.NO_PAY, OrderStatusEnum.CANCELED, "取消订单", "cancel"),
    SERVE_PROVIDER_CANCEL(OrderStatusEnum.NO_SERVE, OrderStatusEnum.DISPATCHING, "服务人员/机构取消订单", "serve_provider_cancel"),
    CLOSE_DISPATCHING_ORDER(OrderStatusEnum.DISPATCHING, OrderStatusEnum.CLOSED, "派单中订单关闭", "close_dispatching_order"),
    CLOSE_NO_SERVE_ORDER(OrderStatusEnum.NO_SERVE, OrderStatusEnum.CLOSED, "待服务订单关闭", "close_no_serve_order"),
    CLOSE_SERVING_ORDER(OrderStatusEnum.SERVING, OrderStatusEnum.CLOSED, "服务中订单关闭", "close_serving_order"),
//    CLOSE_NO_EVALUATION_ORDER(OrderStatusEnum.NO_EVALUATION, OrderStatusEnum.CLOSED, "待评价订单关闭", "close_no_evaluation_order"),
CLOSE_FINISHED_ORDER(OrderStatusEnum.FINISHED, OrderStatusEnum.CLOSED, "已完成订单关闭", "close_finished_order");
    /**
     * 源状态
     */
    private final OrderStatusEnum sourceStatus;

    /**
     * 目标状态
     */
    private final OrderStatusEnum targetStatus;

    /**
     * 描述
     */
    private final String desc;

    /**
     * 代码
     */
    private final String code;
}

3.4、定义订单快照类

快照是订单变化瞬间的状态及相关信息。

比如:001号订单创建成功此时记录它的快照信息(订单号、下单人、订单详细信息、订单状态等),当001号订单支付成功由待支付状态变化为派单中状态此时也会记录它的快照信息(订单号、下单人、支付状态、支付相关信息,订单状态等相关信息),由此可以看出订单快照可以追溯订单的历史变化信息,只要状态发生变化便会记录快照。

快照基础类型是StateMachineSnapshot,如果我们要实现订单快照则需要定义一个订单快照类OrderSnapshotDTO 去继承StateMachineSnapshot类型,代码如下:

java 复制代码
package com.jzo2o.orders.base.model.dto;

import com.jzo2o.statemachine.core.StateMachineSnapshot;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 订单快照
 *
 * @author itcast
 * @create 2023/8/19 10:30
 **/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSnapshotDTO extends StateMachineSnapshot {
//....原来的内容保持不变,添加以下代码

@Override
public String getSnapshotId() {
    return String.valueOf(id);
}

@Override
public Integer getSnapshotStatus() {
    return ordersStatus;
}

@Override
public void setSnapshotId(String snapshotId) {
    this.id = Long.parseLong(snapshotId);
}

@Override
public void setSnapshotStatus(Integer snapshotStatus) {
    this.ordersStatus = snapshotStatus;
}

}

3.5、定义事件变更动作

当执行状态变更事件会伴随着执行具体的动作,此部分对应状态机四要素中的动作。

定义订单支付成功动作类,实现StatusChangeHandler接口,泛型中指定快照类型。

此动作是订单支付成功执行的动作。

动作类的bean名称为"状态机名称_事件名称",例如下边的动作类bean的名称为order_payed,表示order状态机的payed事件。

java 复制代码
package com.jzo2o.orders.base.handler;

import cn.hutool.db.DbRuntimeException;
import com.jzo2o.orders.base.enums.OrderPayStatusEnum;
import com.jzo2o.orders.base.enums.OrderStatusEnum;
import com.jzo2o.orders.base.model.dto.OrderSnapshotDTO;
import com.jzo2o.orders.base.model.dto.OrderUpdateStatusDTO;
import com.jzo2o.orders.base.service.IOrdersCommonService;
import com.jzo2o.statemachine.core.StateMachineSnapshot;
import com.jzo2o.statemachine.core.StatusChangeEvent;
import com.jzo2o.statemachine.core.StatusChangeHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * 订单支付成功处理器
 *
 * @author itcast
 * @create 2023/8/17 18:08
 **/
@Slf4j
@Component("order_payed")
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {
    @Resource
    private IOrdersCommonService ordersService;

    /**
     * 订单支付处理逻辑
     *
     * @param bizId   业务id
     * @param bizSnapshot 快照
     */
    @Override
    public void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {
        log.info("支付成功事件处理逻辑开始,订单号:{}", bizId);
        
    }


}

3.6、定义订单状态机类

AbstractStateMachine状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类,比如我们实现订单状态机就需要继承AbstractStateMachine抽象类。

成员变量

初始状态:设置初始状态,比如订单的初始状态为待支付。

状态机名称:返回的状态机的标识,比如订单状态机返回"order"作为订单状态机的名称。

方法

返回状态机名称:返回状态机的名称

返回初始状态:返回初始状态

启动状态机:开始进行状态机管理,通常在新建实例时调用此方法,比如:新建一个订单调用此方法将订单状态设置为初始状态,传入参数:业务主键(如订单id)

变更状态:调用此方法更改状态

后处理方法:当状态变更后统一执行的逻辑。

java 复制代码
package com.jzo2o.orders.base.config;


/**
 * 订单状态机
 *
 * @author itcast
 * @create 2023/8/4 11:20
 **/
@Component
public class OrderStateMachine extends AbstractStateMachine<OrderSnapshotDTO> {

 
    public OrderStateMachine(StateMachinePersister stateMachinePersister, BizSnapshotService bizSnapshotService, RedisTemplate redisTemplate) {
        super(stateMachinePersister, bizSnapshotService, redisTemplate);
    }

    /**
     * 设置状态机名称
     *
     * @return 状态机名称
     */
    @Override
    protected String getName() {
        return "order";
    }

    @Override
    protected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) {

    }

    /**
     * 设置状态机初始状态
     *
     * @return 状态机初始状态
     */
    @Override
    protected OrderStatusEnum getInitState() {
        return OrderStatusEnum.NO_PAY;
    }

}

3.7、状态机表设计

状态机使用MySQL对状态进行持久化

3.7.1、状态机持久化表

每个订单对应状态机表中的一条记录。

state_machine_name :针对订单的状态机起个名称叫order,针对服务单的状态机可以起个名称为serve。

biz_id:存储订单id

state:记录该订单的当前状态

java 复制代码
create table `jzo2o-orders`.state_persister
(
    id                 bigint auto_increment comment '主键'
        constraint `PRIMARY`
        primary key,
    state_machine_name varchar(255)                       null comment '状态机名称',
    biz_id             varchar(255)                       null comment '业务id',
    state              varchar(255)                       null comment '状态',
    create_time        datetime default CURRENT_TIMESTAMP null comment '创建时间',
    update_time        datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
    constraint 唯一索引
        unique (state_machine_name, biz_id)
)
    comment '状态机持久化表' charset = utf8mb4;

3.7.2、状态机快照表

一个订单在快照表有多条记录,每变一个状态会记录该状态下的快照信息(即订单相关的详细信息)便于查询订单变化的历史记录。

state_machine_name :同上

biz_id :同上

db_shard_id:暂时用不到

state:对应快照的状态

biz_data:快照信息(json格式),用在订单状态机就是记录订单相关的信息。

java 复制代码
create table `jzo2o-orders`.biz_snapshot
(
    id                 bigint auto_increment comment '主键'
        constraint `PRIMARY`
        primary key,
    state_machine_name varchar(50)                        null comment '状态机名称',
    biz_id             varchar(50)                        null comment '业务id',
    db_shard_id        bigint                             null comment '分库键',
    state              varchar(50)                        null comment '状态代码',
    biz_data           varchar(5000)                      null comment '业务数据',
    create_time        datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time        datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '业务数据快照' charset = utf8mb4;
相关推荐
知兀10 分钟前
【MybatisPlus】后端用枚举类,数据库用tinyint,存在枚举类型转换
java
StockTV13 分钟前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
User_芊芊君子15 分钟前
【OpenAI 把 AI 玩明白了】:自主推理 + 动态知识图谱,这 4 个技术突破要颠覆行业
java·人工智能·知识图谱
c++之路1 小时前
C++20概述
java·开发语言·c++20
Championship.23.241 小时前
Linux Top 命令族深度解析与实战指南
java·linux·服务器·top·linux调试
橘子海全栈攻城狮1 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
逻辑驱动的ken1 小时前
Java高频面试考点18
java·开发语言·数据库·算法·面试·职场和发展·哈希算法
冷雨夜中漫步2 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
直奔標竿2 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring
__土块__2 小时前
AI 系统可观测性落地:从请求链路到管理后台的指标决策实践
状态机·可观测性·系统稳定性·故障排查·管理后台·监控告警·ai工程