转账 - 增加转账定时任务

This commit is contained in:
jason
2023-11-07 23:11:07 +08:00
parent 09d45e6393
commit 15313c2992
15 changed files with 314 additions and 83 deletions

View File

@ -66,9 +66,9 @@ public interface ErrorCodeConstants {
// ========== 转账模块 1-007-009-000 ==========
ErrorCode PAY_TRANSFER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1_007_009_000, "发起转账报错,错误码:{},错误提示:{}");
ErrorCode PAY_TRANSFER_NOT_FOUND = new ErrorCode(1_007_009_001, "转账单不存在");
ErrorCode PAY_TRANSFER_STATUS_IS_SUCCESS = new ErrorCode(1_007_009_002, "转账单已成功转账");
ErrorCode PAY_TRANSFER_EXISTS = new ErrorCode(1_007_009_003, "已经存在转账单");
ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经存在,请查询转账订单相关状态");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH = new ErrorCode(1_007_009_002, "两次相同转账请求的类型不匹配");
ErrorCode PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH = new ErrorCode(1_007_009_003, "两次相同转账请求的金额不匹配");
ErrorCode PAY_MERCHANT_TRANSFER_EXISTS = new ErrorCode(1_007_009_004, "该笔业务的转账已经发起,请查询转账订单相关状态");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_WAITING = new ErrorCode(1_007_009_005, "转账单不处于待转账");
ErrorCode PAY_TRANSFER_STATUS_IS_NOT_PENDING = new ErrorCode(1_007_009_006, "转账单不处于待转账或转账中");

View File

@ -40,9 +40,13 @@ public enum PayTransferStatusEnum {
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
public static boolean isWaiting(Integer status) {
return Objects.equals(status, WAITING.getStatus());
}
public static boolean isInProgress(Integer status) {
return Objects.equals(status, IN_PROGRESS.getStatus());
}
/**
* 是否处于待转账或者转账中的状态

View File

@ -18,7 +18,7 @@ public interface PayTransferConvert {
PayTransferDO convert(PayTransferCreateReqDTO dto);
PayTransferUnifiedReqDTO convert2(PayTransferCreateReqDTO dto);
PayTransferUnifiedReqDTO convert2(PayTransferDO dto);
PayTransferCreateReqDTO convert(PayTransferCreateReqVO vo);

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.pay.dal.mysql.transfer;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@ -40,6 +40,10 @@ public interface PayTransferMapper extends BaseMapperX<PayTransferDO> {
.betweenIfPresent(PayTransferDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayTransferDO::getId));
}
default List<PayTransferDO> selectListByStatus(Integer status){
return selectList(PayTransferDO::getStatus, status);
}
}

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
@ -181,4 +182,9 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
throw new UnsupportedOperationException("待实现");
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.pay.job.transfer;
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.transfer.PayTransferService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 转账订单的同步 Job
*
* 由于转账订单的转账结果,有些渠道是异步通知进行同步的,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。
*
* @author jason
*/
@Component
public class PayTransferSyncJob implements JobHandler {
@Resource
private PayTransferService transferService;
@Override
@TenantJob
public String execute(String param) {
int count = transferService.syncTransfer();
return StrUtil.format("同步转账订单 {} 个", count);
}
}

View File

@ -48,4 +48,10 @@ public interface PayTransferService {
*/
PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO);
/**
* 同步渠道转账单状态
*
* @return 同步到状态的转账数量,包括转账成功、转账失败、转账中的
*/
int syncTransfer();
}

View File

@ -1,14 +1,16 @@
package cn.iocoder.yudao.module.pay.service.transfer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;
@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
import cn.iocoder.yudao.module.pay.dal.mysql.transfer.PayTransferMapper;
import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@ -27,7 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Objects;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.pay.convert.transfer.PayTransferConvert.INSTANCE;
@ -75,56 +78,60 @@ public class PayTransferServiceImpl implements PayTransferService {
@Override
public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
// 1.1 校验转账单是否可以提交
validateTransferCanCreate(reqDTO.getAppId(), reqDTO.getMerchantTransferId());
// 1.2 校验 App
// 1.1 校验 App
PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
// 1.3 校验支付渠道是否有效
// 1.2 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
PayClient client = channelService.getPayClient(channel.getId());
if (client == null) {
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND);
}
// 2.创建转账单
String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
PayTransferDO transfer = INSTANCE.convert(reqDTO)
.setChannelId(channel.getId())
.setNo(no).setStatus(WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl());
transferMapper.insert(transfer);
PayTransferRespDTO unifiedTransferResp = null;
// 1.3 校验转账单已经发起过转账。
PayTransferDO transfer = validateTransferCanCreate(reqDTO);
if (transfer == null) {
// 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
String no = noRedisDAO.generate(TRANSFER_NO_PREFIX);
transfer = INSTANCE.convert(reqDTO)
.setChannelId(channel.getId())
.setNo(no).setStatus(WAITING.getStatus())
.setNotifyUrl(payApp.getTransferNotifyUrl());
transferMapper.insert(transfer);
}
try {
// 3. 调用三方渠道发起转账
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(reqDTO)
.setOutTransferNo(no);
unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
} catch (ServiceException ex) {
// 业务异常.直接返回转账失败的结果
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生业务异常]", transfer.getId(), reqDTO, ex);
unifiedTransferResp = PayTransferRespDTO.closedOf("", "", no, ex);
} catch (Throwable e) {
// 注意这里仅打印异常,不进行抛出。
// 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续通过转账回调、或者转账轮询可以拿到。
// TODO 需要加转账回调业务接口 和 转账轮询未实现
// 最终,在异常的情况下,支付中心会异步回调业务的转账回调接口,提供转账结果
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
if (Objects.nonNull(unifiedTransferResp)) {
PayTransferUnifiedReqDTO transferUnifiedReq = INSTANCE.convert2(transfer)
.setOutTransferNo(transfer.getNo());
PayTransferRespDTO unifiedTransferResp = client.unifiedTransfer(transferUnifiedReq);
// 4. 通知转账结果
getSelf().notifyTransfer(channel, unifiedTransferResp);
} catch (Throwable e) {
// 注意这里仅打印异常,不进行抛出。
// 原因是:虽然调用支付渠道进行转账发生异常(网络请求超时),实际转账成功。这个结果,后续转账轮询可以拿到。
// 或者使用相同 no 再次发起转账请求
log.error("[createTransfer][转账 id({}) requestDTO({}) 发生异常]", transfer.getId(), reqDTO, e);
}
return transfer.getId();
}
@Override
public PayTransferDO getTransfer(Long id) {
return transferMapper.selectById(id);
}
@Override
public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
return transferMapper.selectPage(pageReqVO);
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
if (transfer != null) {
// 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
}
if (ObjectUtil.notEqual(dto.getPrice(), transfer.getPrice())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_PRICE_NOT_MATCH);
}
if (ObjectUtil.notEqual(dto.getType(), transfer.getType())) {
throw exception(PAY_SAME_MERCHANT_TRANSFER_TYPE_NOT_MATCH);
}
}
// 如果状态为等待状态。不知道渠道转账是否发起成功。 允许使用相同的 no 再次发起转账,渠道会保证幂等
return transfer;
}
@Transactional(rollbackFor = Exception.class)
@ -138,17 +145,40 @@ public class PayTransferServiceImpl implements PayTransferService {
if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
notifyTransferClosed(channel, notify);
}
// 转账处理中的回调
if (PayTransferStatusRespEnum.isInProgress(notify.getStatus())) {
notifyTransferInProgress(channel, notify);
}
// WAITING 状态无需处理
// TODO IN_PROGRESS 待处理
}
private void validateTransferCanCreate(Long appId, String merchantTransferId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, merchantTransferId);
if (transfer != null) { // 是否存在
throw exception(PAY_MERCHANT_TRANSFER_EXISTS);
private void notifyTransferInProgress(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (isInProgress(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
return;
}
if (!isWaiting(transfer.getStatus())) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
// 2.更新
int updateCounts = transferMapper.updateByIdAndStatus(transfer.getId(),
CollUtil.newArrayList(WAITING.getStatus()),
new PayTransferDO().setStatus(IN_PROGRESS.getStatus()));
if (updateCounts == 0) {
throw exception(PAY_TRANSFER_STATUS_IS_NOT_WAITING);
}
log.info("[notifyTransferInProgress][transfer({}) 更新为转账进行中状态]", transfer.getId());
// 3. 插入转账通知记录
notifyService.createPayNotifyTask(PayNotifyTypeEnum.TRANSFER.getType(),
transfer.getId());
}
private void notifyTransferSuccess(PayChannelDO channel, PayTransferRespDTO notify) {
// 1.校验
PayTransferDO transfer = transferMapper.selectByNo(notify.getOutTransferNo());
@ -210,6 +240,56 @@ public class PayTransferServiceImpl implements PayTransferService {
}
@Override
public PayTransferDO getTransfer(Long id) {
return transferMapper.selectById(id);
}
@Override
public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
return transferMapper.selectPage(pageReqVO);
}
@Override
public int syncTransfer() {
List<PayTransferDO> list = transferMapper.selectListByStatus(WAITING.getStatus());
if (CollUtil.isEmpty(list)) {
return 0;
}
int count = 0;
for (PayTransferDO transfer : list) {
count += syncTransfer(transfer) ? 1 : 0;
}
return count;
}
private boolean syncTransfer(PayTransferDO transfer) {
try {
// 1. 查询转账订单信息
PayClient payClient = channelService.getPayClient(transfer.getChannelId());
if (payClient == null) {
log.error("[syncTransfer][渠道编号({}) 找不到对应的支付客户端]", transfer.getChannelId());
return false;
}
PayTransferRespDTO resp = payClient.getTransfer(transfer.getNo(),
PayTransferTypeEnum.typeOf(transfer.getType()));
// 2. 回调转账结果
notifyTransfer(transfer.getChannelId(), resp);
return true;
} catch (Throwable ex) {
log.error("[syncTransfer][transfer({}) 同步转账单状态异常]", transfer.getId(), ex);
return false;
}
}
private void notifyTransfer(Long channelId, PayTransferRespDTO notify) {
// 校验渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId);
// 通知转账结果给对应的业务
TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyTransfer(channel, notify));
}
/**
* 获得自身的代理对象,解决 AOP 生效问题
*