Loading

03-支付服务

1. 交易流程

下面我们来看下基础服务组件中的交易模块,我们已完成结算功能,如图所示,在结算这个模块中我们都会进入到一个子流程【交易流程】:

对于交易,大家应该都知道,就是买东西付款,卖东西收款,在任何一个盈利的系统中,都离不开交易模块,下图是一个扫码支付的粗略流程:

  1. 收银人员发起【订单结算】,向三方服务器发起支付请求,获得二维码链接
  2. 收银人员展示二维码给客户
  3. 客户扫描二维码,向三方支付发起支付
  4. 三方支付会为收银系统和用户推送支付状态:成功/失败

交易的本质是什么?从中我们可以看出所有支付是不是都是类似的,支付系统中关心的几个维度:

1、谁收款?2、谁付款?3、价格多少?4、是否成功?5、支付渠道选择?

应用请求三方中私钥公钥的使用:

三方返回信息给应用中私钥公钥的使用:

小结:

  • 公钥加密,私钥解密
  • 应用服务器【餐掌柜】需要保存秘钥:
    • 应用服务私钥(工具生成)
    • 支付宝公钥(三方提供)
  • 三方服务器【支付宝/微信】需要保存秘钥:
    • 应用服务公钥(上传)
    • 支付宝私钥(三方保存,不对外提供)

2. 需求分析

2.1 整体流程

流程说明:

  • 用户下单成功后,系统会为其分配快递员;
  • 快递员根据取件任务进行上门取件,与用户确认物品信息、重量、体积、运费等内容,确认无误后,取件成功;
  • 快递员会询问用户,是支付宝还是微信付款,根据用户的选择,展现支付二维码;
  • 用户使用手机,打开支付宝或微信进行扫描操作,用户进行付款操作,最终会有支付成功或失败情况;
  • 后续的逻辑暂时不考虑,支付微服务只考虑支付部分的逻辑即可。

2.2 调用时序图

支付业务与其他业务相比,相对独立,所以比较适合将支付业务划分为一个微服务,而支付业务并不关系物流业务中运输、取派件等业务,只关心付款金额、付款平台、所支付的订单等。

支付微服务在整个系统架构中的业务时序图:

支付微服务的工程结构:

├─sl-express-ms-trade-api               支付Feign接口
├─sl-express-ms-trade-domain            接口DTO实体
└─sl-express-ms-trade-service           支付具体实现
    ├─com.sl.ms.trade.config				配置包,二维码、Redisson、xxl-job
	├─com.sl.ms.trade.constant				常量类包
	├─com.sl.ms.trade.controller			web控制器包
	├─com.sl.ms.trade.entity				数据库实体包
	├─com.sl.ms.trade.enums					枚举包
	├─com.sl.ms.trade.handler				三方平台的对接实现(支付宝、微信)
	├─com.sl.ms.trade.job					定时任务,扫描支付状态
	├─com.sl.ms.trade.mapper				mybatis接口
	├─com.sl.ms.trade.service				服务包
	├─com.sl.ms.trade.util					工具包

3. 支付渠道管理

支付是对接支付平台完成的,例如支付宝、微信、京东支付等,一般在这些平台上需要申请账号信息,通过这些账号信息完成与支付平台的交互,在我们的支付微服务中,将这些数据称之为【支付渠道】,并且将其存储到数据库中,通过程序可以支付渠道进行管理。

其中表中已经包含了 2 条数据,分别是支付宝和微信的账号信息,可以直接与支付平台对接。

4. 扫码支付

扫码支付的基本原理就是通过调用支付平台的接口,提交支付请求,支付平台会返回支付链接,将此支付链接生成二维码,用户通过手机上的支付宝或微信进行扫码支付。流程如下:

【交易单表 sl_trading】是指,针对于订单进行支付的记录表,其中记录了订单号,支付状态、支付平台、金额、是否有退款等信息。具体表结构如下:

所有需要对接支付的项目都需要将自身的业务订单转换成〈交易单 VO〉对象,对接交易平台。

下面展现了整体的扫描支付代码调用流程,我们将按照下面的流程进行代码的阅读。

4.1 幂等性处理

在向支付平台申请支付之前对交易单对象做幂等性处理,主要是防止重复的生成交易单以及一些业务逻辑的处理,具体是在com.sl.ms.trade.handler.impl.BeforePayHandlerImpl#idempotentCreateTrading() 中完成的。

其代码如下:

@Override
public void idempotentCreateTrading(TradingEntity tradingEntity) throws SLException {
    TradingEntity trading = tradingService.findTradByProductOrderNo(tradingEntity.getProductOrderNo());
    if (ObjectUtil.isEmpty(trading)) {
        // 新交易单,生成交易号
        Long id = Convert.toLong(identifierGenerator.nextId(tradingEntity));
        tradingEntity.setId(id);
        tradingEntity.setTradingOrderNo(id);
        return;
    }
    TradingStateEnum tradingState = trading.getTradingState();
    if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.YJS, TradingStateEnum.MD)) {
        // 已结算、免单:直接抛出重复支付异常
        throw new SLException(TradingEnum.TRADING_STATE_SUCCEED);
    } else if (ObjectUtil.equals(TradingStateEnum.FKZ, tradingState)) {
        // 付款中,如果支付渠道一致,说明是重复,抛出支付中异常,否则需要更换支付渠道
        // 举例:第一次通过支付宝付款,付款中用户取消,改换了微信支付
        if (StrUtil.equals(trading.getTradingChannel(), tradingEntity.getTradingChannel())) {
            throw new SLException(TradingEnum.TRADING_STATE_PAYING);
        } else {
            tradingEntity.setId(trading.getId()); // id设置为原订单的id
            // 新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
            tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
        }
    } else if (ObjectUtil.equalsAny(tradingState, TradingStateEnum.QXDD, TradingStateEnum.GZ)) {
        // 取消订单,挂账:创建交易号,对原交易单发起支付
        tradingEntity.setId(trading.getId()); // id设置为原订单的id
        // 重新生成交易号,在这里就会出现id 与 TradingOrderNo 数据不同的情况,其他情况下是一样的
        tradingEntity.setTradingOrderNo(Convert.toLong(identifierGenerator.nextId(tradingEntity)));
    } else {
        // 其他情况:直接交易失败
        throw new SLException(TradingEnum.PAYING_TRADING_FAIL);
    }
}

在此代码中,主要是逻辑是:

  • 如果根据订单号查询交易单数据,如果不存在说明新交易单,生成交易单号后直接返回,这里的交易单号也是使用雪花 id。
  • 如果支付状态是已经【支付成功】或【免单 - 不需要支付】,直接抛出异常。
  • 如果支付状态是【付款中】,此时有两种情况
    • 如果支付渠道相同(此前使用支付宝付款,本次也是使用支付宝付款),这种情况抛出异常
    • 如果支付渠道不同,我们是允许在生成二维码后更换支付渠道,此时需要重新生成交易单号,此时交易单号与 id 将不同。
  • 如果支付状态是【取消订单】或【挂账】,将 id 设置为原交易号,交易号重新生成,这样做的目的是既保留了原订单的交易号,又可以生成新的交易号(不重新生成的话,没有办法在支付平台进行支付申请),与之前不会有影响。

4.2 HandlerFactory

对于 NativePayHandler 会有不同平台的实现,比如:支付宝、微信,每个平台的接口参数、返回值都不一样,所以是没有办法共用的,只要是每个平台都去编写一个实现类。

那问题来了,我们该如何选择呢?

在这里我们采用了工厂模式进行获取对应的 NativePayHandler 实例,并且定义了 PayChannelHandler 父接口,在 PayChannelHandler 中定义了 PayChannelEnum payChannel();,所有 Handler 都要实现该方法用于“亮明身份”自己是哪个平台的实现,返回值是枚举。接口之间的集成关系如下:

可以看出,NativePayHandler 继承了 PayChannelHandler,它有两个实现类,分别是 AliNativePayHandler、WechatNativePayHandler,其他的后面再讲。

有了这个基础后,HandlerFactory 就好实现了,其基本原理是:根据传入的 PayChannelEnum 与 Class handler,在 Spring 容器中找到 handler 实现类,这个是多个,具体用哪个呢,再根据 handler 的 payChannel() 的返回值做比较,相同的就是要找的实例。核心代码如下:

public static <T> T get(PayChannelEnum payChannel, Class<T> handler) {
    Map<String, T> beans = SpringUtil.getBeansOfType(handler);
    for (Map.Entry<String, T> entry : beans.entrySet()) {
        Object obj = ReflectUtil.invoke(entry.getValue(), "payChannel");
        if (ObjectUtil.equal(payChannel, obj)) {
            return (T) entry.getValue();
        }
    }
    return null;
}

4.3 分布式锁

在扫描支付的方法中使用到了锁,为什么要使用锁呢?想一下这样的情况,快递员提交了支付请求,由于网络等原因一直没有返回二维码,此时快递员针对该订单又发起了一次请求,这样的话就可能针对于一个订单生成了 2 个交易单,这样就重复了,所以我们需要在处理请求生成交易单时对该订单锁定,如果获取到锁就执行,否则就抛出异常。

在这里我们使用的 Redission 的分布式锁的实现,首先要解释下为什么是用分布式锁,不是用本地锁,是因为微服务在生产部署时一般都是集群的,而我们需要的在多个节点之间锁定,并不是在一个节点内锁定,所以就要用到分布式锁。

String key = TradingCacheConstant.CREATE_PAY + productOrderNo;
// 获取公平锁,优先分配给先发出请求的线程
RLock lock = redissonClient.getFairLock(key);
try {
    // 获取锁
    if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
        // ------------ 省略部分代码 ------------
        return tradingEntity;
    }
    throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
} catch (SLException e) {
    throw e;
} catch (Exception e) {
    log.error("统一收单线下交易预创建异常:{}", ExceptionUtil.stacktraceToString(e));
    throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
} finally {
    lock.unlock();
}

4.4 生成二维码

支付宝或微信的扫码支付返回是一个链接,并不是二维码,所以我们需要根据链接生成二维码,生成二维码的库使用的是 hutool。最终生成的二维码图片使用的 base64 字符串返回给前端。

package com.sl.ms.trade.service.impl;

import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.sl.ms.trade.config.QRCodeConfig;
import com.sl.ms.trade.enums.PayChannelEnum;
import com.sl.ms.trade.service.QRCodeService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class QRCodeServiceImpl implements QRCodeService {

    @Resource
    private QRCodeConfig qrCodeConfig;

    @Override
    public String generate(String content, PayChannelEnum payChannel) {
        QrConfig qrConfig = new QrConfig();
        // 设置边距
        qrConfig.setMargin(this.qrCodeConfig.getMargin());
        // 二维码颜色
        qrConfig.setForeColor(HexUtil.decodeColor(this.qrCodeConfig.getForeColor()));
        // 设置背景色
        qrConfig.setBackColor(HexUtil.decodeColor(this.qrCodeConfig.getBackColor()));
        // 纠错级别
        qrConfig.setErrorCorrection(
            ErrorCorrectionLevel.valueOf(this.qrCodeConfig.getErrorCorrectionLevel()));
        // 设置宽
        qrConfig.setWidth(this.qrCodeConfig.getWidth());
        // 设置高
        qrConfig.setHeight(this.qrCodeConfig.getHeight());
        if (ObjectUtil.isNotEmpty(payChannel)) {
            // 设置logo
            qrConfig.setImg(this.qrCodeConfig.getLogo(payChannel));
        }
        return QrCodeUtil.generateAsBase64(content, qrConfig, ImgUtil.IMAGE_TYPE_PNG);
    }

    @Override
    public String generate(String content) {
        return generate(content, null);
    }

}

具体的配置存储在 Nacos 中:

# 二维码配置
# 边距,二维码和背景之间的边距
qrcode.margin = 2
# 二维码颜色,默认黑色
qrcode.fore-color = #000000
# 背景色,默认白色
qrcode.back-color = #ffffff
# 低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
# 纠错级别,可选参数:L、M、Q、H,默认:M
qrcode.error-correction-level = M
# 宽
qrcode.width = 300
# 高
qrcode.height = 300

配置的映射类:

package com.sl.ms.trade.config;

import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import com.sl.ms.trade.enums.PayChannelEnum;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.awt.*;

/**
 * 二维码生成参数配置
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "sl.qrcode")
public class QRCodeConfig {

    private static Image WECHAT_LOGO;
    private static Image ALIPAY_LOGO;

    static {
        WECHAT_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/wechat.png"));
        ALIPAY_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/alipay.png"));
    }

    // 边距,二维码和背景之间的边距
    private Integer margin = 2;
    // 二维码颜色,默认黑色
    private String foreColor = "#000000";
    // 背景色,默认白色
    private String backColor = "#ffffff";
    // 纠错级别,可选参数:L、M、Q、H,默认:M
    // 低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。
    // 高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。
    private String errorCorrectionLevel = "M";
    // 宽
    private Integer width = 300;
    // 高
    private Integer height = 300;

    public Image getLogo(PayChannelEnum payChannelEnum) {
        switch (payChannelEnum) {
            case ALI_PAY: {
                return ALIPAY_LOGO;
            }
            case WECHAT_PAY: {
                return WECHAT_LOGO;
            }
            default: {
                return null;
            }
        }
    }
}

5. 基础服务

在支付宝或微信平台中,支付方式是多种多样的,对于一些服务而言是通用的,比如:查询交易单、退款、查询退款等,所以我们将基于这些通用的接口封装基础服务。

5.1 查询交易

用户创建交易后,到底有没有支付成功,还是取消支付,这个可以通过查询交易单接口查询的,支付宝和微信也都提供了这样的接口服务。

a. Controller

/**
  * 统一收单线下交易查询
  * 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。
  *
  * @param tradingOrderNo 交易单号
  * @return 交易单
  */
@PostMapping("query/{tradingOrderNo}")
@ApiOperation(value = "查询统一收单线下交易", notes = "查询统一收单线下交易")
@ApiImplicitParam(name = "tradingOrderNo", value = "交易单", required = true)
public TradingDTO queryTrading(@PathVariable("tradingOrderNo") Long tradingOrderNo) {
    return this.basicPayService.queryTrading(tradingOrderNo);
}

b. Service

在 Service 中实现了交易单查询的逻辑,代码结构与扫描支付类似。具体与支付平台的对接由 BasicPayHandler 完成。

@Override
public TradingDTO queryTrading(Long tradingOrderNo) throws SLException {
    // 通过单号查询交易单数据
    TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
    // 查询前置处理:检测交易单参数
    this.beforePayHandler.checkQueryTrading(trading);
    String key = TradingCacheConstant.QUERY_PAY + tradingOrderNo;
    RLock lock = redissonClient.getFairLock(key);
    try {
        // 获取锁
        if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
            // 选取不同的支付渠道实现
            BasicPayHandler handler = HandlerFactory.get(
                trading.getTradingChannel(), BasicPayHandler.class);
            Boolean result = handler.queryTrading(trading);
            if (result) {
                // 如果交易单已经完成,需要将二维码数据删除,节省数据库空间,如果有需要可以再次生成
                if (ObjectUtil.equalsAny(trading.getTradingState(), 
                                         radingStateEnum.YJS, TradingStateEnum.QXDD)) {
                    trading.setQrCode("");
                }
                // 更新数据
                this.tradingService.saveOrUpdate(trading);
            }
            return BeanUtil.toBean(trading, TradingDTO.class);
        }
        throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
    } catch (SLException e) {
        throw e;
    } catch (Exception e) {
        log.error("查询交易单数据异常: trading = {}", trading, e);
        throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
    } finally {
        lock.unlock();
    }
}

c. 支付宝实现

@Override
public Boolean queryTrading(TradingEntity trading) throws SLException {
    // 查询配置
    Config config = AlipayConfig.getConfig(trading.getEnterpriseId());
    // Factory使用配置
    Factory.setOptions(config);
    AlipayTradeQueryResponse queryResponse;
    try {
        // 调用支付宝API:通用查询支付情况
        queryResponse = Factory
            .Payment
            .Common()
            .query(String.valueOf(trading.getTradingOrderNo()));
    } catch (Exception e) {
        String msg = StrUtil.format("查询支付宝统一下单失败:trading = {}", trading);
        log.error(msg, e);
        throw new SLException(msg, 
                              TradingEnum.NATIVE_QUERY_FAIL.getCode(), 
                              TradingEnum.NATIVE_QUERY_FAIL.getStatus());
    }
    // 修改交易单状态
    trading.setResultCode(queryResponse.getSubCode());
    trading.setResultMsg(queryResponse.getSubMsg());
    trading.setResultJson(JSONUtil.toJsonStr(queryResponse));
    boolean success = ResponseChecker.success(queryResponse);
    // 响应成功,分析交易状态
    if (success) {
        String tradeStatus = queryResponse.getTradeStatus();
        if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatus)) {
            // 支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)
            trading.setTradingState(TradingStateEnum.QXDD);
        } else if (StrUtil.equalsAny(tradeStatus, 
                        TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) {
            // TRADE_SUCCESS(交易支付成功)
            // TRADE_FINISHED(交易结束,不可退款)
            trading.setTradingState(TradingStateEnum.YJS);
        } else {
            // 非最终状态不处理,当前交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)不处理
            return false;
        }
        return true;
    }
    throw new SLException(trading.getResultJson(), 
                TradingEnum.NATIVE_QUERY_FAIL.getCode(), TradingEnum.NATIVE_QUERY_FAIL.getStatus());
}

d. 微信支付实现

@Override
public Boolean queryTrading(TradingEntity trading) throws SLException {
    // 获取微信支付的client对象
    WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId());
    // 请求地址
    String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}", trading.getTradingOrderNo());
    // 请求参数
    Map<String, Object> params = MapUtil.<String, Object>builder()
        .put("mchid", client.getMchId())
        .build();
    WeChatResponse response;
    try {
        response = client.doGet(apiPath, params);
    } catch (Exception e) {
        log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
        throw new SLException(NATIVE_REFUND_FAIL, e);
    }
    if (response.isOk()) {
        JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
        // 交易状态,枚举值:
        // SUCCESS:支付成功
        // REFUND:转入退款
        // NOTPAY:未支付
        // CLOSED:已关闭
        // REVOKED:已撤销(仅付款码支付会返回)
        // USERPAYING:用户支付中(仅付款码支付会返回)
        // PAYERROR:支付失败(仅付款码支付会返回)
        String tradeStatus = jsonObject.getStr("trade_state");
        if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) {
            trading.setTradingState(TradingStateEnum.QXDD);
        } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_REFUND_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) {
            trading.setTradingState(TradingStateEnum.YJS);
        } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_NOTPAY)) {
            // 如果是未支付,需要判断下时间,超过2小时未知的订单需要关闭订单以及设置状态为QXDD
            long between = LocalDateTimeUtil.between(trading.getCreated(), LocalDateTimeUtil.now(), ChronoUnit.HOURS);
            if (between >= 2) {
                return this.closeTrading(trading);
            }
        } else {
            // 非最终状态不处理
            return false;
        }
        // 修改交易单状态
        trading.setResultCode(tradeStatus);
        trading.setResultMsg(jsonObject.getStr("trade_state_desc"));
        trading.setResultJson(response.getBody());
        return true;
    }
    throw new SLException(response.getBody(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getCode());
}

5.2 退款

a. Controller

/**
  * 统一收单交易退款接口
  * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,
  * 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
  *
  * @param tradingOrderNo 交易单号
  * @param refundAmount 退款金额
  * @return
  */
@PostMapping("refund")
@ApiOperation(value = "统一收单交易退款", notes = "统一收单交易退款")
@ApiImplicitParams({
    @ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true),
    @ApiImplicitParam(name = "refundAmount", value = "退款金额", required = true)
})
public void refundTrading(@RequestParam("tradingOrderNo") Long tradingOrderNo,
                          @RequestParam("refundAmount") BigDecimal refundAmount) {
    Boolean result = this.basicPayService.refundTrading(tradingOrderNo, refundAmount);
    if (!result) {
        throw new SLException(TradingEnum.BASIC_REFUND_COUNT_OUT_FAIL);
    }
}

b. Service

@Override
@Transactional
public Boolean refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws SLException {
    // 通过单号查询交易单数据
    TradingEntity trading = tradingService.findTradByTradingOrderNo(tradingOrderNo);
    // 设置退款金额
    trading.setRefund(NumberUtil.add(refundAmount, trading.getRefund()));
    // 入库前置检查
    this.beforePayHandler.checkRefundTrading(trading);
    String key = TradingCacheConstant.REFUND_PAY + tradingOrderNo;
    RLock lock = redissonClient.getFairLock(key);
    try {
        // 获取锁
        if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
            // 幂等性的检查
            RefundRecordEntity refundRecord = beforePayHandler.idempotentRefundTrading(trading, refundAmount);
            if (null == refundRecord) {
                return false;
            }
            // 选取不同的支付渠道实现
            BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
            Boolean result = handler.refundTrading(refundRecord);
            if (result) {
                // 更新退款记录数据
                this.refundRecordService.saveOrUpdate(refundRecord);
                // 设置交易单是退款订单
                trading.setIsRefund(Constants.YES);
                this.tradingService.saveOrUpdate(trading);
            }
            return true;
        }
        throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
    } catch (SLException e) {
        throw e;
    } catch (Exception e) {
        log.error("查询交易单数据异常:{}", ExceptionUtil.stacktraceToString(e));
        throw new SLException(TradingEnum.NATIVE_QUERY_FAIL);
    } finally {
        lock.unlock();
    }
}

c. 支付宝实现

@Override
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
    // 查询配置
    Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
    // Factory使用配置
    Factory.setOptions(config);
    // 调用支付宝API:通用查询支付情况
    AlipayTradeRefundResponse refundResponse;
    try {
        // 支付宝easy sdk
        refundResponse = Factory
            .Payment
            .Common()
            // 扩展参数:退款单号
            .optional("out_request_no", refundRecord.getRefundNo())
            .refund(Convert.toStr(refundRecord.getTradingOrderNo()),
                    Convert.toStr(refundRecord.getRefundAmount()));
    } catch (Exception e) {
        String msg = StrUtil.format("调用支付宝退款接口出错!refundRecord = {}", refundRecord);
        log.error(msg, e);
        throw new SLException(msg, TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
    }
    refundRecord.setRefundCode(refundResponse.getSubCode());
    refundRecord.setRefundMsg(JSONUtil.toJsonStr(refundResponse));
    boolean success = ResponseChecker.success(refundResponse);
    if (success) {
        refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
        return true;
    }
    throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
}

d. 微信实现

@Override
public Boolean refundTrading(RefundRecordEntity refundRecord) throws SLException {
    // 获取微信支付的client对象
    WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
    // 请求地址
    String apiPath = "/v3/refund/domestic/refunds";
    // 请求参数
    Map<String, Object> params = MapUtil.<String, Object>builder()
        .put("out_refund_no", Convert.toStr(refundRecord.getRefundNo()))
        .put("out_trade_no", Convert.toStr(refundRecord.getTradingOrderNo()))
        .put("amount", MapUtil.<String, Object>builder()
             .put("refund", NumberUtil.mul(refundRecord.getRefundAmount(), 100)) // 本次退款金额
             .put("total", NumberUtil.mul(refundRecord.getTotal(), 100)) // 原订单金额
             .put("currency", "CNY") // 币种
             .build())
        .build();
    WeChatResponse response;
    try {
        response = client.doPost(apiPath, params);
    } catch (Exception e) {
        log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e);
        throw new SLException(NATIVE_REFUND_FAIL, e);
    }
    refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
    refundRecord.setRefundMsg(response.getBody());
    if (response.isOk()) {
        JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
        // SUCCESS:退款成功
        // CLOSED:退款关闭
        // PROCESSING:退款处理中
        // ABNORMAL:退款异常
        String status = jsonObject.getStr("status");
        if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
            refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
        } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
            refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
        } else {
            refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
        }
        return true;
    }
    throw new SLException(refundRecord.getRefundMsg(), NATIVE_REFUND_FAIL.getCode(), NATIVE_REFUND_FAIL.getStatus());
}

5.3 查询退款

a. Controller

/**
  * 统一收单交易退款查询接口
  * @param refundNo 退款交易单号
  * @return
  */
@PostMapping("refund/{refundNo}")
@ApiOperation(value = "查询统一收单交易退款", notes = "查询统一收单交易退款")
@ApiImplicitParam(name = "refundNo", value = "退款交易单", required = true)
public RefundRecordDTO queryRefundDownLineTrading(@PathVariable("refundNo") Long refundNo) {
    return this.basicPayService.queryRefundTrading(refundNo);
}

b. Service

@Override
public RefundRecordDTO queryRefundTrading(Long refundNo) throws SLException {
    // 通过单号查询交易单数据
    RefundRecordEntity refundRecord = this.refundRecordService.findByRefundNo(refundNo);
    // 查询前置处理
    this.beforePayHandler.checkQueryRefundTrading(refundRecord);
    String key = TradingCacheConstant.REFUND_QUERY_PAY + refundNo;
    RLock lock = redissonClient.getFairLock(key);
    try {
        // 获取锁
        if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
            // 选取不同的支付渠道实现
            BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class);
            Boolean result = handler.queryRefundTrading(refundRecord);
            if (result) {
                // 更新数据
                this.refundRecordService.saveOrUpdate(refundRecord);
            }
            return BeanUtil.toBean(refundRecord, RefundRecordDTO.class);
        }
        throw new SLException(TradingEnum.REFUND_FAIL);
    } catch (SLException e) {
        throw e;
    } catch (Exception e) {
        log.error("查询退款交易单数据异常: refundRecord = {}", refundRecord, e);
        throw new SLException(TradingEnum.REFUND_FAIL);
    } finally {
        lock.unlock();
    }
}

c. 支付宝实现

@Override
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
    // 查询配置
    Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId());
    // Factory使用配置
    Factory.setOptions(config);
    AlipayTradeFastpayRefundQueryResponse response;
    try {
        response = Factory.Payment.Common().queryRefund(
            Convert.toStr(refundRecord.getTradingOrderNo()),
            Convert.toStr(refundRecord.getRefundNo()));
    } catch (Exception e) {
        log.error("调用支付宝查询退款接口出错!refundRecord = {}", refundRecord, e);
        throw new SLException(TradingEnum.NATIVE_REFUND_FAIL, e);
    }
    refundRecord.setRefundCode(response.getSubCode());
    refundRecord.setRefundMsg(JSONUtil.toJsonStr(response));
    boolean success = ResponseChecker.success(response);
    if (success) {
        refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
        return true;
    }
    throw new SLException(refundRecord.getRefundMsg(), TradingEnum.NATIVE_REFUND_FAIL.getCode(), TradingEnum.NATIVE_REFUND_FAIL.getStatus());
}

d. 微信支付

@Override
public Boolean queryRefundTrading(RefundRecordEntity refundRecord) throws SLException {
    // 获取微信支付的client对象
    WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId());
    // 请求地址
    String apiPath = StrUtil.format("/v3/refund/domestic/refunds/{}", refundRecord.getRefundNo());
    WeChatResponse response;
    try {
        response = client.doGet(apiPath);
    } catch (Exception e) {
        log.error("调用微信接口出错!apiPath = {}", apiPath, e);
        throw new SLException(NATIVE_QUERY_REFUND_FAIL, e);
    }
    refundRecord.setRefundCode(Convert.toStr(response.getStatus()));
    refundRecord.setRefundMsg(response.getBody());
    if (response.isOk()) {
        JSONObject jsonObject = JSONUtil.parseObj(response.getBody());
        // SUCCESS:退款成功
        // CLOSED:退款关闭
        // PROCESSING:退款处理中
        // ABNORMAL:退款异常
        String status = jsonObject.getStr("status");
        if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) {
            refundRecord.setRefundStatus(RefundStatusEnum.SENDING);
        } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) {
            refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS);
        } else {
            refundRecord.setRefundStatus(RefundStatusEnum.FAIL);
        }
        return true;
    }
    throw new SLException(response.getBody(), NATIVE_QUERY_REFUND_FAIL.getCode(), NATIVE_QUERY_REFUND_FAIL.getStatus());
}

6. 同步支付状态

在支付平台创建交易单后,如果用户支付成功,我们怎么知道支付成功了呢?一般的做法有两种,分别是【异步通知】和【主动查询】,基本的流程如下:

说明:

  • 在用户支付成功后,支付平台会通知【支付微服务】,这个就是异步通知,需要在【支付微服务】中对外暴露接口
  • 由于网络的不确定性,异步通知可能出现故障
  • 支付微服务中需要有定时任务,查询正在支付中的订单的状态
  • 可以看出【异步通知】与【主动定时查询】这两种方式是互不的,缺一不可。

6.1 异步通知

异步通知的是需要通过外网的域名地址请求到的,由于我们还没有真正上线,那支付平台如何请求到我们本地服务的呢?

这里可以使用【内网穿透】技术来实现,通过【内网穿透软件】将内网与外网通过隧道打通,外网可以读取内网中的数据。

在这里推荐 2 个免费的内网穿透服务,分别是:cpolar、NATAPP

a. NotifyController

@RestController
@Api(tags = "支付通知")
@RequestMapping("notify")
public class NotifyController {
    @Resource
    private NotifyService notifyService;

    /**
     * 微信支付成功回调
     *
     * @param httpEntity   微信请求信息
     * @param enterpriseId 商户id
     * @return 正常响应200,否则响应500
     */
    @PostMapping("wx/{enterpriseId}")
    public ResponseEntity<String> wxPayNotify(HttpEntity<String> httpEntity, @PathVariable("enterpriseId") Long enterpriseId) {
        try {
            // 获取请求头
            HttpHeaders headers = httpEntity.getHeaders();
            // 构建微信请求数据对象
            NotificationRequest request = new NotificationRequest.Builder()
                    .withSerialNumber(headers.getFirst("Wechatpay-Serial")) // 证书序列号(微信平台)
                    .withNonce(headers.getFirst("Wechatpay-Nonce"))         // 随机串
                    .withTimestamp(headers.getFirst("Wechatpay-Timestamp")) // 时间戳
                    .withSignature(headers.getFirst("Wechatpay-Signature")) // 签名字符串
                    .withBody(httpEntity.getBody())
                    .build();
            // 微信通知的业务处理
            this.notifyService.wxPayNotify(request, enterpriseId);
        } catch (SLException e) {
            Map<String, Object> result = MapUtil.<String, Object>builder()
                    .put("code", "FAIL")
                    .put("message", e.getMsg())
                    .build();
            // 响应500
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(JSONUtil.toJsonStr(result));
        }
        return ResponseEntity.ok("success");
    }

    /**
     * 支付宝支付成功回调
     *
     * @param enterpriseId 商户id
     * @return 正常响应200,否则响应500
     */
    @PostMapping("alipay/{enterpriseId}")
    public ResponseEntity<String> aliPayNotify(HttpServletRequest request,
                                               @PathVariable("enterpriseId") Long enterpriseId) {
        try {
            // 支付宝通知的业务处理
            this.notifyService.aliPayNotify(request, enterpriseId);
        } catch (SLException e) {
            // 响应500
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
        return ResponseEntity.ok("success");
    }
}

b. NotifyService

public interface NotifyService {
    /**
     * 微信支付通知,官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
     *
     * @param request      微信请求对象
     * @param enterpriseId 商户id
     * @throws SLException 抛出SL异常,通过异常决定是否响应200
     */
    void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException;
    /**
     * 支付宝支付通知,官方文档:https://opendocs.alipay.com/open/194/103296?ref=api
     *
     * @param request      请求对象
     * @param enterpriseId 商户id
     * @throws SLException 抛出SL异常,通过异常决定是否响应200
     */
    void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException;
}

注意:

  • 支付成功的通知请求,一定要确保是真正来自支付平台,防止伪造请求造成数据错误,导致财产损失
  • 对于响应回的数据需要进行解密处理
@Slf4j
@Service
public class NotifyServiceImpl implements NotifyService {

    @Resource
    private TradingService tradingService;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private MQFeign mqFeign;

    @Override
    public void wxPayNotify(NotificationRequest request, Long enterpriseId) throws SLException {
        // 查询配置
        WechatPayHttpClient client = WechatPayHttpClient.get(enterpriseId);
        JSONObject jsonData;
        // 验证签名,确保请求来自微信
        try {
            // 确保在管理器中存在自动更新的商户证书
            client.createHttpClient();
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            Verifier verifier = certificatesManager.getVerifier(client.getMchId());
            // 验签和解析请求数据
            NotificationHandler notificationHandler = new NotificationHandler(verifier, client.getApiV3Key().getBytes(StandardCharsets.UTF_8));
            Notification notification = notificationHandler.parse(request);
            if (!StrUtil.equals("TRANSACTION.SUCCESS", notification.getEventType())) {
                // 非成功请求直接返回,理论上都是成功的请求
                return;
            }
            // 获取解密后的数据
            jsonData = JSONUtil.parseObj(notification.getDecryptData());
        } catch (Exception e) {
            throw new SLException("验签失败");
        }
        if (!StrUtil.equals(jsonData.getStr("trade_state"), TradingConstant.WECHAT_TRADE_SUCCESS)) {
            return;
        }
        // 交易单号
        Long tradingOrderNo = jsonData.getLong("out_trade_no");
        log.info("微信支付通知:tradingOrderNo = {}, data = {}", tradingOrderNo, jsonData);
        // 更新交易单
        this.updateTrading(tradingOrderNo, jsonData.getStr("trade_state_desc"), jsonData.toString());
    }
    private void updateTrading(Long tradingOrderNo, String resultMsg, String resultJson) {
        String key = TradingCacheConstant.CREATE_PAY + tradingOrderNo;
        RLock lock = redissonClient.getFairLock(key);
        try {
            // 获取锁
            if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) {
                TradingEntity trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo);
                if (trading.getTradingState() == TradingStateEnum.YJS) {
                    // 已付款
                    return;
                }
                // 设置成付款成功
                trading.setTradingState(TradingStateEnum.YJS);
                // 清空二维码数据
                trading.setQrCode("");
                trading.setResultMsg(resultMsg);
                trading.setResultJson(resultJson);
                this.tradingService.saveOrUpdate(trading);
                // 发消息通知其他系统支付成功
                TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
                        .tradingOrderNo(trading.getTradingOrderNo())
                        .productOrderNo(trading.getProductOrderNo())
                        .statusCode(TradingStateEnum.YJS.getCode())
                        .statusName(TradingStateEnum.YJS.name())
                        .build();
                String msg = JSONUtil.toJsonStr(Collections.singletonList(tradeStatusMsg));
                this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, msg);
                return;
            }
        } catch (Exception e) {
            throw new SLException("处理业务失败");
        } finally {
            lock.unlock();
        }
        throw new SLException("处理业务失败");
    }

    @Override
    public void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws SLException {
        // 获取参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        Map<String, String> param = new HashMap<>();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            param.put(entry.getKey(), StrUtil.join(",", entry.getValue()));
        }
        String tradeStatus = param.get("trade_status");
        if (!StrUtil.equals(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS)) {
            return;
        }
        // 查询配置
        Config config = AlipayConfig.getConfig(enterpriseId);
        Factory.setOptions(config);
        try {
            Boolean result = Factory
                    .Payment
                    .Common().verifyNotify(param);
            if (!result) {
                throw new SLException("验签失败");
            }
        } catch (Exception e) {
            throw new SLException("验签失败");
        }
        // 获取交易单号
        Long tradingOrderNo = Convert.toLong(param.get("out_trade_no"));
        // 更新交易单
        this.updateTrading(tradingOrderNo, "支付成功", JSONUtil.toJsonStr(param));
    }
}

c. 网关对外暴露接口

bootsarp-{profile}.yml 中增加如下内容:

- id: sl-express-ms-trade
  uri: lb://sl-express-ms-trade
  predicates:
    - Path=/trade/notify/**
  filters:
    - StripPrefix=1
    - AddRequestHeader=X-Request-From, sl-express-gateway

说明:对于支付系统在网关中的暴露仅仅暴露通知接口,其他接口不暴露。

6.2 定时任务

一般在项目中实现定时任务主要是两种技术方案,一种是 Spring Task,另一种是 xxl-job,其中 Spring Task 是适合单体项目中使用,而 xxl-job 是分布式任务调度框架,更适合在分布式项目中使用,所以在支付微服务中我们将采用 xxl-job 来实现。

a. xxl-job

在微服务架构体系中,服务之间通过网络交互来完成业务处理的,在分布式架构下,一个服务往往会部署多个实例来运行我们的业务,如果在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度

分布式系统的特点,并且提高任务的调度处理能力:

  • 并行任务调度
    • 集群部署单个服务,这样就可以多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。
  • 高可用
    • 若某一个实例宕机,不影响其他实例来执行任务。
  • 弹性扩容
    • 当集群中增加实例就可以提高并执行任务的处理效率。
  • 任务管理与监测
    • 对系统中存在的所有定时任务进行统一的管理及监测。
    • 让开发人员及运维人员能够时刻了解任务执行情况,从而做出快速的应急处理响应。

XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

我们采用 docker 进行部署安装 xxl-job 的调度中心,安装命令:

docker run \
-e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.150.101:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 \
--spring.datasource.username=root \
--spring.datasource.password=123" \
--restart=always \
-p 28080:8080 \
-v xxl-job-admin-applogs:/data/applogs \
--name xxl-job-admin \
-d \
xuxueli/xxl-job-admin:2.3.0

xxl-job 共用到 8 张表:

  • xxl_job_lock:任务调度锁表;
  • xxl_job_group:执行器信息表,维护任务执行器信息;
  • xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
  • xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
  • xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
  • xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
  • xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
  • xxl_job_user:系统用户表;

xxl-job 支持的路由策略非常丰富:

  • FIRST(第一个):固定选择第一个机器;
  • LAST(最后一个):固定选择最后一个机器;
  • ROUND(轮询):在线的机器按照顺序一次执行一个
  • RANDOM(随机):随机选择在线的机器;
  • CONSISTENT_HASH(一致性 HASH):每个任务按照 Hash 算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
  • LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
  • LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
  • FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
  • BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
  • SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

调度流程:

b. TradeJob

在此任务中包含两个任务,一个是查询支付状态,另一个是查询退款状态。

@Slf4j
@Component
public class TradeJob {

    @Value("${sl.job.trading.count:100}")
    private Integer tradingCount;
    @Value("${sl.job.refund.count:100}")
    private Integer refundCount;
    @Resource
    private TradingService tradingService;
    @Resource
    private RefundRecordService refundRecordService;
    @Resource
    private BasicPayService basicPayService;
    @Resource
    private MQFeign mqFeign;

    /**
     * 分片广播方式查询支付状态
     * 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理
     */
    @XxlJob("tradingJob")
    public void tradingJob() {
        // 分片参数
        int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
        int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
        List<TradingEntity> list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount);
        if (CollUtil.isEmpty(list)) {
            XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
            return;
        }
        // 定义消息通知列表,只要是状态不为【付款中】就需要通知其他系统
        List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
        for (TradingEntity trading : list) {
            if (trading.getTradingOrderNo() % shardTotal != shardIndex) {
                continue;
            }
            try {
                // 查询交易单
                TradingDTO tradingDTO = this.basicPayService.queryTrading(trading.getTradingOrderNo());
                if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) {
                    TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
                            .tradingOrderNo(trading.getTradingOrderNo())
                            .productOrderNo(trading.getProductOrderNo())
                            .statusCode(tradingDTO.getTradingState().getCode())
                            .statusName(tradingDTO.getTradingState().name())
                            .build();
                    tradeMsgList.add(tradeStatusMsg);
                }
            } catch (Exception e) {
                XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", 
                                 shardIndex, shardTotal, trading, e);
            }
        }
        if (CollUtil.isEmpty(tradeMsgList)) {
            return;
        }
        // 发送消息通知其他系统
        String msg = JSONUtil.toJsonStr(tradeMsgList);
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, 
                             Constants.MQ.RoutingKeys.TRADE_UPDATE_STATUS, 
                             msg);
    }
    
    /**
     * 分片广播方式查询退款状态
     */
    @XxlJob("refundJob")
    public void refundJob() {
        // 分片参数
        int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0);
        int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1);
        List<RefundRecordEntity> list = this.refundRecordService.findListByRefundStatus(RefundStatusEnum.SENDING, refundCount);
        if (CollUtil.isEmpty(list)) {
            XxlJobHelper.log("查询到退款单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal);
            return;
        }
        // 定义消息通知列表,只要是状态不为【退款中】就需要通知其他系统
        List<TradeStatusMsg> tradeMsgList = new ArrayList<>();
        for (RefundRecordEntity refundRecord : list) {
            if (refundRecord.getRefundNo() % shardTotal != shardIndex) {
                continue;
            }
            try {
                // 查询退款单
                RefundRecordDTO refundRecordDTO = this.basicPayService.queryRefundTrading(refundRecord.getRefundNo());
                if (RefundStatusEnum.SENDING != refundRecordDTO.getRefundStatus()) {
                    TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder()
                            .tradingOrderNo(refundRecord.getTradingOrderNo())
                            .productOrderNo(refundRecord.getProductOrderNo())
                            .refundNo(refundRecord.getRefundNo())
                            .statusCode(refundRecord.getRefundStatus().getCode())
                            .statusName(refundRecord.getRefundStatus().name())
                            .build();
                    tradeMsgList.add(tradeStatusMsg);
                }
            } catch (Exception e) {
                XxlJobHelper.log("查询退款单出错!shardIndex = {}, shardTotal = {}, refundRecord = {}",
                                 shardIndex, shardTotal, refundRecord, e);
            }
        }
        if (CollUtil.isEmpty(tradeMsgList)) {
            return;
        }
        // 发送消息通知其他系统
        String msg = JSONUtil.toJsonStr(tradeMsgList);
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, 
                             Constants.MQ.RoutingKeys.REFUND_UPDATE_STATUS, 
                             msg);
    }
}
posted @ 2024-04-18 08:41  tree6x7  阅读(10)  评论(0编辑  收藏  举报