1. 交易流程
下面我们来看下基础服务组件中的交易模块,我们已完成结算功能,如图所示,在结算这个模块中我们都会进入到一个子流程【交易流程】:
对于交易,大家应该都知道,就是买东西付款,卖东西收款,在任何一个盈利的系统中,都离不开交易模块,下图是一个扫码支付的粗略流程:
- 收银人员发起【订单结算】,向三方服务器发起支付请求,获得二维码链接
- 收银人员展示二维码给客户
- 客户扫描二维码,向三方支付发起支付
- 三方支付会为收银系统和用户推送支付状态:成功/失败
交易的本质是什么?从中我们可以看出所有支付是不是都是类似的,支付系统中关心的几个维度:
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
- 默认端口映射到 28080
- 日志挂载到 /var/lib/docker/volumes/xxl-job-admin-applogs
- 通过 PARAMS 环境变量设置数据库链接参数
- 数据库脚本:https://gitee.com/xuxueli0323/xxl-job/blob/2.3.0/doc/db/tables_xxl_job.sql
- 目前已经安装完成,直接访问即可:http://xxl-job.sl-express.com/xxl-job-admin/
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);
}
}
标签:03,服务,refundRecord,SLException,trading,支付,return,退款
From: https://www.cnblogs.com/liujiaqi1101/p/18141746