mall + pay:

1. 支付订单的状态同步 Job
2. 支付订单的过期关闭 Job
This commit is contained in:
YunaiV 2023-07-22 15:48:45 +08:00
parent 348d073718
commit 15cca8f3de
7 changed files with 247 additions and 7 deletions

View File

@ -33,6 +33,16 @@ public enum PayOrderStatusRespEnum {
return Objects.equals(status, SUCCESS.getStatus());
}
/**
* 判断是否已退款
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isRefund(Integer status) {
return Objects.equals(status, REFUND.getStatus());
}
/**
* 判断是否支付关闭
*

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
@ -23,4 +24,10 @@ public interface PayOrderExtensionMapper extends BaseMapperX<PayOrderExtensionDO
return selectList(PayOrderExtensionDO::getOrderId, orderId);
}
default List<PayOrderExtensionDO> selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) {
return selectList(new LambdaQueryWrapper<PayOrderExtensionDO>()
.eq(PayOrderExtensionDO::getStatus, status)
.ge(PayOrderExtensionDO::getCreateTime, minCreateTime));
}
}

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
@ -52,4 +53,10 @@ public interface PayOrderMapper extends BaseMapperX<PayOrderDO> {
.eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status));
}
default List<PayOrderDO> selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) {
return selectList(new LambdaQueryWrapper<PayOrderDO>()
.eq(PayOrderDO::getStatus, status)
.lt(PayOrderDO::getExpireTime, expireTime));
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.pay.job.order;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付订单的过期 Job
*
* 支付超过过期时间时支付渠道是不会通知进行过期所以需要定时进行过期关闭
*
* @author 芋道源码
*/
@Component
@TenantJob
public class PayOrderExpireJob implements JobHandler {
@Resource
private PayOrderService orderService;
@Override
public String execute(String param) {
int count = orderService.expireOrder();
return StrUtil.format("支付过期 {} 个", count);
}
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.pay.job.order;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 支付订单的同步 Job
*
* 由于支付订单的状态是由支付渠道异步通知进行同步考虑到异步通知可能会失败小概率所以需要定时进行同步
*
* @author 芋道源码
*/
@Component
@TenantJob
public class PayOrderSyncJob implements JobHandler {
/**
* 同步创建时间在 N 分钟之前的订单
*
* 为什么同步 10 分钟之前的订单
* 因为一个订单发起支付到支付成功大多数在 10 分钟内需要保证轮询到
* 如果设置为 3060 或者更大时间范围会导致轮询的订单太多影响性能当然你也可以根据自己的业务情况来处理
*/
private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10);
@Resource
private PayOrderService orderService;
@Override
public String execute(String param) {
LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE);
int count = orderService.syncOrder(minCreateTime);
return StrUtil.format("同步订单 {} 个", count);
}
}

View File

@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -105,4 +106,19 @@ public interface PayOrderService {
*/
PayOrderExtensionDO getOrderExtension(Long id);
/**
* 同步订单的支付状态
*
* @param minCreateTime 最小创建时间
* @return 同步到已支付的订单数量
*/
int syncOrder(LocalDateTime minCreateTime);
/**
* 将已过期的订单状态修改为已关闭
*
* @return 过期的订单数量
*/
int expireOrder();
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.pay.service.order;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@ -39,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@ -152,6 +155,7 @@ public class PayOrderServiceImpl implements PayOrderService {
.setReturnUrl(reqVO.getReturnUrl())
// 订单相关字段
.setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
unifiedOrderReqDTO.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofSeconds(70))); // TODO 芋艿稍后删除掉
PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
// 4. 如果调用直接支付成功则直接更新支付单状态为成功例如说付款码支付免密支付时就直接验证支付成功
@ -297,7 +301,7 @@ public class PayOrderServiceImpl implements PayOrderService {
throw exception(ORDER_EXTENSION_NOT_FOUND);
}
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功直接返回不用重复更新
log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId());
return orderExtension;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态必须是待支付
@ -310,7 +314,7 @@ public class PayOrderServiceImpl implements PayOrderService {
if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId());
return orderExtension;
}
@ -331,7 +335,7 @@ public class PayOrderServiceImpl implements PayOrderService {
}
if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功直接返回不用重复更新
&& Objects.equals(order.getExtensionId(), orderExtension.getId())) {
log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId());
return true;
}
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态必须是待支付
@ -349,7 +353,7 @@ public class PayOrderServiceImpl implements PayOrderService {
if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ORDER_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId());
return false;
}
@ -364,12 +368,12 @@ public class PayOrderServiceImpl implements PayOrderService {
throw exception(ORDER_EXTENSION_NOT_FOUND);
}
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭直接返回不用重复更新
log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId());
log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId());
return;
}
// 一般出现先是支付成功然后支付关闭都是全部退款导致关闭的场景这个情况我们不更新支付拓展单只通过退款流程更新支付单
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
return;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态必须是待支付
@ -383,7 +387,7 @@ public class PayOrderServiceImpl implements PayOrderService {
if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId());
log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId());
}
@Override
@ -414,6 +418,128 @@ public class PayOrderServiceImpl implements PayOrderService {
return orderExtensionMapper.selectById(id);
}
@Override
public int syncOrder(LocalDateTime minCreateTime) {
// 1. 查询指定创建时间内的待支付订单
List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe(
PayOrderStatusEnum.WAITING.getStatus(), minCreateTime);
if (CollUtil.isEmpty(orderExtensions)) {
return 0;
}
// 2. 遍历执行
int count = 0;
for (PayOrderExtensionDO orderExtension : orderExtensions) {
count += syncOrder(orderExtension) ? 1 : 0;
}
return count;
}
/**
* 同步单个支付拓展单
*
* @param orderExtension 支付拓展单
* @return 是否已支付
*/
private boolean syncOrder(PayOrderExtensionDO orderExtension) {
try {
// 1.1 查询支付订单信息
PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
// 1.2 回调支付结果
notifyOrder(orderExtension.getChannelId(), respDTO);
// 2. 如果是已支付则返回 1
return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus());
} catch (Throwable e) {
log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e);
return false;
}
}
@Override
public int expireOrder() {
// 1. 查询过期的待支付订单
List<PayOrderDO> orders = orderMapper.selectListByStatusAndExpireTimeLt(
PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now());
if (CollUtil.isEmpty(orders)) {
return 0;
}
// 2. 遍历执行
int count = 0;
for (PayOrderDO order : orders) {
count += expireOrder(order) ? 1 : 0;
}
return count;
}
/**
* 同步单个支付单
*
* @param order 支付单
* @return 是否已过期
*/
private boolean expireOrder(PayOrderDO order) {
try {
// 1. 需要先处理关联的支付拓展单避免错误的过期已支付 or 已退款的订单
List<PayOrderExtensionDO> orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId());
for (PayOrderExtensionDO orderExtension : orderExtensions) {
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) {
continue;
}
// 情况一校验数据库中的 orderExtension 是不是已支付
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]",
order.getId(), orderExtension.getId());
return false;
}
// 情况二调用三方接口查询支付单状态是不是已支付/已退款
PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) {
// 补充说明按道理应该是 WAITING => SUCCESS => REFUND 状态如果直接 WAITING => REFUND 状态说明中间丢了过程
// 此时需要人工介入手工补齐数据保持 WAITING => SUCCESS => REFUND 的过程
log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]",
orderExtension.getId(), toJsonString(respDTO));
return false;
}
if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
notifyOrder(orderExtension.getChannelId(), respDTO);
return false;
}
// 兜底逻辑将支付拓展单更新为已关闭
PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setChannelNotifyData(toJsonString(respDTO));
if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(),
updateObj) == 0) {
log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId());
return false;
}
log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId());
}
// 2. 都没有上述情况可以安心更新为已关闭
PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus());
if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) {
log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
return false;
}
log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId());
return true;
} catch (Throwable e) {
log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e);
return false;
}
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*