目录
1 订单退款功能
1.1 需求分析
用户下单成功可以取消订单,在订单的不同状态下去取消订单其执行的逻辑是不同的:
- 待支付状态下取消订单: 更改订单的状态为已取消。
-订单已支付,状态为待派单时取消订单: 更改订单的状态为已关闭,请求支付服务自动退款。
用户在订单列表点击订单信息进入订单详情页面,点击“取消订单”
进入取消订单界面:
选择取消原因,点击“提交”。
1.2 接口分析
前端传入的参数:订单id、取消原因。
响应参数:无,前端根据状态码判断是否成功。
接口名称:取消订单
接口路径:PUT /orders-manager/consumer/orders/cancel
1.3 退款流程分析
根据需求分析,当订单已支付状态为派单中时取消订单后进行自动退款,此时需要调用支付服务的申请退款接口。
流程如下:
取消订单执行如下操作:
1、更新订单状态
待支付状态下取消订单后更新订单状态为“已取消”
派单中状态下取消订单后更新订单状态为“已关闭”
2、保存取消订单记录,记录取消的原因等信息。
3、远程调用支付服务的退款接口申请退款。
取消派单中的订单存在如下问题:
远程调用退款接口操作不放在事务方法中,避免影响数据库性能。
如果远程调用退款接口失败了将无法退款,这个怎么处理?
以上问题采用异步退款的方式来解决:
如下图:
1、使用数据库事务控制,保存以下数据
更新订单状态。
保存取消订单记录表,记录取消的原因等信息。
保存退款记录表。
2、事务提交后先启动一个线程请求支付服务的退款接口(为了及时退款)
3、定时任务扫描退款记录表,对未退款的记录请求支付服务进行退款,退款成功更新订单的退款状态,并删除退款记录。
说明:
第2步的作用为了第一时间申请退款,因为定时任务会有一定的延迟。
第3步的作用是由定时任务去更新退款的状态,因为调用了退款接口只是申请退款了,退款结果可能还没有拿到,通过定时任务再次请求支付服务的退款接口,拿到退款结果。
1.4 表结构设计
订单取消记录表:
create table `jzo2o-orders`.orders_canceled
(
id bigint not null comment '订单id'
constraint `PRIMARY`
primary key,
canceller_id bigint null comment '取消人',
canceler_name varchar(50) null comment '取消人名称',
canceller_type int null comment '取消人类型,1:普通用户,4:运营人员',
cancel_reason varchar(50) null comment '取消原因',
cancel_time datetime null comment '取消时间',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
订单退款记录表:存储 待退款的记录
create table `jzo2o-orders`.orders_refund
(
id bigint not null comment '订单id'
constraint `PRIMARY`
primary key,
trading_order_no bigint null comment '支付服务交易单号',
real_pay_amount decimal(10, 2) null comment '实付金额',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间'
)
1.5 取消未支付订单实现
根据需求,取消订单需要实现两部分功能:
针对未支付订单的取消操作:
- 修改订单的状态为已取消。
- 保存取消订单的记录。
1.5.1 接口开发
Controller层开发
@RestController("operationOrdersController")
@Api(tags = "运营端-订单相关接口")
@RequestMapping("/operation/orders")
public class OperationOrdersController {
@Autowired
private IOrdersManagerService ordersManagerService;
@PutMapping("/cancel")
@ApiOperation("取消订单")
public void cancel(@RequestBody OrderCancelReqDTO orderCancelReqDTO) {
//填充数据
OrderCancelDTO orderCancelDTO = BeanUtil.toBean(orderCancelReqDTO, OrderCancelDTO.class);
CurrentUserInfo currentUserInfo = UserContext.currentUser();
orderCancelDTO.setCurrentUserId(currentUserInfo.getId());
orderCancelDTO.setCurrentUserName(currentUserInfo.getName());
orderCancelDTO.setCurrentUserType(currentUserInfo.getUserType());
ordersManagerService.cancel(orderCancelDTO);
}
}
Service层开发
/**
* 取消订单
* @param orderCancelDTO 取消订单模型
*/
@Override
public void cancel(OrderCancelDTO orderCancelDTO) {
//判断订单是否存在
if(ObjectUtils.isNull(this.getById(orderCancelDTO.getId()))){
throw new CommonException("要取消的订单不存在");
}
Integer ordersStatus = this.getById(orderCancelDTO.getId()).getOrdersStatus();
if(ordersStatus.equals(OrderStatusEnum.NO_PAY.getStatus())){
//对未支付订单进行取消
owner.cancelByNoPay(orderCancelDTO);
}else if(ordersStatus.equals(OrderStatusEnum.DISPATCHING.getStatus())){
//对已支付(派单中)订单进行取消
}else{
throw new CommonException("当前订单状态不支持取消");
}
}
/**
* 未支付状态取消订单
*/
@Transactional(rollbackFor = Exception.class)
public void cancelByNoPay(OrderCancelDTO orderCancelDTO){
//添加订单取消记录
OrdersCanceled ordersCanceled = BeanUtils.toBean(orderCancelDTO,OrdersCanceled.class);
ordersCanceled.setCancellerId(orderCancelDTO.getCurrentUserId());
ordersCanceled.setCancelerName(orderCancelDTO.getCurrentUserName());
ordersCanceled.setCancellerType(orderCancelDTO.getCurrentUserType());
ordersCanceled.setCancelTime(LocalDateTime.now());
canceledService.save(ordersCanceled);
//修改订单状态为已取消
OrderUpdateStatusDTO orderUpdateStatusDTO = new OrderUpdateStatusDTO();
//需要传入 id 原始状态 更新状态
orderUpdateStatusDTO.setId(orderCancelDTO.getId());
orderUpdateStatusDTO.setOriginStatus(OrderStatusEnum.NO_PAY.getStatus());
orderUpdateStatusDTO.setTargetStatus(OrderStatusEnum.CANCELED.getStatus());
Integer res = commonService.updateStatus(orderUpdateStatusDTO);
if(res <= 0){
throw new CommonException("取消订单失败");
}
}
1.5.2 接口测试
先弄个没支付的订单:
点击取消支付:
功能测试完成
1.5 取消已支付订单实现(定时任务+懒加载)
1.5.1 需求分析
如何实现自动取消订单操作呢?
这里最关键有一个定时的要求,从下单开始计算15分钟后取消订单。
如果要分秒不差执行取消订单操作就需要使用定时器每秒去判断是否到达过期时间。
如果订单较多使用多个定时器会耗费CPU。
其实,我们可以不用分秒不差执行取消操作,从用户的角度去思考,通常是用户点击订单查看的时候发现不能支付,因为到达支付过期时间。
所以,我们可以通过定时任务和懒加载方式。
定时任务方式:每分钟将支付过期的订单查询出来进行取消操作。
懒加载方式:当用户查看订单详情时判断如果订单未支付且支付超时,此时触发订单取消操作。
1.5.1.1 实现定时任务取消订单
Service层搜索超时任务
/**
* 查询超时订单id列表
*
* @param count 数量
* @return 订单id列表
*/
@Override
public List<Orders> queryOverTimePayOrdersListByCount(Integer count) {
//根据订单创建时间查询超过15分钟未支付的订单
List<Orders> list = lambdaQuery()
//查询待支付状态的订单
.eq(Orders::getOrdersStatus, OrderStatusEnum.NO_PAY.getStatus())
//小于当前时间减去15分钟,即待支付状态已过15分钟
.lt(Orders::getCreateTime, LocalDateTime.now().minusMinutes(15))
.last("limit " + count)
.list();
return list;
}
定时任务代码
/**
* 自动取消超时订单
*/
@XxlJob(value = "cancelOverTimePayOrder")
public void canalOverTimePayOrder(){
//查出超时订单
List<Orders> orders = ordersCreateService.queryOverTimePayOrdersListByCount(100);
if(CollUtils.isEmpty(orders)){
log.info("没有超时订单");
return;
}
for (Orders order : orders) {
OrderCancelDTO orderCancelDTO = BeanUtils.toBean(order,OrderCancelDTO.class);
orderCancelDTO.setCurrentUserType(UserType.SYSTEM);
orderCancelDTO.setCancelReason("订单超时自动取消");
ordersManagerService.cancel(orderCancelDTO);
}
//改为已取消
}
添加定时任务
测试
测试成功
1.5.1.2 实现懒加载取消订单
在原来查询订单信息的代码加入判断,加入超时未支付则改为取消
Service层改写
/**
* 根据订单id查询
*
* @param id 订单id
* @return 订单详情
*/
@Override
public OrderResDTO getDetail(Long id) {
Orders orders = queryById(id);
//懒加载方式 取消超时订单
orders = canalIfPayOvertime(orders);
OrderResDTO orderResDTO = BeanUtil.toBean(orders, OrderResDTO.class);
return orderResDTO;
}
/**
* 如果支付过期则取消订单
* @param orders
*/
private Orders canalIfPayOvertime(Orders orders){
//创建订单未支付15分钟后自动取消
if(orders.getOrdersStatus().equals(OrderStatusEnum.NO_PAY.getStatus()) && orders.getCreateTime().isBefore(LocalDateTime.now().minusMinutes(15))){
//查询支付结果,如果支付最新状态仍是未支付进行取消订单
OrdersPayResDTO ordersPayResDTO = ordersCreateService.getPayResultFromTradServer(orders.getId());
int payResultFromTradServer = ordersPayResDTO.getPayStatus();
if(payResultFromTradServer != OrderPayStatusEnum.PAY_SUCCESS.getStatus()){
OrderCancelDTO orderCancelDTO = BeanUtils.toBean(orders, OrderCancelDTO.class);
orderCancelDTO.setCurrentUserType(UserType.SYSTEM);
orderCancelDTO.setCancelReason("订单超时,系统自动取消");
cancelByNoPay(orderCancelDTO);
orders = getById(orders.getId());
}
}
return orders;
}
测试
改一下时间
测试成功
1.6 取消已支付订单(派单中的订单)
1.6.1 定义取消派单中订单service
当订单状态为派单中,取消此类订单需要进行退款操作,根据退款流程,需要作以下操作:
- 添加取消订单记录。
- 更新订单状态为“已关闭”。
- 添加退款记录。
定义service方法实现上边3个操作,并且进行事务控制。
/**
* 已支付状态取消订单
*/
@Transactional(rollbackFor = Exception.class)
public void cancelByDispatching(OrderCancelDTO orderCancelDTO) {
//更改订单状态为已关闭
OrderUpdateStatusDTO orderUpdateStatusDTO = new OrderUpdateStatusDTO();
//需要传入 id 原始状态 更新状态
orderUpdateStatusDTO.setId(orderCancelDTO.getId());
orderUpdateStatusDTO.setOriginStatus(OrderStatusEnum.NO_PAY.getStatus());
orderUpdateStatusDTO.setTargetStatus(OrderStatusEnum.CLOSED.getStatus());
Integer res = commonService.updateStatus(orderUpdateStatusDTO);
if(res <= 0){
throw new CommonException("取消订单失败");
}
//插入取消订单记录
OrdersCanceled ordersCanceled = BeanUtils.toBean(orderCancelDTO,OrdersCanceled.class);
ordersCanceled.setCancellerId(orderCancelDTO.getCurrentUserId());
ordersCanceled.setCancelerName(orderCancelDTO.getCurrentUserName());
ordersCanceled.setCancellerType(orderCancelDTO.getCurrentUserType());
ordersCanceled.setCancelTime(LocalDateTime.now());
canceledService.save(ordersCanceled);
//保存退款记录
OrdersRefund ordersRefund = BeanUtils.toBean(orderCancelDTO, OrdersRefund.class);
ordersRefundService.save(ordersRefund);
}
1.6.2 定义定时任务
在OrdersHandler 类中定义定时任务方法。
定时任务根据退款记录去请求第三方支付服务的退款接口,根据退款结果进行处理,如果退款成功将更新订单的退款状态、删除退款记录。
@XxlJob("handleRefundOrders")
public void handleRefundOrders(){
//查询退款记录
List<OrdersRefund> ordersRefunds = ordersRefundService.queryRefundOrderListByCount(100);
if(CollUtils.isEmpty(ordersRefunds)){
log.info("退款定时任务:未查询到订单信息");
return;
}
//遍历退款记录,请求支付服务进行退款
for (OrdersRefund ordersRefund : ordersRefunds) {
requestRefundOrder(ordersRefund);
}
//退款成功则更新订单的退款状态
}
/**
* 请求退款
* @param ordersRefund 退款记录
*/
public void requestRefundOrder(OrdersRefund ordersRefund){
ExecutionResultResDTO executionResultResDTO = null;
//请求支付服务进行退款
try {
executionResultResDTO = refundRecordApi.refundTrading(ordersRefund.getTradingOrderNo(), ordersRefund.getRealPayAmount());\
}catch (Exception e){
e.printStackTrace();
}
//解析退款状态,不是退款中则更新状态
if(ObjectUtils.isNotNull(executionResultResDTO) && executionResultResDTO.getRefundStatus() != OrderRefundStatusEnum.REFUNDING.getStatus()){
//更新订单退款状态
refundOrder(ordersRefund,executionResultResDTO);
}
}
/**
* 更新退款状态
* @param ordersRefund
* @param executionResultResDTO
*/
@Transactional(rollbackFor = Exception.class)
public void refundOrder(OrdersRefund ordersRefund, ExecutionResultResDTO executionResultResDTO) {
//初始状态为退款中
int refundStatus = OrderRefundStatusEnum.REFUNDING.getStatus();
if(executionResultResDTO.getRefundStatus()==OrderRefundStatusEnum.REFUND_SUCCESS.getStatus()){
refundStatus = OrderRefundStatusEnum.REFUND_SUCCESS.getStatus();
}else if(executionResultResDTO.getRefundStatus()==OrderRefundStatusEnum.REFUND_FAIL.getStatus()){
refundStatus = OrderRefundStatusEnum.REFUND_FAIL.getStatus();
}
//如果是退款中状态,程序结束
if (ObjectUtil.equal(refundStatus, OrderRefundStatusEnum.REFUNDING.getStatus())) {
return;
}
//更新订单表的退款状态 支付服务的退款状态、第三方支付平台的退款单号
LambdaUpdateWrapper<Orders> updateWrapper = new LambdaUpdateWrapper<Orders>().eq(Orders::getId, ordersRefund.getId())
.ne(Orders::getRefundStatus, refundStatus)
.set(Orders::getRefundStatus, refundStatus)
.set(Orders::getRefundNo, executionResultResDTO.getRefundNo())
.set(Orders::getRefundId, executionResultResDTO.getRefundId());
int update = ordersMapper.update(null, updateWrapper);
if(update > 0){
//清除退款记录
ordersRefundService.removeById(ordersRefund.getId());
}
}
1.6.3 功能测试
首先下个单支付下:
点击退款
退款成功