目录
1 状态机
1.1 状态机介绍
1.1.1 当前存在的问题
在预约下单模块设计订单状态共有7种,如下图:
目前我们使用了待支付、派单中两种状态,在代码中我们发现存在对订单状态进行硬编码的情况,但是随着开发的深入这种代码会越来越多,比如在实现对订单进行关闭时代码会写成如下的形式:
1//运营人员在订单完成时取消订单//执行此场景下的业务逻辑//更新订单状态为派单中update(id,已关闭)
)
if(订单状态==服务中){//运营人员在服务中时取消订单//执行此场景下的业务逻辑//更新订单状态为已关闭update(id,已关闭)
)
...
以上代码存在问题如下:
在业务代码中对订单状态进行硬编码如果有一天更改了业务逻辑就需要更改代码,不方便进行系统扩展和维护。
另外对订单状态的管理是散落在很多地方不方便对订单状态进行统一管理和维护。
1.1.2 使用状态机解决问题
针对以上问题如何解决呢?
我们可以使用状态机对订单状态进行统一管理。
什么是状态机?
上图在UML中叫状态图(又叫状态机图),UML是软件开发中的一种建模语言,用来辅助进行软件设计,常用的如:类图、对象、状态图、序列图等,注意状态机图并不是状态机,状态机是一种数学模型,应用在自动化控制、计算机科学、通信等很多领域,简单理解状态机就是对状态进行统一管理的数学模型。
我们画的状态图是状态机在计算机科学中的应用方法,还有状态机设计模式也是状态机在软件领域的应用方法。
状态机设计模式是状态机在软件中的应用,状态机设计模式描述了一个对象在内部状态发生变化时如何改变其行为,将状态之间的变更定义为事件,将事件暴露出去,通过执行状态变更事件去更改状态,这是状态机设计模式的核心内容。
理解状态机设计模式需要理解四个要素:现态、事件、动作、次态。
1、现态:是指当前所处的状态。
2、事件:当一个条件被满足,状态会由现态变为新的状态,事件发生会触发一个动作,或者执行一次状态的迁移。
3、动作:发生事件执行的动作,动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
4、次态:条件满足后要迁往的新状态。
我们拿待支付状态到派单中状态举例:
现态:订单当前处于待支付状态那么现态为待支付。
事件:用户支付成功为事件,支付成功是条件,当条件满足进行状态迁移。
动作:将订单状态由待支付更改为派单中。
次态:派单中。
使用状态机优化代码:
使用状态机之后对代码进行以下优化。
支付成功更改订单状态的代码优化如下:
if(支付状态==支付成功){//调用状态机执行支付成功事件orderStateMachine.changeStatus(id,支付成功事件);
}
订单取消的代码优化如下:
orderStateMachine.changeStatus(id,订单完成时取消订单事件);
我们发现使用状态机的代码并没有对订单状态进行硬编码,只是指定了订单id和事件名称,执行changeStatus方法后自动更改订单的状态。
1.2 实现订单状态机
1.2.1 编写订单状态机
1.2.1.1 依赖引入
本项目基于状态机设计模式开发了状态机组件,代码在jzo2o-framework中,如果在订单管理服务中实现订单状态机需要添加状态机的依赖。
在jzo2o-orders-base工程的pom.xml中添加状态机组件的依赖
<dependency><groupId>com.jzo2o</groupId><artifactId>jzo2o-statemachine</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
1.2.1.2 订单状态枚举类
阅读订单状态枚举类,它实现了StatusDefine 状态接口,不论是现态还是次态都需要实现状态接口。
定义每个枚举需要注意见名知意,比如:NO_PAY(0, “待支付”, “NO_PAY”)表示待支付状态。
订单状态枚举类如下:
@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;}
}
1.2.1.3 状态变更事件枚举类
所有状态之间存在的变更都需要定义状态变更事件,它实现了StatusChangeEvent 状态变更事件接口,事件对应状态机四要素的事件
代码如下,重点看PAYED:
PAYED(OrderStatusEnum.NO_PAY, OrderStatusEnum.DISPATCHING, “支付成功”, “payed”)表示由NO_PAY(未支付)状态变化为DISPATCHING(派单中)状态,事件名称为“支付成功”(payed)。
@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;
}
1.2.1.4 定义订单快照类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSnapshotDTO extends StateMachineSnapshot {/*** 订单id*/private Long id;/*** 订单所属人*/private Long userId;/*** 服务类型id*/private Long serveTypeId;/*** 服务类型名称*/private String serveTypeName;/*** 服务项id*/private Long serveItemId;/*** 服务项名称*/private String serveItemName;/*** 服务项图片*/private String serveItemImg;/*** 服务单位*/private Integer unit;/*** 服务id*/private Long serveId;/*** 订单状态,0:待支付,100:派单中,200:待服务,300:服务中,500:订单完成,600:订单取消,700已关闭*/private Integer ordersStatus;/*** 支付状态,2:待支付,4:支付成功*/private Integer payStatus;/*** 退款,0:发起退款,1:退款中,2:退款成功 3:退款失败*/private Integer refundStatus;/*** 单价*/private BigDecimal price;/*** 购买数量*/private Integer purNum;/*** 订单总金额*/private BigDecimal totalAmount;/*** 实际支付金额*/private BigDecimal realPayAmount;/*** 优惠金额*/private BigDecimal discountAmount;/*** 城市编码*/private String cityCode;/*** 服务详细地址*/private String serveAddress;/*** 联系人手机号*/private String contactsPhone;/*** 联系人姓名*/private String contactsName;/*** 服务开始时间*/private LocalDateTime serveStartTime;/*** 经度*/private String lon;/*** 纬度*/private String lat;/*** 支付时间*/private LocalDateTime payTime;/*** 评价时间*/private LocalDateTime evaluationTime;/*** 订单创建时间*/private LocalDateTime createTime;/*** 订单更新时间*/private LocalDateTime updateTime;/*** 支付服务交易单号*/private Long tradingOrderNo;/*** 支付服务退款单号*/private Long refundNo;/*** 支付渠道【支付宝、微信、现金、免单挂账】*/private String tradingChannel;/*** 三方流水,微信支付订单号或支付宝订单号*/private String thirdOrderId;/*** 退款三方流水,微信支付订单号或支付宝订单号*/private String thirdRefundOrderId;/*** 取消人id*/private Long cancellerId;/*** 取消人名称*/private String cancelerName;/*** 取消人类型*/private Integer cancellerType;/*** 取消时间*/private LocalDateTime cancelTime;/*** 取消原因*/private String cancelReason;/*** 实际服务完成时间*/private LocalDateTime realServeEndTime;/*** 评价状态*/private Integer evaluationStatus;@Overridepublic String getSnapshotId() {return String.valueOf(id);}@Overridepublic Integer getSnapshotStatus() {return ordersStatus;}@Overridepublic void setSnapshotId(String snapshotId) {this.id = Long.parseLong(snapshotId);}@Overridepublic void setSnapshotStatus(Integer snapshotStatus) {this.ordersStatus = snapshotStatus;}}
1.2.1.5 定义事件变更动作类
/*** 支付成功执行的动作*/
@Slf4j
@Component("order_payed")
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {@Resourceprivate IOrdersCommonService ordersService;/*** 订单支付处理逻辑** @param bizId 业务id* @param bizSnapshot 快照*/@Overridepublic void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {log.info("支付成功事件处理逻辑开始,订单号:{}", bizId);}
}
1.2.1.5 定义订单状态机类
AbstractStateMachine状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类,比如我们实现订单状态机就需要继承AbstractStateMachine抽象类。
@Component
public class OrderStateMachine extends AbstractStateMachine<OrderSnapshotDTO> {protected OrderStateMachine(StateMachinePersister stateMachinePersister, BizSnapshotService bizSnapshotService, RedisTemplate redisTemplate) {super(stateMachinePersister, bizSnapshotService, redisTemplate);}/*** 指定状态机的名称* @return*/@Overrideprotected String getName() {return "order";}/*** * @param bizSnapshot*/@Overrideprotected void postProcessor(OrderSnapshotDTO bizSnapshot) {}/*** 指定订单的初始状态* @return*/@Overrideprotected StatusDefine getInitState() {return OrderStatusEnum.NO_PAY;}
}
1.2.1.6 状态机表设计
状态机使用MySQL对状态进行持久化,涉及到如下表:
状态机持久化表:
每个订单对应状态机表中的一条记录。
state_machine_name :针对订单的状态机起个名称叫order,针对服务单的状态机可以起个名称为serve。
biz_id:存储订单id
state:记录该订单的当前状态
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)
)
状态机快照表:
一个订单在快照表有多条记录,每变一个状态会记录该状态下的快照信息(即订单相关的详细信息)便于查询订单变化的历史记录。
state_machine_name :同上
biz_id :同上
db_shard_id:暂时用不到
state:对应快照的状态
biz_data:快照信息(json格式),用在订单状态机就是记录订单相关的信息。
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 '更新时间'
)
1.2.2 测试订单状态机
加载订单状态机
在base工程进行导入状态机
@Configuration
@ComponentScan({"com.jzo2o.orders.base.service","com.jzo2o.orders.base.handler"})
@MapperScan("com.jzo2o.orders.base.mapper")
@Import({OrderStateMachine.class})
@EnableConfigurationProperties({DispatchProperties.class, ExecutorProperties.class})
public class AutoImportConfiguration {
}
测试启动状态机
调用OrderStateMachine的start()方法启动一个订单的状态机,启动状态机表示订单用状态机管理状态,启动状态机后会设置订单的初始状态。
观察state_persister表有一条10100号订单的状态持久化记录,每条订单对应state_persister表的一条记录。
观察biz_snapshot表有一条10100号订单的快照信息,一条订单在biz_snapshot表对应多个条记录,每次订单状态变更都会产生一个快照
@SpringBootTest
@Slf4j
public class OrderStateMachineTest {@Resourceprivate OrderStateMachine orderStateMachine;@Testpublic void test_start() {//启动状态机String start = orderStateMachine.start("10100");log.info("返回初始状态:{}", start);}@Testpublic void test_changeStatus() {//状态变更orderStateMachine.changeStatus("10100", OrderStatusChangeEventEnum.PAYED);}}
根据状态变更事件定义可知,执行测试方法后1010订单的状态由NO_PAY(待支付)变更为DISPATCHING(派单中)。
1.2.3 使用订单状态机
目标:
下单时使用状态机
在支付成功时使用状态机
下单时启动状态机
下单后创建一个新订单,使用状态机的启动方法表示用状态机对该订单的状态开始进行管理。
@Transactional(rollbackFor = Exception.class)
public void add(Orders orders) {boolean save = this.save(orders);if (!save) {throw new DbRuntimeException("下单失败");}//构建快照对象OrderSnapshotDTO orderSnapshotDTO = BeanUtil.toBean(baseMapper.selectById(orders.getId()), OrderSnapshotDTO.class);//状态机启动orderStateMachine.start(null,String.valueOf(orders.getId()),orderSnapshotDTO);
}
支付成功使用状态机
- 定义状态变更动作类
在动作类中更新订单的状态,在动作类中更新订单的状态要比在多处业务代码中对订单状态硬编码要强的多,因为可以在动作类中统一对订单状态进行管理。
除了更新订单状态以外还需要填充订单快照的相关信息,这里主要是支付相关的信息,包括:支付状态、支付时间、支付服务的交易单号、第三方支付的交易单号等。
@Resourceprivate IOrdersCommonService ordersService;/*** 订单支付处理逻辑** @param bizId 业务id* @param bizSnapshot 快照*/@Overridepublic void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {log.info("支付成功事件处理逻辑开始,订单号:{}", bizId);//统一对订单状态进行更新 未支付变为派单中OrderUpdateStatusDTO orderUpdateStatusDTO = new OrderUpdateStatusDTO();orderUpdateStatusDTO.setId(bizSnapshot.getId());orderUpdateStatusDTO.setOriginStatus(OrderStatusEnum.NO_PAY.getStatus());orderUpdateStatusDTO.setTargetStatus(OrderStatusEnum.DISPATCHING.getStatus());orderUpdateStatusDTO.setPayStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus());orderUpdateStatusDTO.setTradingOrderNo(bizSnapshot.getTradingOrderNo());orderUpdateStatusDTO.setTransactionId(bizSnapshot.getThirdOrderId());orderUpdateStatusDTO.setPayTime(bizSnapshot.getPayTime());orderUpdateStatusDTO.setTradingChannel(bizSnapshot.getTradingChannel());Integer res = ordersService.updateStatus(orderUpdateStatusDTO);if(res < 1){throw new CommonException("订单号为"+bizId+"的支付事件处理失败");}}
- 在支付成功方法中使用状态机
使用状态机执行支付成功状态变更。
/*** 支付成功, 更新数据库的订单表及其他信息** @param tradeStatusMsg 交易状态消息*/@Override@Transactional(rollbackFor = Exception.class)public void paySuccess(TradeStatusMsg tradeStatusMsg){/* boolean update = lambdaUpdate().eq(Orders::getId, tradeStatusMsg.getProductOrderNo()).eq(Orders::getOrdersStatus,0) //订单状态只能由待支付 才可以变为派单中.set(Orders::getPayStatus, OrderPayStatusEnum.PAY_SUCCESS.getStatus()).set(Orders::getTransactionId, tradeStatusMsg.getTransactionId()).set(Orders::getOrdersStatus, OrderStatusEnum.DISPATCHING.getStatus()) //订单状态变为派单中.set(Orders::getPayTime, LocalDateTime.now()).update();if(!update){throw new CommonException("支付成功,更新"+tradeStatusMsg.getProductOrderNo()+"订单状态为派单中失败");}*///使用状态机将待支付状态变为派单中OrderSnapshotDTO orderSnapshotDTO = OrderSnapshotDTO.builder().payTime(LocalDateTime.now()).tradingOrderNo(tradeStatusMsg.getTradingOrderNo()).tradingChannel(tradeStatusMsg.getTradingChannel()).thirdOrderId(tradeStatusMsg.getTransactionId()).build();orderStateMachine.changeStatus(null,tradeStatusMsg.getProductAppId().toString(), OrderStatusChangeEventEnum.PAYED,orderSnapshotDTO);}
测试
测试成功