03-支付服务

news/发布时间2024/5/4 15:09:10

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 {@Resourceprivate QRCodeConfig qrCodeConfig;@Overridepublic 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)) {// 设置logoqrConfig.setImg(this.qrCodeConfig.getLogo(payChannel));}return QrCodeUtil.generateAsBase64(content, qrConfig, ImgUtil.IMAGE_TYPE_PNG);}@Overridepublic 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小时未知的订单需要关闭订单以及设置状态为QXDDlong 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 sdkrefundResponse = 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 {@Resourceprivate 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();// 响应500return 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) {// 响应500return 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 {@Resourceprivate TradingService tradingService;@Resourceprivate RedissonClient redissonClient;@Resourceprivate MQFeign mqFeign;@Overridepublic 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("处理业务失败");}@Overridepublic 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-tradeuri: lb://sl-express-ms-tradepredicates:- 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;@Resourceprivate TradingService tradingService;@Resourceprivate RefundRecordService refundRecordService;@Resourceprivate BasicPayService basicPayService;@Resourceprivate 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);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/58441543.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

读天才与算法:人脑与AI的数学思维笔记03_AlphaGo

读天才与算法:人脑与AI的数学思维笔记03_AlphaGo1. 国际象棋 1.1. 1997年计算机“深蓝”(Deep Blue)击败了顶尖国际象棋手,但机器取代数学研究机构还言之尚早 1.2. 下国际象棋与数学的形式化证明颇有相似之处,但学者认为中国围棋的思维方式更能够体现数学家思考的创造性和…

框架图与动机结构化与可重定目标代码生成

框架图与动机结构化与可重定目标代码生成 用于数值计算的代码生成方法传统上侧重于优化循环嵌套的性能。相关分析侧重于标量元素,因为循环嵌套的主体通常计算单个元素。这样的分析必须考虑内存依赖性与混叠。这些方法在过去进行了深入研究,并已达到高度成熟。当从像C或Fortra…

MVCC

多版本并发控制,多个事物并发的情况下到底该访问哪个版本你解释一下MVCC?mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突, 它的底层实现主要是依赖了数据库中的三个部分,隐藏字段,undo log日志和readView读视图 隐藏字段是指:在mysql中给每…

NGINX Ingress Controller 设置未配置过的域名增加默认路由

背景 k8s 集群对应的公网 slb ip 经常被人绑定域名,监控侧经常会收集到 502 相关状态码的异常告警,着手处理这种bad case策略 1. 所有没有在ingress 配置过的域名要进行处理,即不是公司的、非法绑定到slb 上的域名要加上一条策略 2. NGINX Ingress Controller 设置未配置过…

centos7 安装 Mysql 5.7.28,详细完整教程

https://cloud.tencent.com/developer/article/18863391. 下载 MySQL yum包wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm 复制2.安装MySQL源rpm -Uvh mysql57-community-release-el7-10.noarch.rpm 复制3.安装MySQL服务端,需要等待一些时间yum ins…