一个正确的支付系统应如何设计?支付及回调

功能实现

1、实现小程序端展示金额输入界面、支付按钮
2、实现小程序下单接口
3、小程序调起微信支付接口
4、小程序通知回调url
5、定时任务查单
6、取消订单

小程序金额输入界面:

小程序支付界面如下所示:


一个正确的支付系统应如何设计?支付及回调

微信下单接口开发

输入金额后点击支付按钮,调用小程序下单api(后端接口调用),创建WxPayController用来接收小程序的请求。

@ApiOperation("小程序统一下单API")
@PostMapping("/jsApiPay")
@RequiresAuthentication
public R jsApiPay(@RequestBody Map<String, Object> productInfo) throws Exception {
log.info("发起支付请求 v3");
//返回预付费标识id和订单号
Map<String, Object> map = wxPayService.jsApiPay(productInfo);
return R.ok().setData(map);
}

创建WxPayServiceImpl编写jsApiPay 方法:

@Override
public Map<String, Object> jsApiPay(Map productInfo) throws Exception {

Gson gson = new Gson();
//获取用户标识
Object openId = productInfo.get("openId");
if (openId == null) {
throw new RuntimeException("用户标识为空,出现错误!");
}

log.info("生成订单");

//生成订单(根据用户id)
DepositOrderInfo orderInfo = depositOrderInfoService.createOrderByProductInfo(productInfo);
if (orderInfo != null) {
// 获取预付费标识id 这里有两种方案,:丛redis中获取,丛数据库中获取
String prepayId = orderInfo.getPrepayId();
//核查prepayid的有效性(根据redis存放的过期时间进行判断)
// 如果这里超时,还需要重新下单,而且将原来那笔订单的状态更新为超时
if (this.checkParams(prepayId)) {
log.info("订单已存在,不需要重新下单");
//返回预付费id
Map<String, Object> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
map.put("prepayResponse", gson.fromJson(orderInfo.getPrepayResponse(), PrepayWithRequestPaymentResponse.class));
return map;
}
}

log.info("调用统一下单API");

log.info("商户config配置为==="+config);
if (config == null){
config = WxInitUtils.getInstance(wxPayConfig);
}
JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
// 填充预下单参数
PrepayRequest request = new PrepayRequest();
//appid
request.setAppid(wxPayConfig.getAppid());
//商户id
request.setMchid(wxPayConfig.getMchId());
//产品描述
assert orderInfo != null;
request.setDescription(orderInfo.getOrderTitle());
//商户订单号
request.setOutTradeNo(orderInfo.getOrderNo());
//通知url
request.setNotifyUrl(wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

//金额信息
Amount amount = new Amount();
amount.setTotal(orderInfo.getTotalFee());
amount.setCurrency("CNY");
request.setAmount(amount);

//用户信息
Payer payer = new Payer();
payer.setOpenid(openId.toString());
request.setPayer(payer);

log.info("请求参数 ===> {}" + request.toString());
PrepayWithRequestPaymentResponse response = null;

try {
// response包含了调起支付所需的所有参数,可直接用于前端调起支付
response = service.prepayWithRequestPayment(request);
} catch (Exception e) {
log.error("请求下单失败,错误信息" + e.getMessage());
throw new RuntimeException("请求下单失败,错误信息" + e.getMessage());
}

//处理返回值
assert response != null;
String packageVal = response.getPackageVal();
String prepayId = packageVal;
//预支付签名串
String paySign = response.getPaySign();

// 保存prepay_id到数据库,或redis中,并设置有效期为2小时 保存prepayId、paySign
String orderNo = orderInfo.getOrderNo();
depositOrderInfoService.updateOrderPrepayInfo(orderNo, prepayId, response);

//返回预付费订单信息
Map<String, Object> map = new HashMap<>();
map.put("prepayResponse", response);
map.put("orderNo", orderInfo.getOrderNo());
return map;

/**
* 执行状态 :已经执行
* 前端备注:前端拿到prepay_id 后需要进行:
* 1、调起小程序支付前端api
* 2、开启一个前端定时器,用来实时查询订单的状态,支付成功后进行页面跳转。
*/

}

在此方法中,主要实现三个步骤:
(1)生成订单: 往订单表(ORDER_INFO)中插入一条记录,包括支付的金额、支付的用户id、默认支付状态(待支付)。此外还要考虑针对于当前金额、当前用户频繁的下单,但是未支付这种情况进行控制,代码段如下:

@Override
public DepositOrderInfo createOrderByProductInfo(Map productInfo) {
// 获取当前登录人的user_id
String userId = UserUtil.getCurrUserInfo().getId();
//将元转为分
long totalFee= DataTypeUtils.yuanToFen(new BigDecimal(productInfo.get("totalFee").toString())) ;
//根据当前用户id查找已存在但未支付的订单,如果未支付的订单存在则不再生成新的订单。
//totalFee为下单金额,新加原因为:用户取消支付后又重新支付,这个判断金额相同还是用之前的订单,金额不同则重新下单
DepositOrderInfo orderInfo = this.getNoPayOrderByUserId(userId,totalFee);
//如果金额不同则任务是新的订单
if( orderInfo != null){
return orderInfo;
}
//生成订单数据
orderInfo = new DepositOrderInfo();
orderInfo.setOrderTitle("保证金缴纳");
//订单号
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
//默认已退金额为0
orderInfo.setRefundFee(0);
// 需要丛productInfo中取值
orderInfo.setTotalFee((int)totalFee);
// 设置订单状态默认值为未支付
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//默认设置微信付款状态为未支付、在小程序微信支付回调中修改这个值
orderInfo.setTradeState(OrderStatus.NOTPAY.getType());
//设置退款状态初始值为未退款
orderInfo.setRefundStatus(OrderStatus.NO_REFUND.getType());
orderInfo.setUserId(userId);
basemapper.insert(orderInfo);
return orderInfo;
}

(2)为了避免重复生成订单数据 ,所以在下单后会判断之前是否生成过,如果生成过则不需要重新生成。微信下单接口会返回一个prepayID,此字段的有效期为2小时。这里可以根据实际业务进行处理订单的有效期,这里我是直接存到了数据库。代码块如下,如果之前创建过订单就直接返回给前端小程序进行下单。

if (orderInfo != null) {     
// 获取预付费标识id 这里有两种方案,:丛redis中获取,丛数据库中获取
String prepayId = orderInfo.getPrepayId();
//核查prepayid的有效性(根据redis存放的过期时间进行判断)
// 如果这里超时,还需要重新下单,而且将原来那笔订单的状态更新为超时
if (this.checkParams(prepayId)) {
log.info("订单已存在,不需要重新下单");
//返回预付费id
Map<String, Object> map = new HashMap<>();
map.put("orderNo", orderInfo.getOrderNo());
map.put("prepayResponse", gson.fromJson(orderInfo.getPrepayResponse(), PrepayWithRequestPaymentResponse.class));
return map;
}
}

(3)调用sdk api实现微信下单,并根据下单返回的结果更新订单的prepayID的值 ,这个在上面完整的方法中已经写出。注意的是,服务端的sdk已经为我们封装了加签和验签的相关方法,我们不需要关心,只关心业务调用即可。
一个正确的支付系统应如何设计?支付及回调

小程序调起微信支付

微信下单后,服务端sdk给我们响应了prepayResponse,这个响应数据包括了小程序端调起微信支付的所有参数,如下:签名、appid、时间戳、随机数、支付信息(packageVal)

String message = request.getAppid() + "n" + timestamp + "n" + nonceStr + "n" + packageVal + "n";      
logger.debug("Message for RequestPayment signatures is[{}]", message);
String sign = this.signer.sign(message).getSign();
PrepayWithRequestPaymentResponse response = new PrepayWithRequestPaymentResponse();
response.setAppId(request.getAppid());
response.setTimeStamp(String.valueOf(timestamp));
response.setNonceStr(nonceStr);
response.setPackageVal(packageVal);
response.setSignType(this.signType);
response.setPaySign(sign);

小程序调起微信支付我们使用uni-app提供的api,如果使用微信原生api,可参考微信官方。

  //微信支付接口
wxRequestPayment(data){
console.log('开始调用微信支付接口')
return new Promise((resolve,reject) =>{
uni.requestPayment({
provider: 'wxpay',
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.packageVal,
signType: 'RSA',
paySign: data.paySign,
success: function(res) {
//支付成功调这个
console.log('支付成功回调'+JSON.stringify(res))
resolve(res)
},
fail: function(err) {
//取消支付或者支付失败会调这个方法
console.log('支付失败'+JSON.stringify(err))
reject(err)
}
});
})
},

上述支付方法中,调用微信支付模块来完成支付的过程,其结果有以下几种情况:

成功: 表示微信客户端已经正常打开,并且显示了支付页面。用户可以在支付页面输入密码或者使用指纹等方式确认支付,成功调起支付并不代表支付真实成功,还需要等待用户的支付操作和微信的支付结果回调。
失败: 表示微信客户端无法打开,或者打开后出现异常。可能的原因有:签名错误、未注册APPID、项目设置APPID不正确、注册的APPID与设置的不匹配、其他异常原因等。失败调起支付意味着支付流程中断,需要商户检查相关参数和设置,或者联系微信支付客服解决,失败还包括一种情况就是用户主动取消: 表示用户在微信客户端打开支付页面后,主动点击取消按钮,放弃支付。用户取消调起支付无需商户处理,但是商户可以根据业务逻辑,提示用户重新支付或者取消订单。

所以,前端返回如何区分是成功返回,还是用户取消支付或者异常的呢?有如下状态列表,如果是小程序:

回调类型 errMsg 说明
success requestPayment:ok 调用支付成功
fail requestPayment:fail cancel 用户取消支付
fail requestPayment:fail (detail message) 调用支付失败,其中 detail message 为后台返回的详细失败原因

如何判断支付成功?系统如何设计

小程序客户端调起微信支付成功并输入密码支付后,微信会同步响应调起的结果,也就是刚刚说的小程序调起微信的过程。但并不清楚微信后台是否真的支付成功了。由于网络异常或者系统的波动,可能会导致用户支付成功,但是商户侧未能成功接收到支付结果通知,进而显示订单未支付的情况。商户侧的订单状态更新不及时,容易造成用户投诉,甚至是重复支付的情况发生。

为了完成如下目标: 商户在未能收到支付结果通知时,也能及时、准确地获取到订单的支付状态,提升商户系统的健壮性,减少因为订单状态不同步导致的用户投诉,系统设计方案如下:


一个正确的支付系统应如何设计?支付及回调


(1)商户App或者前端页面收到支付返回时,商户需要调用商户查单接口确认订单状态,并把查询结果展示给用户。

(2)商户后台需要准确、高效地处理微信支付发送的异步支付结果通知,并按接口规范把处理结果返回给微信支付。

(3)商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。

(4)商户在T+1日从微信支付侧获取T日的交易账单,并与商户系统中的订单核对。如出现订单在微信支付侧成功,但是在商户侧未成功的情况,商户需要给用户补发货或者退款处理。

所以在我们实际处理时,应该这样做:

1 前端定时处理

前端支付页面开启定时器,调用商户侧查单接口,这个查单接口不是微信查单接口,而是查询本地订单状态,也就是自己写个接口查询order_info表订单的状态。此订单的状态是微信支付回调通知返回的结果。开启定时的目的是为了实时获取到真实的订单状态,并将查询的结果展示给用户。伪代码如下:

wx.request().then(response = >{    
* if(){
* //启动定时器
* * this.timer = setInterval(() => {
* * //查询订单是否支付成功
* * this.queryOrderStatus().then(res){
* //如果res为SUCCESS则跳转成功界面,并清除当前订单定时器。
* }
* * }, 3000)
* * })
* }
*  queryOrderStatus(){
* orderInfoApi.queryOrderStatus(this.orderNo).then(response => {
* console.log('查询订单状态:' + response.code)
* // 支付成功后的页面跳转
* if (response.code === 0) {
* console.log('清除定时器')
* clearInterval(this.timer)
* // 三秒后跳转到订单列表
* setTimeout(() => {
* this.$router.push({ path: '/success' })
* }, 3000)
* }
* })
* }

后端的商户查单接口如下,我们可以发现就是单纯的查询了一下订单表的信息

    @Override
public OrderInfo queryOrderStatus(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
tOrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
if(orderInfo == null){
return null;
}
return orderInfo;
}

2 后端服务处理:

2.1 支付回调处理
商户后台需要准确、高效地处理微信支付发送的异步支付结果通知,并按接口规范把处理结果返回给微信支付。详情请参考接口规范和注意事项

2.2 定时轮询查单

如果长时间没有收到支付结果通知,商户后台应该定时轮询调用《微信支付查单接口》去核实订单状态。

方案一:

以订单下单成功时间为基准(或者以前端支付返回成功或者报错后,第一次调用商户查单接口未成功的时间为基准),每隔5秒/30秒/1分钟/3分钟/5分钟/10分钟/30分钟调用《微信支付查单接口》关闭订单。(轮询时间间隔和次数,商户可以根据自身业务场景灵活设置)

方案二:

定时任务每隔30秒启动一次,找出最近10分钟内创建并且未支付的订单,调用《微信支付查单接口》核实订单状态。系统记录订单查询的次数,在10次查询之后状态还是未支付成功,则停止后续查询,并调用《关单接口 》关闭订单。(轮询时间间隔和次数,商户可以根据自身业务场景灵活设置)

支付回调

用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。微信下单时会设置通知url地址,回调URL是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为HTTPS地址。请确保回调URL是外网可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。回调URL示例:”https://pay.weixin.qq.com/wxpay/pay.action“。

所以我们首先要提供一个接口供微信平台调用,并接收微信平台支付的结果更新订单的状态,并做后续的业务操作。然而微信平台调用回调接口有一些规则:

(1)notify_url的代码处理逻辑不能做登录态校验。
(2)商户系统收到支付结果通知,需要在5秒内返回应答报文,否则微信支付认为通知失败,后续会重复发送通知。
(3)同样的通知可能会多次发送给商户系统,商户系统必须能够正确处理重复的通知。如果已处理过,直接给微信支付返回成功。
(4)商户侧对微信支付回调IP有防火墙策略限制的,需要对以下IP段开通白名单:

  上海电信出口网段:101.226.103.0/25
  上海联通出口网段:140.207.54.0/25
  上海CAP出口网段:121.51.58.128/25
  深圳电信出口网段:183.3.234.0/25
  深圳联通出口网段:58.251.80.0/25
  深圳CAP出口网段:121.51.30.128/25
  香港出口网段:203.205.219.128/25
  退款结果通知、分账动账通知IP(新增):175.24.214.208、 175.24.211.24、 175.24.213.135、     109.244.180.23、 114.132.203.119、 43.139.43.69

所以根据以上规则,我们要在开发通知回调接口时考虑这些问题。在支付回调通知接口api文档里也给我们做了一些说明和注意事项:

1、对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功,(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h – 总计 24h4m)

2、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

3、如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。

特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

有关请求报文:支付结果通知是以POST 方法访问商户设置的通知URL,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情。

支付回调的开发步骤

步骤一:验证签名

微信支付会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。

步骤二:参数解密

为了保证安全性,微信支付在回调通知,对关键信息进行了AES-256-GCM加密。商户应当按照以下的流程进行解密关键信息,解密的流程:

用商户平台上设置的APIv3密钥【微信商户平台 —>账户设置—>API安全—>设置APIv3密钥】,记为key;
获取resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),以及resource.nonce和resource.associated_data;
使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象。

步骤三:更新订单状态

如果成功更新订单状态为支付成功,其他状态根据返回的实际状态进行更新

代码实现

控制层代码:解析微信平台参数、验证签名、解密等,完整的代码如下

    @ApiOperation("小程序支付通知回调")
@PostMapping(value = "/jsapi/notify")
public ResponseEntity jsapiNotify(HttpServletRequest request, HttpServletResponse response) {
Gson gson = new Gson();
log.info("微信支付回调通知过来了");
try {
//创建一个map用来存储所有的header信息
Map<String, Object> headerMap = new HashMap<>();
//处理通知参数 1、获取header值
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
//获取一个header的名称
String name = headerNames.nextElement();
//获取该header的值
String value = request.getHeader(name);
headerMap.put(name, value);
}
log.info("通知回调中header的值===>" + headerMap);

String wechatPaySerial = headerMap.get(WECHAT_PAY_SERIAL).toString();
String wechatSignature = headerMap.get(WECHAT_PAY_SIGNATURE).toString();
String wechatpayNonce = headerMap.get(WECHAT_PAY_NONCE).toString();
String wechatTimestamp = headerMap.get(WECHAT_PAY_TIMESTAMP).toString();
// String wechatPaySignatureType = headerMap.get("Wechatpay-Signature-Type").toString();

String requestBody = HttpUtils.readData(request);
log.info("支付通知的完整数据 ===> {}", requestBody);
//反序列化请求body串得到map数据
Map<String, Object> bodyMap = gson.fromJson(requestBody, HashMap.class);
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);

// 构造 RequestParam以用来进行验签和解密
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(wechatPaySerial)
.nonce (wechatpayNonce)
.signature(wechatSignature)
.timestamp(wechatTimestamp)
.body(requestBody)
.build();

// 初始化 NotificationParser(验签、解密)
NotificationParser parser = new NotificationParser((NotificationConfig) config);

Transaction transaction = null;
try {
// 验签、解密并转换成 Transaction
transaction = parser.parse(requestParam, Transaction.class);
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
log.error("通知验签失败", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
log.info("通知验签成功");
//处理订单
wxPayService.processOrder(bodyMap, transaction);
//应答超时
//模拟接收微信端的重复通知(超过5s商户没有给微信平台回应则超时,平台继续发通知)
// TimeUnit.SECONDS.sleep(5);
// 处理成功,返回 200 OK 状态码

ResponseEntity builder = ResponseEntity.status(HttpStatus.OK).build();
return builder;

} catch (Exception e) {
e.printStackTrace();
log.error("支付回调失败");
//失败应答:如果处理失败,应返回 4xx/5xx 的状态码,例如 500 INTERNAL_SERVER_ERROR
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

下面我们来简单说一下验签和解密的过程:微信主动请求回调url时 会先使用APIv3密钥加密敏感信息,然后使用微信平台的私钥加签。商户侧收到后会首先使用微信平台的公钥来验签,验签通过后,使用APIv3对称密钥进行解密得到明文。以上验签和解密的步骤都在sdk中提供了。

得到明文Transaction transaction后,我们要做以下3部分内容:
首先,我们要根据返回的的信息处理订单的状态,这里要控制不能重复更新订单的状态,如果订单未处理,我们才处理,如果已经处理了就不处理了。
其次,由于微信会频繁的发送回调通知,为了处理并发使得线程安全,所以我们考虑使用redis分布式锁来进行控制(如果业务发布到了多个节点的情况下)

再次,对于更新订单和插入支付流水要放到同一个事务中进行,所以我们还要考虑事务的处理。

针对以上3点内容,我们主要做的代码如下,代码逻辑均在 wxPayService.processOrder(bodyMap, transaction); 中。

创建wxPayService,定义processOrder方法:

@Override
public void processOrder(Map<String, Object> bodyMap, Transaction transaction) {
/*在对业务数据进行状态检查和处理之前,支付回调重复通知处理。
要采用数据锁进行并发控制,以避免函数重入造成的数据混乱*/

//尝试获取锁:
// 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
RLock lockPayNotify = redisson.getLock(LockType.PAY_NOTIFY_LOCK.getType());
try {
//5s内拿不到锁无法执行,30s后自动释放锁,避免业务异常发生死锁
if (lockPayNotify.tryLock(5, 30, TimeUnit.SECONDS)) {
WxPayServiceImpl o =(WxPayServiceImpl) AopContext.currentProxy();
o.handleNotifyPay(bodyMap,transaction);
}
} catch (InterruptedException e) {
e.printStackTrace();
log.error("支付回调锁中断");
} finally {
//由于锁可重入,需要判断当前现存是否还持有该锁,如果是就释放锁
if (lockPayNotify.isHeldByCurrentThread()) {
log.info("支付通知回调锁被释放");
//要主动释放锁
lockPayNotify.unlock();
}
}
}

上述代码中,在 o.handleNotifyPay(bodyMap,transaction); 方法中添加了锁的处理,主要是为了控制业务数据的重复处理,以免产生脏数据,因为如果我们在回调方法中添加5秒的延时,那么就相当于微信迟迟收不到响应,微信平台就会每隔3秒就会发送一次到微信回调接口(按本人经验测试,比较频繁,不像微信平台说的那样),所以在频繁的访问下就会出现线程不安全的情况(比如针对同一个订单会插入多条支付流水记录,读取订单的状态不是最新的状态),这里为什么使用分布式锁而不是本地锁呢?因为,实际的生产环境中微服务不可避免的会以分布式方式或集群方式部署,微信平台也会根据回调的url访问到不同的节点上,这样本地锁就会失效,所以这里使用了分布式锁的处理方式。 来看看handleNotifyPay的代码块,代码中都添加了注释,请在注释中查看每句代码的含义

/**
* 支付回调处理业务数据
* @param bodyMap
* @param transaction
*/

@Override
@Transactional(rollbackFor = Exception.class)
public void handleNotifyPay(Map<String, Object> bodyMap,Transaction transaction) {
Gson gson = new Gson();
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//得到明文序列化字符串
String plainText = gson.toJson(transaction);
log.info("明文 ===> {}", plainText);

//获取商户订单号
String orderNo = transaction.getOutTradeNo();
//获取订单状态
String tradeState = transaction.getTradeState().name();
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
DepositOrderInfo orderInfo = depositOrderInfoService.getOrderStatus(orderNo);
if (orderInfo == null) {
return;
}
if (!OrderStatus.NOTPAY.getType().equals(orderInfo.getOrderStatus())) {
return;
}

//更新订单状态(应该更新为通知回调返回给我们的状态)
if (!"SUCCESS".equals(tradeState)) {
depositOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.valueOf(tradeState));
//记录支付日志
depositPaymentRecordService.createPaymentRecord(plainText, transaction, "支付回调");
return;
}
//订单支付成功
depositOrderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
depositPaymentRecordService.createPaymentRecord(plainText, transaction, "支付回调");

log.info("交易成功!【支付回调】开始执行异步任务--更新业务数据");

//如果订单的状态判断为成功才能更新业务数据
/**
* 建立新的事务(新写一个服务)(建立一个异步任务(这里暂时先不创建异步任务,因为无法进行事务的传播
* 这涉及了多线程之间的事务共享问题了),加锁)
* //更新业务数据的时候出现异常。金额更新(出现异常,插入使用记录表),在保证金使用记录表中
* 舍弃:1、客户端吊起更新(支付完成后跳转页面-去调用(前后台交互问题))
* 使用:2、服务端调起更新(必须新建一个事务去更新业务数据,但是)(建议选这种方式)
*/

//构造交易数据
TradeInfoInVo tradeInfoInVo = new TradeInfoInVo();
//订单号
tradeInfoInVo.setPayTradeNo(transaction.getOutTradeNo());
//交易金额
tradeInfoInVo.setTradeAmount(transaction.getAmount().getPayerTotal());
tradeInfoInVo.setEventType(TradeLogType.PAY.getCode());
tradeInfoInVo.setUserId(orderInfo.getUserId());
this.updateBusiDataByPayInfo(tradeInfoInVo);
}

handleNotifyPay 方法中使用了声明式事务方式,这里选择了将锁放到了事务的外面,就是为了让锁完美的生效,减少失效的概率。此方法中会根据微信给的明文解析微信的支付状态,并将实际的状态更新到订单表中,并插入支付流水记录。如果支付成功会处理业务数据,这里的业务数据就是更新保证金相关的表。更新业务数据时为了和微信支付事务隔离开,使用的是异步线程池,目的是业务数据处理的结果不影响更新订单相关的操作。

总结

本期文章我们完成了主动支付和支付回调的逻辑,下一期文章,继续完成主动查单接口,及业务数据更新的实际逻辑,如果出现异常了,该使用哪些补偿措施,并趁火打劫来完成退款逻辑。


原文始发于微信公众号(小核桃编程):一个正确的支付系统应如何设计?支付及回调

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/215979.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!