【云岚到家】-day10-1-状态机增删查
- 1 订单管理
- 1)订单管理管什么?
- 2 基础设计
- 2.1 订单状态流转
- 1)订单状态流转图
- 2)订单状态
- 3)服务单状态
- 2.2 数据库设计
- 1)表设计
- 2)分库分表
- 2.3 状态机设计
- 1)订单状态机
- 2)订单快照
- 3 具体业务
- 3.1 创建订单
- 1)订单号生成规则
- 2)价格计算
- 3)优惠券核销(分布式事务)
- 4)防止订单重复提交
- 3.2 订单支付
- 1)对接第三方支付接口
- 2)如何保证接口安全性?
- 3)支付服务的设计
- 4)对接支付服务
- 3.3 查询订单
- 1)单条订单查询优化方案
- 2)用户端订单列表优化方案
- 3)运营端订单列表优化方案
- 3.4 取消订单
- 3.4.1 取消订单技术方案
- 1)需求分析
- 2)技术分析
- 3)策略模式
- 4)取消订单使用策略模式
- 3.4.2 取消订单优化
- 1)使用策略模式实现取消订单
- 2)测试
- 3.5 删除订单
- 1)需求分析
- 2)技术方案分析
- 3)阅读代码
- 3.6 服务单管理
- 1)需求分析
- 2)测试
- 3)阅读代码
1 订单管理
1)订单管理管什么?
前边我们完成了预约下单业务的开发,顾客购买家政服务需要在平台预约下单,订单是指顾客或客户对商家或服务提供者发出的购买请求或需求的记录,与订单相关的业务都属于订单管理的范畴,在一些大的平台中订单管理是一个独立的系统,在一些小的平台中订单管理是一个独立的模块。不管是独立的系统还是模块,订单管理都负责处理和管理与订单相关的各种业务。
订单管理涵盖以下内容:
- **订单创建:**允许用户创建新订单,选择商品、商品数量、支付方式等。
- **订单查询:**提供用户查询订单的功能,可以按订单状态、时间范围等条件进行筛选。
- **订单详情:**显示订单的详细信息,包括商品清单、价格、运费、收货地址等。
- **订单支付:**提供多种支付方式,支持用户完成订单支付操作。
- **订单状态管理:**管理订单的不同状态,如待支付、已支付、已发货、已完成、已取消等。
- **库存管理:**在订单创建时,要检查商品库存是否充足,成功支付后要扣减库存。
- **价格计算:**根据用户选择的商品、优惠券、运费等信息计算订单总金额。
- **优惠券管理:**允许用户使用优惠券,系统需要验证优惠券的有效性,并计算折扣金额。
- **物流管理:**记录订单的物流信息,包括快递公司、快递单号等,方便用户追踪物流信息。
- **售后服务:**提供用户申请退款、退货、换货等售后服务,需要有相应的审核和处理流程。
- **评价和评论:**允许用户对已完成的订单进行评价和评论,提供用户反馈。
- **订单导出:**提供导出订单数据的功能,以便进一步的分析和报表生成。
- **定时任务:**处理订单超时未支付、自动确认收货等定时任务。
- **订单提醒:**发送短信、邮件或推送通知,提醒用户订单状态变更、支付成功等。
- **统计分析:**对订单数据进行统计和分析,为业务决策提供参考。
- **历史订单:**通过冷热分离对已经完成的历史订单进行单独管理,提高热数据的处理效率。
在以上内容中有一些属于订单管理本身的业务,有一些属于与订单管理存在交互的业务,我们的重点放在订单管理本身的业务,如下图:
以上模块中服务管理是本项目特有的,服务人员或机构抢到订单后根据预约时间上门进行服务,服务管理模块是对家政服务的过程进行跟踪管理,服务人员抢单成功生成服务单,服务管理就是对服务单的管理。
2 基础设计
如何对订单模块进行设计,通常从基础设计开始,本节从订单状态、订单数据库等几个方面入手进行设计。
2.1 订单状态流转
产品经理通常以订单的生命周期为线索进行管理,首先根据业务梳理订单的状态流转。
1)订单状态流转图
下图是产品经理设计的订单的状态流转图:
订单状态用黑色表示,订单相关的支付状态为紫色表示,订单相关的服务单状态用绿色表示。
订单状态:0:待支付,100:派单中,200:待服务,300:服务中,500:订单完成,600:已取消,700:已关闭。
支付状态:2:未支付,4:支付成功
服务单状态:0:待分配,1:待服务,2:服务中,3:服务完成,4:取消
2)订单状态
订单状态共有7种,如下图:
待支付:用户下单成功,该订单的初始状态为待支付。
派单中:用户支付成功后订单的状态由待支付变为派单中。
待服务:服务人员或机构抢单成功订单的状态由派单中变为待服务。
服务中:服务人员开始服务,订单状态变为服务中。
订单完成:服务人员完成服务,订单状态变为订单完成。
已取消:订单是待支付状态时用户取消订单,订单状态变为已取消。
已关闭:订单已支付状态下取消订单后订单状态变为已关闭。
3)服务单状态
服务人员抢单成功或系统派单成功将生成订单对应的服务单,服务单状态如下:
待分配:机构抢单成功后服务单的初始状态
待服务:服务人员抢单成功后服务单的初始状态
服务中:服务人员现场开始服务后服务单的状态
服务完成:服务人员服务完成后服务单的状态
已取消:用户取消订单或运营人员取消订单后的服务单状态
2.2 数据库设计
1)表设计
在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构,由于本项目中一个订单中只包括一种家政服务,所以无须记录订单明细的信息只设计订单表。
详细的订单表设计方法参考第四章相关内容。
create table `jzo2o-orders`.orders
(id bigint not null comment '订单id'constraint `PRIMARY`primary key,user_id bigint not null comment '订单所属人',serve_type_id bigint null comment '服务类型id',serve_type_name varchar(50) null comment '服务类型名称',serve_item_id bigint not null comment '服务项id',serve_item_name varchar(50) null comment '服务项名称',serve_item_img varchar(255) null comment '服务项图片',unit int null comment '服务单位',serve_id bigint not null comment '服务id',orders_status int not null comment '订单状态,0:待支付,100:派单中,200:待服务,300:服务中,400:待评价,500:订单完成,600:已取消,700:已关闭',pay_status int null comment '支付状态,2:待支付,4:支付成功',refund_status int null comment '退款状态 1退款中 2退款成功 3退款失败',price decimal(10, 2) not null comment '单价',pur_num int default 1 not null comment '购买数量',total_amount decimal(10, 2) not null comment '订单总金额',real_pay_amount decimal(10, 2) not null comment '实际支付金额',discount_amount decimal(10, 2) not null comment '优惠金额',city_code varchar(20) not null comment '城市编码',serve_address varchar(255) not null comment '服务详细地址',contacts_phone varchar(20) not null comment '联系人手机号',contacts_name varchar(255) not null comment '联系人姓名',serve_start_time datetime not null comment '服务开始时间',lon double(10, 5) null comment '经度',lat double(10, 5) null comment '纬度',pay_time datetime null comment '支付时间',evaluation_time datetime null comment '评价时间',trading_order_no bigint null comment '支付服务交易单号',transaction_id varchar(50) null comment '第三方支付的交易号',refund_no bigint null comment '支付服务退款单号',refund_id varchar(50) null comment '第三方支付的退款单号',trading_channel varchar(50) null comment '支付渠道',display int default 1 null comment '用户端是否展示,1:展示,0:隐藏',sort_by bigint null comment '排序字段,serve_start_time秒级时间戳+订单id后六位',create_time datetime default CURRENT_TIMESTAMP not null,update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
)comment '订单表' charset = utf8mb4;
- 服务单表设计
服务单表记录了服务人员进行家政服务的过程。
表结构如下:
-- 服务任务
-- auto-generated definition
create table orders_serve_0
(id bigint not null comment '任务id'primary key,user_id bigint null comment '属于哪个用户',serve_provider_id bigint not null comment '服务人员或服务机构id',serve_provider_type int null comment '服务者类型,2:服务端服务,3:机构端服务',institution_staff_id bigint null comment '机构服务人员id',orders_id bigint null comment '订单id',orders_origin_type int not null comment '订单来源类型,1:抢单,2:派单',city_code varchar(50) not null comment '城市编码',serve_type_id bigint not null comment '服务分类id',serve_start_time datetime null comment '预约时间',serve_item_id bigint not null comment '服务项id',serve_status int not null comment '任务状态',settlement_status int default 0 not null comment '结算状态,0:不可结算,1:待结算,2:结算完成',real_serve_start_time datetime null comment '实际服务开始时间',real_serve_end_time datetime null comment '实际服务完结时间',serve_before_imgs json null comment '服务前照片',serve_after_imgs json null comment '服务后照片',serve_before_illustrate varchar(255) null comment '服务前说明',serve_after_illustrate varchar(255) null comment '服务后说明',cancel_time datetime null comment '取消时间,可以是退单,可以是取消时间',orders_amount decimal(10, 2) null comment '订单金额',pur_num int null comment '购买数量',create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',sort_by bigint null comment '排序字段(serve_start_time(秒级时间戳)+订单id(后6位))',display int default 1 null comment '服务端/机构端是否展示,1:展示,0:隐藏',is_deleted int default 0 null comment '是否是逻辑删除',update_by bigint null comment '更新人'
)comment '服务任务' charset = utf8mb4;
2)分库分表
为了解决日益增长的订单数据对系统造成的瓶颈,在系统架构时设计分库分表的技术方案。
对订单表、服务单表进行分库分表。
订单表分库分表方案:
分库方案:
设计三个数据库,根据用户id哈希,分库表达式为:db_用户id % 3
参考历史经验,前期设计三个数据库,每个数据库使用主从结构部署,可以支撑项目3年左右的运行,虽然哈希存在数据迁移问题,在很长一段时间也不用考虑这个问题。
分表方案:
根据订单范围分表,0—1500万落到table_0,1500万—3000万落到table_1,依次类推。根据范围分表不存在数据库迁移问题,方便系统扩容。
分表表达式:orders_${(int)Math.floor(id % 10000000000 / 15000000)}
整体方案如下图:
服务单表分库分表方案:
分库方案:
设计三个数据库, 根据服务人员id哈希,分库表达式:jzo2o-orders-${serve_provider_id % 3}
分表方案:
根据订单范围分表,0—1500万落到table_0,1500万—3000万落到table_1,依次类推。
orders_serve_${(int)Math.floor(id % 10000000000 / 15000000)}
2.3 状态机设计
1)订单状态机
当订单的状态非常多且变化关系复杂时就非常有必要使用状态机来管理订单状态,这样可以避免代码中对状态变更的硬编码,提高系统的可扩展可维护性。
本项目基于状态机设计模式开发了状态机组件,状态机设计模式描述了一个对象在内部状态发生变化时如何改变其行为,将状态之间的变更定义为事件,将事件暴露出去,通过执行状态变更事件去更改状态,这是状态机设计模式的核心内容。
- 设计状态机表
在状态机表中记录订单的当前状态。
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;
在状态机表中记录订单的状态,状态机表也进行分库分表
分库方案:
设计三个数据库, 按db_shard_id哈希决定数据库,分库表达式:jzo2o-orders-${db_shard_id % 3},db_shard_id即用户的id
分表方案:
同订单的分表方案,按范围分表,分表表达式为:biz_snapshot_${(int)Math.floor((Long.valueOf(biz_id)) % 10000000000 / 15000000)}
- 首先定义订单状态枚举类
package com.jzo2o.orders.base.enums;import com.jzo2o.statemachine.core.StatusDefine;
import lombok.AllArgsConstructor;
import lombok.Getter;@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;}
}
- 定义状态变更事件枚举类
package com.jzo2o.orders.base.enums;import com.jzo2o.statemachine.core.StatusChangeEvent;
import lombok.AllArgsConstructor;
import lombok.Getter;@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.NO_EVALUATION, "完成服务", "complete_serve"),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_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;
}
- 定义事件变更动作类
拿order_payed事件对应的动作类说明,如下代码:
动作类的bean名称为"状态机名称_事件名称",例如下边的动作类bean的名称为order_payed,表示order状态机的payed事件。
根据事件名称找到事件的定义,如下:
PAYED(OrderStatusEnum.NO_PAY, OrderStatusEnum.DISPATCHING, “支付成功”, “payed”)
通过事件的定义可知 原始状态为OrderStatusEnum.NO_PAY(未支付),目标状态为OrderStatusEnum.DISPATCHING(派单中),支付成功事件执行后将从原始状态改为目标状态。
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;@Slf4j
@Component("order_payed")
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {@Resourceprivate IOrdersCommonService ordersService;/*** 订单支付处理逻辑** @param bizId 业务id* @param statusChangeEventEnum 状态变更事件* @param bizSnapshot 快照*/@Overridepublic void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {log.info("支付事件处理逻辑开始,订单号:{}", bizId);// 修改订单状态和支付状态OrderUpdateStatusDTO orderUpdateStatusDTO = OrderUpdateStatusDTO.builder().id(Long.valueOf(bizId)).originStatus(OrderStatusEnum.NO_PAY.getStatus()).targetStatus(OrderStatusEnum.DISPATCHING.getStatus()).payStatus(OrderPayStatusEnum.PAY_SUCCESS.getStatus()).payTime(LocalDateTime.now()).tradingOrderNo(bizSnapshot.getTradingOrderNo()).transactionId(bizSnapshot.getThirdOrderId()).tradingChannel(bizSnapshot.getTradingChannel()).build();int result = ordersService.updateStatus(orderUpdateStatusDTO);if (result <= 0) {throw new DbRuntimeException("支付事件处理失败");}}}
其它动作类在jzo2o-orders-base工程下:
- 使用订单状态机
首先定义订单状态机类
AbstractStateMachine状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类,比如我们实现订单状态机就需要继承AbstractStateMachine抽象类。
package com.jzo2o.orders.base.config;@Component
public class OrderStateMachine extends AbstractStateMachine<OrderSnapshotDTO> {public OrderStateMachine(StateMachinePersister stateMachinePersister, BizSnapshotService bizSnapshotService, RedisTemplate redisTemplate) {super(stateMachinePersister, bizSnapshotService, redisTemplate);}/*** 设置状态机名称** @return 状态机名称*/@Overrideprotected String getName() {return "order";}@Overrideprotected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) {}/*** 设置状态机初始状态** @return 状态机初始状态*/@Overrideprotected OrderStatusEnum getInitState() {return OrderStatusEnum.NO_PAY;}}
下边通过OrderStateMachine 去使用状态机:
启动状态机:调用OrderStateMachine的start()方法启动一个订单的状态机,表示此订单的状态交由状态机管理并且设置初始状态
//启动状态机,指定订单id
String start = orderStateMachine.start("101");
log.info("返回初始状态:{}", start);
变更状态:
调用OrderStateMachine的changeStatus()方法通过状态变更事件去变更状态,自动执行状态变更事件的动作方法,最终更新订单的状态。
//状态变更,指定状态变更事件
orderStateMachine.changeStatus("101",OrderStatusChangeEventEnum.PAYED);
执行此方法:
state_persister表中101订单的状态变更为DISPATCHING。
biz_snapshot表多了一条101号订单的快照信息。
2)订单快照
- 定义快照类
订单快照是订单变化瞬间的状态及相关信息。
通过订单快照可以查询订单当前状态下的信息,以及历史某个状态下的订单信息。
快照基础类型是StateMachineSnapshot,如果我们要实现订单快照则需要定义一个订单快照类OrderSnapshotDTO 去继承StateMachineSnapshot类型,代码如下:
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;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSnapshotDTO extends StateMachineSnapshot {
....
- 保存订单快照
在启动状态机时指定快照:
/*** 状态机初始化,并保存业务快照,快照分库分表** @param dbShardId 分库键* @param bizId 业务id* @param bizSnapshot 业务快照* @return 初始化状态代码*/
public String start(Long dbShardId, String bizId, T bizSnapshot) {return start(dbShardId, bizId, initState, bizSnapshot);
}
在变更状态时指定新的快照:
/*** 变更状态并保存快照,快照分库分表** @param dbShardId 分库键* @param bizId 业务id* @param statusChangeEventEnum 状态变换事件* @param bizSnapshot 业务数据快照(json格式)*/
public void changeStatus(Long dbShardId, String bizId, StatusChangeEvent statusChangeEventEnum, T bizSnapshot) {
...
}
- 使用订单快照
在查询订单详情时通过查询订单快照,并且对快照信息进行缓存提高查询性能。
下边的代码是状态机提供的查询快照的方法:
/*** 获取当前状态的快照缓存** @param bizId 业务id* @return 快照信息*/
public String getCurrentSnapshotCache(String bizId) {//先查询缓存,如果缓存没有就查询数据库然后存缓存String key = "JZ_STATE_MACHINE:" + name + ":" + bizId;Object object = redisTemplate.opsForValue().get(key);if (ObjectUtil.isNotEmpty(object)) {return object.toString();}String bizSnapshot = getCurrentSnapshot(bizId);redisTemplate.opsForValue().set(key, bizSnapshot, 30, TimeUnit.MINUTES);return bizSnapshot;
}
在查询订单详情方法(部分代码)中调用状态机的查询快照方法:
/*** 根据订单id查询** @param id 订单id* @return 订单详情*/
@Override
public OrderResDTO getDetail(Long id) {//从快照中查询订单数据String jsonResult = orderStateMachine.getCurrentSnapshotCache(String.valueOf(id));OrderSnapshotDTO orderSnapshotDTO = JSONUtil.toBean(jsonResult, OrderSnapshotDTO.class);...
}
3 具体业务
3.1 创建订单
完成创建订单的设计与开发需要从下边几个方面入手。
1)订单号生成规则
常见的订单号生成规则
- 自增数字序列
使用数据库的自增主键或者其他递增的数字序列作为订单号的一部分。例如,订单号可以是"202310280001",其中"20231028"表示日期,"0001"是自增的订单序号。
- 时间戳+随机数
将年月日时分秒和一定范围内的随机数组合起来。例如,订单号可以是"20181028124523" + “1234”,其中"20181028124523"表示日期和时间,"1234"是随机生成的数字。
- 订单类型+日期+序号
将订单类型(例如"01"表示普通订单,“02"表示VIP订单等)、日期和序号组合起来。例如,订单号可以是"0101028100001”,其中"01"表示订单类型,"20181028"表示日期,"00001"是序号。
- 分布式唯一ID生成器
使用分布式唯一ID生成器(例如Snowflake算法)生成全局唯一的ID作为订单号。这种方法保证了在分布式系统中生成的订单号的唯一性和有序性。
本项目订单号生成规则如下:
19位:2位年+2位月+2位日+13位序号
例如:2311011000000000001
实现方案:
1、前6位通过当前时间获取。
2、后13位通过Redis的INCR 命令实现。
2)价格计算
分两种情况:
未使用优惠券:
实际支付金额=订单总金额
订单总金额=服务单价 * 数量
使用优惠券:
实际支付金额=订单总金额-优惠金额。
订单总金额=服务单价 * 数量
优惠金额:调用优惠券服务接口进行计算,分两种情况:满减和折扣。
3)优惠券核销(分布式事务)
- 分布式事务解决方案
如果使用了优惠券需要请求优惠券服务的核销接口进行优惠券核销。
下单与优惠券核销组成分布式事务,进行分布式事务控制,根据需求分布式事务控制满足AP,即强调的是可用性,允许出现短暂不一致,最终达到数据一致性。
如何实现AP?
- 使用MQ或定时任务,一方先成功另一方最终成功
- 使用seata,一方成功另一个方失败,成功方进行回滚
根据需求,订单创建成功可能存在优惠券被使用的问题(虽然可能性极低),所以使用第二种方案,使用seata进行分布式事务控制。
- Seata控制分布式事务
执行流程如下:
Seata事务管理中有三个重要的角色:
- **TC(Transaction Coordinator) -事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。
- **TM (Transaction Manager) -事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- **RM (Resource Manager) -资源管理器:**管理分支事务处理的资源,与TC交互注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
说明:
- 事务管理器(TM)开启全局事务
- 事务管理器(TM)请求RM执行分支事务
下单RM:先向事务协调器(TC)注册下单分支事务,并执行sql(记录undo log),并向TC报告事务状态
undo log:用于回滚事务使用,下单sql是插入记录,根据undo log会生成反向sql删除记录,通过执行反向sql实现回滚事务。
优惠券RM:先向事务协调器(TC)注册优惠券核销分支事务,并执行sql(记录undo log),并向TC报告事务状态
-
事务管理器(TM)请求TC提交全局事务
-
TC检查分支事务状态,如果发现都成功则请求每个RM提交事务(删除undo log),如果发现其中有失败记录则请求每个RM回滚事务,RM回滚事务的方法是通过undo log执行提交事务的反向sql。
-
Seata的使用方法
第一步安装TC事务协调器:参考第八章 “优惠券核销事务控制实现”
启动命令:docker start seata-server
第二步为每个RM创建undo_log表:
在订单数据库和优惠券数据库分别创建undo_log表。
第三步在Service方法中使用@GlobalTransactional开启全局事务。
@GlobalTransactional
public void addWithCoupon(Orders orders, Long couponId) {
...
}
4)防止订单重复提交
什么情况下会重复提交订单?
以下情况会导致重复提交相同的订单:
网络延迟或不稳定: 当用户点击提交按钮后,由于网络延迟或不稳定,前端可能在等待服务端响应时,用户误以为操作未成功,多次点击提交按钮。
重复点击按钮: 用户可能会因为页面加载缓慢或其他原因感到不耐烦,多次点击提交按钮,导致重复提交订单。
页面刷新后的再次提交: 用户在订单提交后,如果刷新页面,浏览器可能会重新发送上一次的表单提交请求,导致订单的重复提交。
如何防止重复提交相同的订单?
通常需要在前端和后端共同去完成:
- 前端防重复提交
禁用提交按钮: 在用户点击提交按钮后,立即将按钮禁用,防止用户多次点击。
显示加载中状态: 提交按钮点击后,显示加载中状态,防止用户再次点击。
- 后端防重复提交
使用分布式锁:以用户id+服务id作为分布式锁锁定下单接口10秒,10秒内该用户不能再次下同一个服务的订单。
3.2 订单支付
订单支付功能的设计与开发从以下几个方面入手
1)对接第三方支付接口
通常项目会开发独立的支付服务对接第三方支付接口,如:微信、支付宝、聚合支付平台等。对于小程序支付需要对接微信的小程序支付接口,对于APP支付和PC网站支付可以对接微信、支付宝或第三方聚合支付平台。
根据授课的需求项目对接的是扫码支付接口,但一定要注意如果项目使用小程序支付一定要对接微信的小程序支付接口。
当然,不管对接是扫码支付还是小程序支付 其方法都是大同小异。
下边总结对接第三方支付接口有哪些:
- 下单接口
业务系统请求第三方支付平台下单,第三方支付平台创建本次支付的交易订单。
- 支付通知
第三方支付平台将支付结果通知给业务系统。
- 支付结果查询
业务系统主动请求第三方支付平台查询支付结果,可以根据业务系统的订单也可以根据第三方支付平台的订单去查询。
- 关闭订单
业务系统请求第三方支付平台关闭订单后此订单将无法继续支付。
- 退款申请
业务系统请求第三方支付平台申请退款,申请退款需要请求第三方支付平台的订单号,可以部分退款,分多次退款,但退款金额不会大于支付金额。
- 申请交易账单
业务系统请求第三方支付平台申请交易账单 的目的是为了进行对账,即业务系统自己记录的订单和金额与第三方支付平台记录的订单是否一致。
2)如何保证接口安全性?
第三方支付平台除了使用https协议进行加密传输以外,还通过接口签名以及敏感参数加密的方式保证接口的安全性。
拿微信支付接口为例,请求任何微信支付接口都需要对请求参数进行签名,微信会进行验签,如果通过才会进行业务处理。
详细参见:https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-generation.html
- 什么是签名和验签
签名是对原始数据通过签名算法生成的一段数据(签名串),用于证明数据的真实性和完整性。签名通常使用密钥进行生成,这个密钥可以是对称密钥或非对称密钥。
验签是对签名串进行验证的过程,用于确认数据的真实性和完整性。验签的过程通常使用与签名过程中相对应的公钥进行解密。
签名和验签是为了防止内容被篡改。
签名和验签是为了防止内容被篡改。
参考微信支付的文档,签名方式如下:
第一步生成签名串:
签名串一共有五行,每一行为一个参数。结尾以\n
(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n
结束,也需要附加一个\n
。
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n示例:
GET\n
2/v3/certificates\n
31554208460\n
4593BEC0C930BF1AFEB40B4A08C8FB242\n
5\n
第二步计算签名值
使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
第三步请求设置http头
通过HTTP Authorization头来传递签名信息,如下:
Authorization: 认证类型 签名信息
示例:
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"
微信支付收到请求根据请求头Authorization的内容进行验签,过程如下:
接收签名和原始数据: 接收到发送方发送的签名和原始数据。
生成SHA-256摘要: 对接收到的原始数据应用 SHA-256 哈希函数,生成摘要。
使用发送方的公钥进行验签: 通常签名是通过发送方的私钥生成的,验证方使用发送方的公钥来验证签名。
比较摘要: 将接收到的摘要与使用公钥运算得到的摘要进行比较。如果一致,说明签名有效,数据未被篡改。
同样,如果微信支付通知支付结果给业务系统,业务系统也需要使用SHA256 with RSA进行验签。
参考:https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-verification.html
- 加密与解密
加密是将原始的、可读的数据(称为明文)通过某种算法和密钥转换成不可读的数据(称为密文)。加密的目的是防止未经授权的访问者能够理解或识别被加密的信息。加密算法通常基于密钥,有对称加密和非对称加密两种主要类型。
- 对称加密: 使用相同的密钥进行加密和解密。常见的对称加密算法包括 AES、DES、3DES。
- 非对称加密: 使用一对密钥,包括公钥和私钥,公钥用于加密,私钥用于解密,或者私钥用于加密,公钥用于解密。常见的非对称加密算法包括 RSA、ECC。
解密是加密的逆过程,即将密文还原为明文。只有持有正确密钥的人或系统能够进行解密操作。解密的目的是还原加密前的原始信息,使其能够被理解和使用。
加密与解密是为了防止内容被泄露,保证内容的机密性。
为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密,商户收到报文后,要解密出明文,APIv3密钥是解密时使用的对称密钥。参考:https://pay.weixin.qq.com/docs/merchant/development/interface-rules/certificate-callback-decryption.html
3)支付服务的设计
关于支付服务的设计详细参考第四章支付服务章节,下边总结要点。
- 使用SDK与微信支付对接
SDK封装了签名、验签、加密及解密的过程,使用SDK开发更高校。
项目使用的SDK是wechatpay-apache-httpclient,地址: https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
- 表设计
支付服务核心的表有支付渠道表、交易单表、退款记录表。
下边是数据流:
说明:
支付接口:收到支付请求后请求第三方支付的下单接口,并向交易单表新增记录。
查询交易结果接口:请求第三方支付的查询支付结果并更新交易单表的支付状态。
接收第三方通过支付结果:更新交易单表的支付状态。
退款接口:新增退款记录
更新退款状态:请求第三方退款结果查询接口查询退款状态,并更新退款状态。
具体的表结构参考:第四章 理解支付服务的设计。
- 接口设计
下单接口(生成二维码)
查询支付结果
退款接口
- 支付服务防止重复支付设计
重复支付是一个订单客户支付多次,造成重复支付。
如果项目实现的是扫码支付,可能存在重复支付的问题,通过以下方式去避免重复支付:
1、同一个订单同一个支付渠道只生成一个支付二维码。
2、在请求第三方支付下单使用分布式锁控制不会重复请求第三方下单。
3、切换支付渠道时先关闭原渠道的交易单再生成新渠道的交易单。
4、使用定时任务每天扫描交易单表,如果存在多个支付成功的交易单则进行自动退款。
4)对接支付服务
使用支付服务的好处是支付服务与微信、支付宝等第三方平台一一对接,业务系统只需要对接支付服务即可,提高对接的效率。
首先支付服务配置支付渠道。
jzo2o-trade
.pay_channel是支付渠道表,默认提供微信、支付宝的支付参数。
其次请求支付服务的下单接口,如果是扫码支付则需要请求支付服务生成二维码。
最后通过MQ接收支付服务的支付结果通知,或业务系统主动请求支付服务查询支付结果。
3.3 查询订单
1)单条订单查询优化方案
针对单条订单信息查询接口可以使用缓存进行优化。
对于单条订单信息查询接口通过快照查询接口查询订单的详情信息。
参考AbstractStateMachine类的String getCurrentSnapshotCache(String bizId)快照查询方法,将快照信息缓存到 redis提供查询效率。
根据订单Id查询缓存信息,先从缓存查询如果缓存没有则查询快照表的数据然后保存到缓存中。缓存设置了过期时间是30分钟。
当订单状态变更,此时订单最新状态的快照有变更,会删除快照缓存,当再次查询快照时从数据库查询最新的快照信息进行缓存。
2)用户端订单列表优化方案
使用滚动查询,一次查询指定数量的记录,不用进行count查询,省去count查询的消耗。
首先在订单表使用排序字段sort_by作为滚动ID,滚动ID是一种递增的序列号,是按开始服务时间计算生成。
然后根据查询条件查询符合条件的订单ID,这里使用覆盖索引优化的方法。
最后对订单信息进行缓存,使用redis的hash结构存储订单信息。
保证缓存一致性:
当用户创建新订单删除该用户在redis的hash结构中的数据。
当用户的订单状态发生变更则删除redis中hash结构中的订单数据库。
保证缓存一致性的代码在状态机中实现:
3)运营端订单列表优化方案
运营端订单列表由于访问量不大这里无需使用缓存,实现方案是首先通过覆盖索引查询符合条件的订单ID,再根据订单ID查询聚集索引拿到数据。
我们根据查询条件创建的联合字段索引是非聚集索引,先从非聚集索引中查询到符合条件的ID,再根据订单ID从聚集索引中查找数据。
3.4 取消订单
3.4.1 取消订单技术方案
1)需求分析
在第四章我们实现了取消订单功能,具体的需求如下:
待支付状态下取消订单:
更改订单的状态为已取消。
订单已支付,状态为待派单时取消订单:
更改订单的状态为已关闭。
请求支付服务自动退款。
完整的取消订单的需求如下图:
订单在不同状态下执行取消订单操作的需求:
待支付:
支付超时系统自动取消。
用户取消订单,订单状态改为已取消。
派单中:
用户和运营人员都可以取消订单,订单状态改为已关闭。
到达服务时间还没有派单成功系统自动取消。
取消订单后自动退款。
删除抢单池记录。
待服务:
用户和运营人员都可以取消订单,订单状态改为已关闭。
取消订单后自动退款。
服务中:
只有运营人员可以取消,订单状态改为已关闭。
取消订单后自动退款。
订单完成:
只有运营人员可以取消,订单状态改为已关闭。
取消订单后自动退款。
2)技术分析
- 根据上边的需求,下边分析订单取消功能。
订单的状态不同取消订单执行的逻辑可能不同
取消待支付的订单:
最终的结果是更改订单状态为已取消,因为没有支付所以不涉及退款。
消派单中的订单:
用户和运营人员都可以取消订单,订单状态改为已关闭。
到达服务时间还没有派单成功系统自动取消。
取消订单后自动退款。
删除抢单池记录。
操作用户不同其权限也不同
普通用户:可取消待支付、派单中、待服务的订单。
运营人员:可取消除待支付状态下的所有订单。
目前的代码如下:
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {//查询订单信息Orders orders = getById(orderCancelDTO.getId());if (ObjectUtil.isNull(orders)) {throw new DbRuntimeException("找不到要取消的订单,订单号:{}",orderCancelDTO.getId());}//将订单中的交易单号信息拷贝到orderCancelDTOorderCancelDTO.setTradingOrderNo(orders.getTradingOrderNo());orderCancelDTO.setRealPayAmount(orders.getRealPayAmount());//订单状态Integer ordersStatus = orders.getOrdersStatus();if(OrderStatusEnum.NO_PAY.getStatus()==ordersStatus){ //订单状态为待支付owner.cancelByNoPay(orderCancelDTO);}else if(OrderStatusEnum.DISPATCHING.getStatus()==ordersStatus){ //订单状态为派单中owner.cancelByDispatching(orderCancelDTO);//新启动一个线程请求退款ordersHandler.requestRefundNewThread(orders.getId());}else{throw new CommonException("当前订单状态不支持取消");}}
根据完整的需求,如何完善上边的代码?
下边这样写是否合适?
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {//查询订单信息Orders orders = getById(orderCancelDTO.getId());if (ObjectUtil.isNull(orders)) {throw new DbRuntimeException("找不到要取消的订单,订单号:{}",orderCancelDTO.getId());}//将订单中的交易单号信息拷贝到orderCancelDTOorderCancelDTO.setTradingOrderNo(orders.getTradingOrderNo());orderCancelDTO.setRealPayAmount(orders.getRealPayAmount());//订单状态Integer ordersStatus = orders.getOrdersStatus();//获取当前用户CurrentUserInfo currentUserInfo = UserContext.currentUser();//用户类型Integer userType = currentUserInfo.getUserType();if(UserType.C_USER==userType){//普通用户取消订单if(OrderStatusEnum.NO_PAY.getStatus()==ordersStatus){ //订单状态为待支付owner.cancelByNoPay(orderCancelDTO);}else if(OrderStatusEnum.DISPATCHING.getStatus()==ordersStatus){ //订单状态为派单中owner.cancelByDispatching(orderCancelDTO);//新启动一个线程请求退款ordersHandler.requestRefundNewThread(orders.getId());}else if(OrderStatusEnum.NO_SERVE.getStatus()==ordersStatus){ //订单状态为待服务//...}else{throw new CommonException("当前订单状态不支持取消");}}else if(UserType.OPERATION==userType){if(OrderStatusEnum.DISPATCHING.getStatus()==ordersStatus) { //订单状态为派单中//...}else if(OrderStatusEnum.NO_SERVE.getStatus()==ordersStatus){//订单状态为待服务}//else if(){}//....}}
3)策略模式
针对上边这种因为不同的场景执行取消订单的逻辑不同可以使用策略模式实现。
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。
策略模式以下部分组成:
抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
具体策略角色:包装了相关的算法和行为。
环境角色:持有一个策略类的引用,最终给客户端调用。
下边是一个例子:
策略接口
package com.jzo2o.orders.manager.strategy;
// 定义策略接口
public interface PaymentStrategy {void pay(BigDecimal amount);
}
具体策略类
package com.jzo2o.orders.manager.strategy;public class CreditCardPayment implements PaymentStrategy {private String cardNumber;public CreditCardPayment(String cardNumber) {this.cardNumber = cardNumber;}@Overridepublic void pay(BigDecimal amount) {System.out.println("信用卡:"+cardNumber+"支付金额:"+amount);}
}public class WeixinPayment implements PaymentStrategy {private String account;public WeixinPayment(String account) {this.account = account;}@Overridepublic void pay(BigDecimal amount) {System.out.println("微信:"+account+"支付金额:"+amount);}
}
环境类
// 环境类,负责维护策略接口
class ShoppingCart {private PaymentStrategy paymentStrategy;public void setPaymentStrategy(PaymentStrategy paymentStrategy) {this.paymentStrategy = paymentStrategy;}public void checkout(BigDecimal amount) {paymentStrategy.pay(amount);}
}
客户端代码
package com.jzo2o.orders.manager.strategy;public class StrategyPatternExample {public static void main(String[] args) {// 创建环境类ShoppingCart shoppingCart = new ShoppingCart();// 选择支付策略--信用卡支付PaymentStrategy creditCardPayment = new CreditCardPayment("1234-5678-9876-5432");shoppingCart.setPaymentStrategy(creditCardPayment);// 进行支付shoppingCart.checkout(new BigDecimal(100));// 切换支付策略,使用微信支付PaymentStrategy weixinPayment = new WeixinPayment("example@example.com");shoppingCart.setPaymentStrategy(weixinPayment);// 进行支付shoppingCart.checkout(new BigDecimal(50));}
}
4)取消订单使用策略模式
学习了策略模式我们可以将取消订单定义为策略接口,针对不同场景下取消订单的逻辑定义为一个一个的策略类,如果哪个场景下的策略有变化只需要修改该策略类即可,如果增加场景也只需要增加策略类。
如下图:
下边阅读代码:
当前使用的是orders工程的dev_02分支,如果没有dev_02分支则需要创建,解压jzo2o-orders-02-0.zip的代码拷贝到dev_02分支下。
- 取消订单策略接口
取消策略接口需要指定用户类型、订单状态及取消信息,根据用户类型和订单状态决定取消订单的逻辑。
package com.jzo2o.orders.manager.strategy;import com.jzo2o.orders.manager.model.dto.OrderCancelDTO;public interface OrderCancelStrategy {/*** 订单取消** @param orderCancelDTO 订单取消模型*/void cancel(OrderCancelDTO orderCancelDTO);
}
OrderCancelDTO 类:
package com.jzo2o.orders.manager.model.dto;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.math.BigDecimal;
import java.time.LocalDateTime;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCancelDTO {/*** 订单id*/private Long id;/*** 用户id*/private Long userId;/*** 当前用户id*/private Long currentUserId;/*** 当前用户名称*/private String currentUserName;/*** 当前用户类型*/private Integer currentUserType;/*** 取消原因*/private String cancelReason;/*** 预约服务开始时间*/private LocalDateTime serveStartTime;/*** 实际支付金额*/private BigDecimal realPayAmount;/*** 城市编码*/private String cityCode;/*** 支付服务交易单号*/private Long tradingOrderNo;
}
- 定义策略类
每个策略类定义的bean名称为“用户类型:订单状态”。
举例:
下边的策略类是普通用户针对待支付的订单进行取消。
@Component(“1:NO_PAY”):定义bean的名称为1:NO_PAY,1表示普通用户,NO_PAY表示待支付订单。
其它策略类也使用此方法定义bean的名称。
下边方法是该策略类针对策略接口的具体实现方法:
取消待支付的订单执行的动作有:更改订单的状态为已取消,添加取消订单记录。
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {//1.校验是否为本人操作if (ObjectUtil.notEqual(orderCancelDTO.getUserId(), orderCancelDTO.getCurrentUserId())) {throw new ForbiddenOperationException("非本人操作");}//2.构建订单快照更新模型OrderSnapshotDTO orderSnapshotDTO = OrderSnapshotDTO.builder().cancellerId(orderCancelDTO.getCurrentUserId()).cancelerName(orderCancelDTO.getCurrentUserName()).cancellerType(orderCancelDTO.getCurrentUserType()).cancelReason(orderCancelDTO.getCancelReason()).cancelTime(LocalDateTime.now()).build();//3.保存订单取消记录OrdersCanceled ordersCanceled = BeanUtil.toBean(orderSnapshotDTO, OrdersCanceled.class);ordersCanceled.setId(orderCancelDTO.getId());ordersCanceledService.save(ordersCanceled);//4.订单状态变更orderStateMachine.changeStatus(orderCancelDTO.getUserId(), orderCancelDTO.getId().toString(), OrderStatusChangeEventEnum.CANCEL, orderSnapshotDTO);
}
- 策略的环境类
在环境类中管理了策略接口
package com.jzo2o.orders.manager.strategy;import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.orders.base.enums.OrderStatusEnum;
import com.jzo2o.orders.base.model.domain.Orders;
import com.jzo2o.orders.manager.service.IOrdersManagerService;
import com.jzo2o.orders.manager.model.dto.OrderCancelDTO;
import com.jzo2o.orders.manager.strategy.OrderCancelStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;@Slf4j
@Component
public class OrderCancelStrategyManager {@Resourceprivate IOrdersManagerService ordersManagerService;//key格式:userType+":"+orderStatusEnum,例:1:NO_PAYprivate final Map<String, OrderCancelStrategy> strategyMap = new HashMap<>();@PostConstructpublic void init() {Map<String, OrderCancelStrategy> strategies = SpringUtil.getBeansOfType(OrderCancelStrategy.class);strategyMap.putAll(strategies);log.debug("订单取消策略类初始化到map完成!");}/*** 获取策略实现** @param userType 用户类型* @param orderStatus 订单状态* @return 策略实现类*/public OrderCancelStrategy getStrategy(Integer userType, Integer orderStatus) {String key = userType + ":" + OrderStatusEnum.codeOf(orderStatus).toString();return strategyMap.get(key);}/*** 订单取消** @param orderCancelDTO 订单取消模型*/public void cancel(OrderCancelDTO orderCancelDTO) {Orders orders = ordersManagerService.queryById(orderCancelDTO.getId());OrderCancelStrategy strategy = getStrategy(orderCancelDTO.getCurrentUserType(), orders.getOrdersStatus());if (ObjectUtil.isEmpty(strategy)) {throw new ForbiddenOperationException("不被许可的操作");}orderCancelDTO.setUserId(orders.getUserId());orderCancelDTO.setServeStartTime(orders.getServeStartTime());orderCancelDTO.setCityCode(orders.getCityCode());orderCancelDTO.setRealPayAmount(orders.getRealPayAmount());orderCancelDTO.setTradingOrderNo(orders.getTradingOrderNo());strategy.cancel(orderCancelDTO);}}
@PostConstruct 是java自带的注解,此注解标记的方法用于在对象创建后依赖注入完成后执行一些初始化操作。
和@PostConstruct对应的还有一个@PreDestroy,是在bean销毁时调用,通常需要做一些释放资源的操作。
在init()方法中取出所有的策略接口实现对象放入strategyMap 中,key为:userType+“:”+orderStatusEnum,即bean的名称,value为对象本身。
查阅 cancel(OrderCancelDTO orderCancelDTO) 取消订单方法:
1、根据用户类型和订单状态取出策略类的对象
2、执行策略对象的cancel(OrderCancelDTO orderCancelDTO) 方法即执行取消订单操作。
3.4.2 取消订单优化
1)使用策略模式实现取消订单
修改取消订单的service方法:
由于取消优惠券核销需要使用分布式事务控制,分布式事务控制影响性能,这里将本地事务提交与分布式事务提交分开编写代码。
@Slf4j
@Service
public class OrdersManagerServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersManagerService {
...
/*** 取消订单** @param orderCancelDTO 取消订单模型*/
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {// 1.有优惠金额的回滚优惠券(当前优惠金额均来自优惠券)Orders orders = getById(orderCancelDTO.getId());if (ObjectUtils.isNull(orders.getDiscountAmount()) || orders.getDiscountAmount().compareTo(BigDecimal.ZERO) > 0) {CouponUseBackReqDTO couponUseBackReqDTO = new CouponUseBackReqDTO();couponUseBackReqDTO.setOrdersId(orderCancelDTO.getId());couponUseBackReqDTO.setUserId(orders.getUserId());//需要取消优惠券核销要使用分布式事务控制owner.cancelWithCoupon(orderCancelDTO, couponUseBackReqDTO);} else {//使用本地事务控制即可owner.cancelWithoutCoupon(orderCancelDTO);}}@Override
@GlobalTransactional
public void cancelWithCoupon(OrderCancelDTO orderCancelDTO, CouponUseBackReqDTO couponUseBackReqDTO) {couponApi.useBack(couponUseBackReqDTO);orderCancelStrategyManager.cancel(orderCancelDTO);
}@Override
@Transactional
public void cancelWithoutCoupon(OrderCancelDTO orderCancelDTO) {orderCancelStrategyManager.cancel(orderCancelDTO);
}
...
2)测试
启动网关
启动jzo2o-orders-manager
启动jzo2o-foundations
启动jzo2o-customer
启动小程序
测试流程:
下单
取消订单
断点跟踪:
在策略环境对象中包括了所有策略实现对象。
继续执行,根据订单状态和用户类型找到符合的策略对象:
3.5 删除订单
1)需求分析
删除订单和取消订单不同,取消订单的目的是终止订单,删除订单是不希望订单信息出现订单列表中。
此需求通常是针对普通用户提供。
用户删除订单后订单信息将不在订单列表显示。
也有平台提供订单回收站查询功能,即查询已经删除的订单。
2)技术方案分析
根据需求,我们在订单表添加逻辑删除标记,删除订单相当于隐藏订单。
在订单表添加字段:display (1:展示,0:隐藏)
删除订单将此字段设置为0。
在订单列表中只查询display 为1的订单信息。
3)阅读代码
- 接口定义
@RestController("consumerOrdersController")
@Api(tags = "用户端-订单相关接口")
@RequestMapping("/consumer/orders")
public class ConsumerOrdersController {... @PutMapping("/hide/{id}")@ApiOperation("订单删除(隐藏)")@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "订单id", required = true, dataTypeClass = Long.class)})public void hide(@PathVariable("id") Long id) {CurrentUserInfo currentUserInfo = UserContext.currentUser();ordersManagerService.hide(id, currentUserInfo.getUserType(), currentUserInfo.getId());}...
- service方法实现
/*** 用户端-订单删除(隐藏)** @param id 订单id* @param userType 用户类型* @param userId 用户id*/
@Override
@Transactional
public void hide(Long id, Integer userType, Long userId) {//1.校验用户类型是否为普通用户if (ObjectUtil.notEqual(userType, UserType.C_USER)) {throw new ForbiddenOperationException("非普通用户不可操作");}//2.校验该操作是否为本人Orders orders = queryById(id);if (ObjectUtil.notEqual(userId, orders.getUserId())) {throw new ForbiddenOperationException("非本人不可操作");}//3.校验订单状态,只能取消状态、关闭状态才能删除if (ObjectUtil.notEqual(OrderStatusEnum.CANCELED.getStatus(), orders.getOrdersStatus()) && ObjectUtil.notEqual(OrderStatusEnum.CLOSED.getStatus(), orders.getOrdersStatus())) {throw new ForbiddenOperationException("订单非取消、关闭状态不可操作");}//4.更新订单显示状态为隐藏displaySetting(id, EnableStatusEnum.DISABLE.getStatus());
}@Override
public void displaySetting(Long id, Integer displayStatus) {LambdaUpdateWrapper<Orders> updateWrapper = Wrappers.<Orders>lambdaUpdate().eq(Orders::getId, id).set(Orders::getDisplay, displayStatus);super.update(updateWrapper);
}
3.6 服务单管理
1)需求分析
- 界面原型
服务人员进入我的订单,查询服务单列表:
点击开始服务,上传服务前的现场照片。
服务人员现场完成服务后进入我的订单,查询待完工的订单。
点击完成服务,上传服务后的现场照片
完成服务的订单在已完成订单中查看
查看订单详情
- 服务单状态
服务人员抢单成功或系统派单成功将生成订单对应的服务单,服务单状态如下:
待服务:服务人员抢单成功生成服务单,服务单的初始状态为待服务。
待分配:机构抢单成功生成服务单,服务单的初始状态为待分配,机构分配服务人员此时状态改为待服务。
服务中:服务人员去现场服务,开始服务后服务单的状态由待服务变为服务中。
服务完成:服务人员完成家政服务,服务单的状态由服务中变为服务完成。
服务取消:用户取消订单或运营人员取消订单后服务单状态改为已取消。
用户取消订单:
待分配—》已取消
待服务—》已取消
运营人员取消订单:
服务中—》已取消
服务完成—》已取消
2)测试
启动jzo2o-orders-manager
启动jzo2o-foundations
启动网关
启动jzo2o-customer
启动jzo2o-publics
启动服务端(前端)
测试流程:
服务人员登录服务端
进入我的订单
开始服务
完成服务
预期结果:
服务人员查看订单详情查看服务单的状态为:已完成
进入数据库查看对应订单的状态为:待评价
3)阅读代码
进入接口文档:http://localhost:11504/orders-manager/doc.html#/home
通过接口查看源码,重点查看以下接口的源码