乐趣区

支付系统设计实现1支付与退款

支付流程

以上是微信 app 支付的流程:

  1. 用户进入 app 选择商品进行购买,在 app 内部结算时生成用户本系统订单(待支付状态),此时返回订单信息与支付方式列表
  2. 用户确认金额无误,并选择支付方式。此时 app 将订单 id 与支付方式传给服务器,服务器根据订单金额与支付方式在外部支付系统下单(预支付订单),并给 app 返回可以唤起响应支付工具的‘支付数据’
  3. app 获取到‘支付数据’调用响应支付 sdk,此时用户会看见微信支付或支付宝支付的页面,此时用户需要确认支付金额并输入正确的密码
  4. sdk 会检查用户的密码信息是否正确并返回支付结果,此时 app 收到 sdk 同步通知的支付结果,并提交到服务器,服务器会记录该笔支付的状态, 切记不能使用 app 上传回来的支付结果作为最终的支付结果,不能信任前端数据
  5. 外部支付系统在处理完成该笔支付后会回调服务器设置的回调接口,通知服务器该笔支付的最终支付状态

支付流程的反思

以上流程时微信与支付宝给出的官方流程,并且也是最标准的流程。但是当外部支付系统并没有微信与支付宝那么优秀的时候,我们的系统就不能按照该流程正常运行下去,下面我说说在使用‘建行支付’时遇到的问题

  1. 服务器收不到支付结果的回调
  2. 即使主动查询支付状态‘建行支付’依然返回未支付的状态

以上两个问题会引发更复杂的问题

  1. 由于没有支付回调,订单状态就不会发送改变用户就会再次支付,造成重复支付的现象
  2. 系统没有预料到会出现重复支付也就没有相应的策略去弥补
  3. 由于系统并不知道支付已经成功,用户在取消订单的时候就不会收到退款

根据这些线上出现的问题,我们决定进行重构,并深层次的处理整个支付流程!

设计思路

1. 确认支付方式

1. 微信 App 支付
2. 支付宝 App 支付
3. 建行支付 App 支付 

2. 如何确保支付成功

1. 外部支付系统(支付宝)成功后回调通知
2. 本系统主动查询外部支付系统订单状态 

3. 如何避免用户重复支付

1. 本系统在发起支付的时候检查‘订单号’是否有已经成功的支付记录
2. 本系统在发起支付的时候检查‘订单号’是否有已经提交的支付记录,如果有需要同步查询外部支付系统该订单的支付状态 

4. 如果用户出现重复支付系统如何处理

1. 系统需要定时检查是否有同一个‘订单号’出现多条成功支付的记录,如果有需要保留一笔支付,其余的进行退款处理    

5. 数据出现异常怎么办(例如:用户说支付完成,但是订单依然是待支付的状态)

1. 所有的支付流程都需要进行数据记录,形成支付流水,这样可以直观的看到用户支付的路径,也方便外部支付系统查询

具体逻辑

  1. ‘支付’是一次支付的记录,可能包含多个订单的支付金额,因为用户在购买商品生成订单的时候会根据商家拆单
  2. ‘支付与订单的隐射’表明该支付中所有的订单信息,每个‘映射’都记录了订单的金额与支付状态,并且重复支付也是发生在该‘映射’上的,因为一个订单智能有一次最终成功支付的记录, 最终哪一个映射是有效的由‘是否当前使用’决定,任何时候一个订单只有一个映射有效
  3. ‘支付’可能有多条退款记录,退款的总金额不能超过支付金额,并且每一笔退款都需要一个唯一的退款交易号来保证不会重复退款,退款交易号由具体业务系统生成(比如退货,取消订单,重复支付)
  4. 所有的退款必须成功
  5. 系统需要主动查询支付状态是‘发起支付’,‘app 同步通知成功’记录在外部支付系统的支付状态,如果在外部支付系统支付成功,这里需要重新设置支付状态为‘已完成’
  6. 支付的外部交易号与退款的退款交易号都是唯一的
  7. 为了保证系统的正常工作我们还需要一些定时器来作为最后的防线

接口实现

1. 支付业务逻辑

public interface IPaymentApplicationService {

/**
 * 创建支付,待支付状态
 * @param orders 订单 JSONArray
 * @param paymentAmount 支付金额
 * @param paymentChannel 支付渠道
 * @param paymentType 支付方式
 * @param accountId 账户
 * @param serviceId 服务
 * @param platformId 平台
 * @param hockParams 回传参数
 * @return
 */
PaymentResultDTO createPayment(List<PaymentOrderDTO> orders, BigDecimal paymentAmount, PaymentChannelEnum paymentChannel, PaymentTypeEnum paymentType, String accountId, String serviceId, String platformId,String hockParams) throws InvalidOperationException;

/**
 * app、小程序、H5 收到回调
 * @param paymentId 支付 id
 * @param isSuccess 是否支付成功
 */
void synchronizedCallback(String paymentId,boolean isSuccess) throws InvalidOperationException, PaymentQueryException, PaymentNotExistException;

/**
 * app、小程序、H5 收到回调
 * @param orderIds 本次支付的所有订单 id
 * @param isSuccess 是否成功
 */
void synchronizedCallback(Collection<String> orderIds,boolean isSuccess);

/**
 * 服务器端收到回调
 * @param paymentId 支付 id
 * @param isSuccess 是否支付成功
 * @param tradeNo 交易号
 */
void asyncCallback(String paymentId,boolean isSuccess,String tradeNo) throws InvalidOperationException;


/**
 * 服务器端收到回调
 * @param outTradeNo 外部交易号
 * @param isSuccess 是否支付成功
 * @param tradeNo 交易号
 */
void asyncCallbackForOutTradeNo(String outTradeNo,boolean isSuccess,String tradeNo);

}

2. 退款业务逻辑

public interface IRefundApplicationService {

/**
 * 发起退款
 * @param paymentId 支付 id
 * @param sysRefundTradeNo 系统内部退款单号
 * @param refundAmount 退款金额
 * @param refundType 退款类型
 * @param reason 退款理由
 * @return 退款 id
 */
void createRefund(String paymentId,String sysRefundTradeNo, BigDecimal refundAmount, RefundTypeEnum refundType, String reason) throws Exception;

/**
 * 针对订单发起退款
 * @param orderId 订单 id
 * @param sysRefundTradeNo 系统内部退款单号
 * @param refundAmount 退款金额
 * @param refundType 退款类型
 * @param reason 退款理由
 * @return 退款 id
 */
void createRefundByOrder(String orderId,String sysRefundTradeNo, BigDecimal refundAmount, RefundTypeEnum refundType, String reason) throws Exception;

/**
 * 退款
 * @param refund 退款实体
 */
void refund(RefundDO refund) throws InvalidOperationException;

}

附件代码

@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class PaymentApplicationServiceImpl implements IPaymentApplicationService {

private final PaymentConfigJpaRepository paymentConfigJpaRepository;
private final PaymentJpaRepository paymentJpaRepository;
private final PaymentOrderJpaRepository paymentOrderJpaRepository;
private final PaymentFlowJpaRepository paymentFlowJpaRepository;
private final OrderJpaRepository orderJpaRepository;

private final WechatPayUtil wechatPayUtil;
private final AliPayUtil aliPayUtil;
private final CcbPayUtil ccbPayUtil;

@Autowired
public PaymentApplicationServiceImpl(PaymentConfigJpaRepository paymentConfigJpaRepository, PaymentJpaRepository paymentJpaRepository, PaymentOrderJpaRepository paymentOrderJpaRepository, PaymentFlowJpaRepository paymentFlowJpaRepository, OrderJpaRepository orderJpaRepository, WechatPayUtil wechatPayUtil, AliPayUtil aliPayUtil, CcbPayUtil ccbPayUtil) {
    this.paymentConfigJpaRepository = paymentConfigJpaRepository;
    this.paymentJpaRepository = paymentJpaRepository;
    this.paymentOrderJpaRepository = paymentOrderJpaRepository;
    this.paymentFlowJpaRepository = paymentFlowJpaRepository;
    this.orderJpaRepository = orderJpaRepository;
    this.wechatPayUtil = wechatPayUtil;
    this.aliPayUtil = aliPayUtil;
    this.ccbPayUtil = ccbPayUtil;
}

@Override
public PaymentResultDTO createPayment(List<PaymentOrderDTO> orders, BigDecimal paymentAmount, PaymentChannelEnum paymentChannel, PaymentTypeEnum paymentType, String accountId, String serviceId, String platformId,String hockParams) throws InvalidOperationException {
    // 成功支付的订单不能再次支付
    List<String> orderIds = orders.stream().map(PaymentOrderDTO::getOrderId).collect(Collectors.toList());
    List<PaymentOrderDO> oldPaymentOrders = paymentOrderJpaRepository.findByOrderIdIn(orderIds);
    for (PaymentOrderDO po : oldPaymentOrders) {if(po.getPaymentStatus().equals(PaymentStatusEnum.SYNCHRONIZE_CALLBACK_SUCCEED.getValue()) || po.getPaymentStatus().equals(PaymentStatusEnum.ASYNC_CALLBACK_SUCCEED.getValue())){
            // 主动查询支付是否成功
            PaymentQueryResult paymentQueryResult;
            paymentQueryResult = getPaymentResult(paymentChannel,po.getOutTradeNo());
            if(paymentQueryResult != null && paymentQueryResult.getTradeStatus().equals(PaymentQueryResult.TradeStatus.SUCCESS)){asyncCallback(po.getPaymentId(),true,paymentQueryResult.getTradeNo());
            }
            throw new InvalidOperationException("订单:" + po.getOrderId() + "已成功支付");
        }else if(po.getPaymentStatus().equals(PaymentStatusEnum.LAUNCH_PAY.getValue())){
            // 发现重复支付
            po.fundRepeatPay();
            paymentOrderJpaRepository.save(po);
        }
    }
    Optional<PaymentConfigDO> paymentConfigOptional = paymentConfigJpaRepository.findByPaymentChannelAndPaymentType(paymentChannel.getValue(),paymentType.getValue());
    if(!paymentConfigOptional.isPresent()){throw new InvalidOperationException("支付方式不存在");
    }
    PaymentConfigDO paymentConfig = paymentConfigOptional.get();
    PaymentDO payment = new PaymentDO(paymentAmount,paymentChannel,paymentType,paymentConfig.getMerchantId(),accountId,serviceId,platformId);
    paymentJpaRepository.save(payment);
    PaymentFlowDO paymentFlow = new PaymentFlowDO(payment.getId(),payment.getOutTradeNo(), PaymentFlowEnum.REQUEST_SIGNATURE,JSONArray.toJSONString(orders));
    paymentFlowJpaRepository.save(paymentFlow);
    List<PaymentOrderDO> paymentOrders = new ArrayList<>();
    for (PaymentOrderDTO order : orders) {String orderId = order.getOrderId();
        if (StringUtils.isBlank(orderId)) {throw new InvalidOperationException("orderId 必传");
        }
        PaymentOrderDO paymentOrder = new PaymentOrderDO(payment.getId(), orderId, payment.getOutTradeNo(), order.getOrderAmount(), JSONObject.toJSONString(order));
        paymentOrders.add(paymentOrder);
    }
    paymentOrderJpaRepository.saveAll(paymentOrders);
    String sign = lunchPay(payment,hockParams);
    return new PaymentResultDTO(payment.getId(),sign,hockParams);
}

private String lunchPay(PaymentDO payment,String hockParams) throws InvalidOperationException {Optional<PaymentConfigDO> paymentConfigOptional = paymentConfigJpaRepository.findByPaymentChannelAndPaymentType(payment.getPaymentChannel(),payment.getPaymentType());
    if(!paymentConfigOptional.isPresent()){throw new InvalidOperationException("支付配置不存在");
    }
    PaymentConfigDO paymentConfig = paymentConfigOptional.get();
    List<PaymentOrderDO> paymentOrders = paymentOrderJpaRepository.findByPaymentId(payment.getId());
    // 计算签名字符串
    String signString;
    PaymentChannelEnum paymentChannel = PaymentChannelEnum.getEnumByVal(paymentConfig.getPaymentChannel());
    switch (paymentChannel){
        case ALIPAY:
            signString = aliPayUtil.getSignStr(paymentConfig.getOrderNamePrefix() + ":" + payment.getId(),payment.getOutTradeNo(),payment.getPaymentAmount(),hockParams);
            break;
        case WECHATPAY:
            signString = wechatPayUtil.getSignStr(paymentConfig.getOrderNamePrefix() + ":" + payment.getId(),payment.getOutTradeNo(),payment.getPaymentAmount(),hockParams);
            break;
        case CCBPAY:
            signString = ccbPayUtil.getSignStr(paymentConfig.getOrderNamePrefix() + ":" + payment.getId(),payment.getOutTradeNo(),payment.getPaymentAmount(),hockParams);
            break;
        default:
            throw new InvalidOperationException("支付方式异常");
    }
    payment.lunchPay(signString);
    for (PaymentOrderDO po : paymentOrders) {po.lunchPay(signString);
    }
    PaymentFlowDO paymentFlow = new PaymentFlowDO(payment.getId(),payment.getOutTradeNo(), PaymentFlowEnum.RECEIVE_SIGNATURE,signString);

    paymentJpaRepository.save(payment);
    paymentOrderJpaRepository.saveAll(paymentOrders);
    paymentFlowJpaRepository.save(paymentFlow);

    return signString;
}

@Override
public void synchronizedCallback(String paymentId, boolean isSuccess) throws InvalidOperationException {Optional<PaymentDO> paymentOptional = paymentJpaRepository.findById(paymentId);
    if(!paymentOptional.isPresent()){throw new InvalidOperationException("支付不存在");
    }
    PaymentDO payment = paymentOptional.get();
    payment.synchronizedCallback(isSuccess);
    List<PaymentOrderDO> paymentOrders = paymentOrderJpaRepository.findByPaymentId(paymentId);
    for (PaymentOrderDO po : paymentOrders) {po.synchronizedCallback(isSuccess);
    }
    PaymentFlowDO paymentFlow = new PaymentFlowDO(payment.getId(),payment.getOutTradeNo(), PaymentFlowEnum.SYNCHRONIZE_CALLBACK,isSuccess + "");
    // 主动查询订单在第三方支付的状态,确保就算没有收到异步回调,支付状态依然正确
    PaymentChannelEnum paymentChannel = PaymentChannelEnum.getEnumByVal(payment.getPaymentChannel());
    PaymentQueryResult paymentQueryResult = getPaymentResult(paymentChannel,payment.getOutTradeNo());

    if(paymentQueryResult != null && paymentQueryResult.getTradeStatus().equals(PaymentQueryResult.TradeStatus.SUCCESS)){asyncCallback(paymentId,true,paymentQueryResult.getTradeNo());
    }
    paymentJpaRepository.save(payment);
    paymentOrderJpaRepository.saveAll(paymentOrders);
    paymentFlowJpaRepository.save(paymentFlow);
}

@Override
public void synchronizedCallback(Collection<String> orderIds, boolean isSuccess) {List<PaymentOrderDO> paymentOrders = paymentOrderJpaRepository.findByOrderIdInAndPaymentStatus(orderIds,PaymentStatusEnum.LAUNCH_PAY.getValue());
    Set<String> paymentIds = paymentOrders.stream().map(PaymentOrderDO::getPaymentId).collect(Collectors.toSet());
    for (String pId : paymentIds) {
        try {synchronizedCallback(pId,true);
        } catch (Exception e) {e.printStackTrace();
            log.error("支付同步通知异常:" + pId + ":" + e.getMessage());
        }
    }
}

@Override
public void asyncCallback(String paymentId, boolean isSuccess, String tradeNo) throws InvalidOperationException {Optional<PaymentDO> paymentOptional = paymentJpaRepository.findById(paymentId);
    if(!paymentOptional.isPresent()){throw new InvalidOperationException("支付不存在");
    }
    List<PaymentOrderDO> paymentOrders = paymentOrderJpaRepository.findByPaymentId(paymentId);
    List<String> orderIds = paymentOrders.stream().map(PaymentOrderDO::getOrderId).collect(Collectors.toList());
    // 这些订单的其他支付:设置为重复
    List<PaymentOrderDO> oldOtherPaymentOrders = paymentOrderJpaRepository.findByOrderIdIn(orderIds).stream().filter(po -> !po.getPaymentId().equalsIgnoreCase(paymentId)).collect(Collectors.toList());
    if(!CollectionUtils.isEmpty(oldOtherPaymentOrders)){for (PaymentOrderDO opo : oldOtherPaymentOrders) {opo.fundRepeatPay();
        }
        paymentOrderJpaRepository.saveAll(oldOtherPaymentOrders);
    }
    PaymentDO payment = paymentOptional.get();
    payment.asyncCallback(isSuccess,tradeNo);
    for (PaymentOrderDO po : paymentOrders) {po.asyncCallback(isSuccess);
        if(isSuccess){
            //todo 需要考虑通知业务系统
            Optional<OrderEntity> orderOptional = orderJpaRepository.findById(Long.valueOf(po.getOrderId()));
            if(orderOptional.isPresent()) {OrderEntity order = orderOptional.get();
                order.paySucceed(order.getOutTradeNo(), tradeNo);
                orderJpaRepository.save(order);
            }
        }
    }
    PaymentFlowDO paymentFlow = new PaymentFlowDO(payment.getId(),payment.getOutTradeNo(), PaymentFlowEnum.ASYNC_CALLBACK,isSuccess + "");
    paymentJpaRepository.save(payment);
    paymentFlowJpaRepository.save(paymentFlow);
}

@Override
public void asyncCallbackForOutTradeNo(String outTradeNo, boolean isSuccess, String tradeNo) {Optional<PaymentDO> paymentOptional = paymentJpaRepository.findByOutTradeNo(outTradeNo);
    if(paymentOptional.isPresent()){PaymentDO payment = paymentOptional.get();
        try{asyncCallback(payment.getId(),true,tradeNo);
        }catch (Exception e) {e.printStackTrace();
            log.error("支付异步通知异常:" + payment.getId() + ":" + e.getMessage());
        }
    }
}

private PaymentQueryResult getPaymentResult(PaymentChannelEnum paymentChannel,String outTradeNo) {
    PaymentQueryResult paymentQueryResult;
    try{switch (paymentChannel){
            case ALIPAY:
                paymentQueryResult = aliPayUtil.paymentQuery(outTradeNo);
                break;
            case WECHATPAY:
                paymentQueryResult = wechatPayUtil.paymentQuery(outTradeNo);
                break;
            case CCBPAY:
                paymentQueryResult = ccbPayUtil.paymentQuery(outTradeNo);
                break;
            default:
                throw new InvalidOperationException("支付方式异常");
        }
        return paymentQueryResult;
    }catch (Exception e){log.error(e.getMessage());
        return null;
    }
}

}

@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class RefundApplicationServiceImpl implements IRefundApplicationService {

private final RefundJpaRepository refundJpaRepository;
private final RefundFlowJpaRepository refundFlowJpaRepository;
private final PaymentJpaRepository paymentJpaRepository;
private final PaymentOrderJpaRepository paymentOrderJpaRepository;

private final WechatPayUtil wechatPayUtil;
private final AliPayUtil aliPayUtil;
private final CcbPayUtil ccbPayUtil;

@Autowired
public RefundApplicationServiceImpl(RefundJpaRepository refundJpaRepository, RefundFlowJpaRepository refundFlowJpaRepository, PaymentJpaRepository paymentJpaRepository, PaymentOrderJpaRepository paymentOrderJpaRepository, WechatPayUtil wechatPayUtil, AliPayUtil aliPayUtil, CcbPayUtil ccbPayUtil) {
    this.refundJpaRepository = refundJpaRepository;
    this.refundFlowJpaRepository = refundFlowJpaRepository;
    this.paymentJpaRepository = paymentJpaRepository;
    this.paymentOrderJpaRepository = paymentOrderJpaRepository;
    this.wechatPayUtil = wechatPayUtil;
    this.aliPayUtil = aliPayUtil;
    this.ccbPayUtil = ccbPayUtil;
}

@Override
public void createRefund(String paymentId,String sysRefundTradeNo, BigDecimal refundAmount, RefundTypeEnum refundType, String reason) throws Exception {Optional<PaymentDO> paymentOptional = paymentJpaRepository.findById(paymentId);
    if(!paymentOptional.isPresent()){throw new InvalidOperationException("支付不存在, 不可退款");
    }
    PaymentDO payment = paymentOptional.get();
    // 检查支付是否完成,未完成就不可退款
    PaymentChannelEnum paymentChannel = PaymentChannelEnum.getEnumByVal(payment.getPaymentChannel());
    PaymentQueryResult paymentQueryResult = getPaymentResult(paymentChannel,payment.getOutTradeNo());
    log.info("pay query : {}",paymentQueryResult);
    if(paymentQueryResult == null ||
        paymentQueryResult.getTradeStatus().equals(PaymentQueryResult.TradeStatus.FAILED) ||
        paymentQueryResult.getTradeStatus().equals(PaymentQueryResult.TradeStatus.CLOSED) ||
        paymentQueryResult.getTradeStatus().equals(PaymentQueryResult.TradeStatus.PAYING)){throw new InvalidOperationException("支付状态异常,不可退款");
    }

    List<Integer> refundStatus = Arrays.asList(RefundStatusEnum.START.getValue(),RefundStatusEnum.SUCCESS.getValue(),RefundStatusEnum.FAIL.getValue());
    List<RefundDO> oldRefunds = refundJpaRepository.findByPaymentIdAndRefundStatusIn(paymentId,refundStatus);
    BigDecimal oldRefundsAmount = oldRefunds.stream().map(RefundDO::getRefundAmount).reduce(BigDecimal.ZERO,BigDecimal::add);
    if(oldRefundsAmount.add(refundAmount).compareTo(payment.getPaymentAmount()) > 0){throw new InvalidOperationException("退款总金额不可超过支付金额");
    }
    RefundDO refund;
    Optional<RefundDO> refundOptional = refundJpaRepository.findBySysRefundTradeNo(sysRefundTradeNo);
    if(refundOptional.isPresent()){RefundDO oldRefund = refundOptional.get();
        if(oldRefund.getRefundStatus().equals(RefundStatusEnum.SUCCESS.getValue())){throw new InvalidOperationException("退款已完成");
        }else if(oldRefund.getRefundStatus().equals(RefundStatusEnum.START.getValue())){throw new InvalidOperationException("退款处理中");
        }
    }else {refund = new RefundDO(paymentId,payment.getOutTradeNo(),sysRefundTradeNo,refundAmount, refundType,reason,payment.getPaymentChannel(), payment.getPaymentType(),payment.getPaymentAmount());
        refundJpaRepository.save(refund);
    }
}

@Override
public void createRefundByOrder(String orderId, String sysRefundTradeNo, BigDecimal refundAmount, RefundTypeEnum refundType, String reason) throws Exception {Optional<PaymentOrderDO> paymentOrderOptional = paymentOrderJpaRepository.findByOrderIdAndIsCurrent(orderId,true);
    if(paymentOrderOptional.isPresent()){PaymentOrderDO paymentOrder = paymentOrderOptional.get();
        createRefund(paymentOrder.getPaymentId(),sysRefundTradeNo,refundAmount,refundType,reason);
        paymentOrderJpaRepository.save(paymentOrder);
    }
}

@Override
public void refund(RefundDO refund) throws InvalidOperationException {PaymentChannelEnum paymentChannel = PaymentChannelEnum.getEnumByVal(refund.getPaymentChannel());
    RefundTypeEnum refundType = RefundTypeEnum.getEnumByVal(refund.getRefundType());
    String respData;
    try {switch (paymentChannel){
            case ALIPAY:
                respData = aliPayUtil.orderPayRefund(refund.getOutTradeNo(),refund.getOutRefundTradeNo(),refund.getPaymentAmount(),refund.getRefundAmount(),refundType);
                break;
            case WECHATPAY:
                respData = wechatPayUtil.orderPayRefund(refund.getOutTradeNo(),refund.getOutRefundTradeNo(),refund.getPaymentAmount(),refund.getRefundAmount(),refundType);
                break;
            case CCBPAY:
                respData = ccbPayUtil.orderPayRefund(refund.getOutTradeNo(),refund.getOutRefundTradeNo(),refund.getPaymentAmount(),refund.getRefundAmount(),refundType);
                break;
            default:
                throw new InvalidOperationException("支付方式异常");
        }
        refundSuccess(refund,respData);
    }catch (Exception e){refundFail(refund,e.getMessage());
    }
}

private void refundSuccess(RefundDO refund,String respData) throws InvalidOperationException {refund.asyncCallback(true);
    refundJpaRepository.save(refund);
    RefundFlowDO refundSyncFlow = new RefundFlowDO(refund.getId(), RefundFlowEnum.SYNCHRONIZE_CALLBACK_SUCCESS,respData);
    refundFlowJpaRepository.save(refundSyncFlow);
}
private void refundFail(RefundDO refund,String errorMessage) throws InvalidOperationException{refund.asyncCallback(false);
    refundJpaRepository.save(refund);
    RefundFlowDO refundSyncFlow = new RefundFlowDO(refund.getId(), RefundFlowEnum.SYNCHRONIZE_CALLBACK_FAIL,errorMessage);
    refundFlowJpaRepository.save(refundSyncFlow);
}

private PaymentQueryResult getPaymentResult(PaymentChannelEnum paymentChannel,String outTradeNo) {
    try {switch (paymentChannel){
            case ALIPAY:
                return aliPayUtil.paymentQuery(outTradeNo);
            case WECHATPAY:
                return wechatPayUtil.paymentQuery(outTradeNo);
            case CCBPAY:
                return ccbPayUtil.paymentQuery(outTradeNo);
            default:
                return null;
        }
    }catch (Exception e){log.error(e.getMessage());
        return null;
    }
}

}

@Component
@Slf4j
@DisallowConcurrentExecution
public class CheckPaymentCloseSchedule extends QuartzJobBean {

@Autowired
private PaymentJpaRepository paymentJpaRepository;
@Autowired
private IPaymentQueryService paymentQueryService;

@Override
public void executeInternal(JobExecutionContext context) throws JobExecutionException {log.info("检查关闭支付定时器启动");
    List<PaymentDO> payments = paymentJpaRepository.findByPaymentStatus(PaymentStatusEnum.LAUNCH_PAY.getValue());
    LocalDateTime now = LocalDateTime.now();
    List<PaymentDO> closePayments = new ArrayList<>();
    for (PaymentDO p : payments) {
        long minutes = 10L;
        if(!now.minusMinutes(minutes).isBefore(p.getLaunchPayTime())){
            // 超过十分钟没有支付,就进行关闭
            try {PaymentQueryResult result = paymentQueryService.getPaymentResult(p.getId());
                if(result == null){p.close();
                    closePayments.add(p);
                    if(closePayments.size() > 100){paymentJpaRepository.saveAll(closePayments);
                        closePayments.clear();
                        p = null;
                    }
                }
            } catch (Exception e) {log.error("关闭支付异常:" + p.getId() + ":" + e.getMessage());
            }
        }
    }
    paymentJpaRepository.saveAll(closePayments);
}

}

@Component
@Slf4j
@DisallowConcurrentExecution
public class CheckPaymentRepeatSchedule extends QuartzJobBean {

@Autowired
private PaymentOrderJpaRepository paymentOrderJpaRepository;
@Autowired
private IRefundApplicationService refundApplicationService;

@Override
public void executeInternal(JobExecutionContext context)throws JobExecutionException {log.info("检查重复支付定时器启动");
    // 找到尚未处理的重复支付记录
    List<PaymentOrderDO> paymentOrders = paymentOrderJpaRepository.findByPaymentStatusAndIsCurrentAndRepeatPayProcess(PaymentStatusEnum.ASYNC_CALLBACK_SUCCEED.getValue(),false,false);
    paymentOrders.forEach(po -> {
        try {refundApplicationService.createRefund(po.getPaymentId(), ObjectId.get().toString(),po.getPaymentAmount(), RefundTypeEnum.REPEAT_PAY,RefundTypeEnum.REPEAT_PAY.getDescription());
            po.refundRepeatPay();
            paymentOrderJpaRepository.save(po);
        } catch (Exception e) {log.error("支付订单:" + po.getId() + ":重复支付退款异常:" + e.getMessage());
        }
    });
}

}

@Component
@Slf4j
@DisallowConcurrentExecution
public class CheckPaymentSuccessSchedule extends QuartzJobBean {

@Autowired
private PaymentJpaRepository paymentJpaRepository;
@Autowired
private PaymentFlowJpaRepository paymentFlowJpaRepository;
@Autowired
private IPaymentApplicationService paymentApplicationService;
@Autowired
private IPaymentQueryService paymentQuery;

@Override
public void executeInternal(JobExecutionContext context) throws JobExecutionException {log.info("检查支付成功定时器启动");
    List<PaymentDO> payments = paymentJpaRepository.findByPaymentStatusIn(Arrays.asList(PaymentStatusEnum.LAUNCH_PAY.getValue(),PaymentStatusEnum.SYNCHRONIZE_CALLBACK_SUCCEED.getValue()));
    for (PaymentDO p : payments) {PaymentQueryResult result = paymentQuery.getPaymentResult(p.getId());
        PaymentFlowDO paymentFlow;
        if(result != null) {paymentFlow = new PaymentFlowDO(p.getId(), p.getOutTradeNo(), PaymentFlowEnum.RECEIVE_PAYMENT_DETAIL, JSONObject.toJSONString(result));
        }else {if(p.getPaymentStatus().equals(PaymentStatusEnum.SYNCHRONIZE_CALLBACK_SUCCEED.getValue())) {String error = "支付:" + p.getId() + "app 同步通知成功,但是第三方支付查询:未支付!";
                log.error(error);
                p.close();
                paymentJpaRepository.save(p);
                paymentFlow = new PaymentFlowDO(p.getId(), p.getOutTradeNo(), PaymentFlowEnum.RECEIVE_PAYMENT_DETAIL, error);
            }else {paymentFlow = new PaymentFlowDO(p.getId(), p.getOutTradeNo(), PaymentFlowEnum.RECEIVE_PAYMENT_DETAIL, "未支付");
            }
        }
        paymentFlowJpaRepository.save(paymentFlow);
        if(result != null){if(result.getTradeStatus().equals(PaymentQueryResult.TradeStatus.PAYING) ||
               result.getTradeStatus().equals(PaymentQueryResult.TradeStatus.SUCCESS)){
                // 第三方支付成功,直接修改支付状态
                try {paymentApplicationService.asyncCallback(p.getId(),true,result.getTradeNo());
                    PaymentFlowDO successPaymentFlow = new PaymentFlowDO(p.getId(),p.getOutTradeNo(), PaymentFlowEnum.ASYNC_CALLBACK_SUCCESS, JSONObject.toJSONString(result));
                    paymentFlowJpaRepository.save(successPaymentFlow);
                } catch (InvalidOperationException e) {log.error("支付:" + p.getId() + "第三方交易成功,本地修改异常:" + e.getMessage());
                }
            }else if(result.getTradeStatus().equals(PaymentQueryResult.TradeStatus.FAILED)){
                try {paymentApplicationService.asyncCallback(p.getId(),false,"");
                    PaymentFlowDO failPaymentFlow = new PaymentFlowDO(p.getId(),p.getOutTradeNo(), PaymentFlowEnum.ASYNC_CALLBACK_FAILED, JSONObject.toJSONString(result));
                    paymentFlowJpaRepository.save(failPaymentFlow);
                } catch (InvalidOperationException e) {log.error("支付:" + p.getId() + "第三方交易失败,本地修改异常:" + e.getMessage());
                }
            }
        }
    }
}

}

@Component
@Slf4j
@DisallowConcurrentExecution
public class CheckRefundSuccessSchedule extends QuartzJobBean {

@Autowired
private RefundJpaRepository refundJpaRepository;
@Autowired
private IRefundApplicationService refundApplicationService;

@Override
public void executeInternal(JobExecutionContext context) {log.info("检查退款成功定时器启动");
    List<RefundDO> refunds = refundJpaRepository.findByRefundStatusIn(Arrays.asList(RefundStatusEnum.FAIL.getValue(),RefundStatusEnum.START.getValue()));
    for (RefundDO r : refunds) {
        try {refundApplicationService.refund(r);
        } catch (InvalidOperationException e) {log.error("退款失败:" + r.getId() + ":" + e.getMessage());
        }
    }
}

}

退出移动版