支付结果通知
/**
* 支付结果-微信通知
* 一共有两层保险,第一个是分布式锁,第二个是数据库乐观锁
* @param id 交易编号
* @param request http 请求对象
*/
@Inner(value = false)
@ApiOperation("支付结果-微信通知")
@PostMapping("/transaction/{id}/result/wx") @Transactional
public void wxPayResult(@PathVariable("id") long id, HttpServletRequest request) {
Trade trade = tradeService.getById(id);
// 加入分布式锁
String lockKey = "pay_".concat(trade.getOrderNo());
redisLocker.lockUp(lockKey, 60);
String body = getRequestBody(request);
TenantPaySetup paySetting = tenantPaySetupCall.getByTenantId(trade.getTenantId());
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(body)
.build();
NotificationConfig config = wxConfigManager.getMerchantConfig(trade.getTenantId());
NotificationParser parser = new NotificationParser(config);
Transaction transaction = null;
try {
transaction = parser.parse(requestParam, Transaction.class);
} catch (Exception e) {
log.error("支付结果验证失败,拒绝处理。");
throw new RuntimeException(e);
}
// 微信支付成功
if (transaction.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
try {
if (trade.getStatus().equals(2)) {
log.info("数据已被其他线程更新,执行跳过");
} else if (trade.getStatus().equals(3)) {
// 没有成功收到微信的成功通知,被系统超时关闭了
log.info("订单关闭,走退款");
wxRefundService.create(trade, "交易已关闭,资金原路退回");
}
/* 如果同笔订单已支付成功,后面的交易记录执行退款 */
long successTradeCount = tradeService.count(
Wrappers.<Trade>lambdaQuery()
.eq(Trade::getOrderNo, trade.getOrderNo())
.eq(Trade::getStatus, 2)
);
if (successTradeCount > 0) {
log.info("同笔订单已支付成功,交易执行退款,orderNo:{}", trade.getOrderNo());
wxRefundService.create(trade, "交易已关闭,资金原路退回");
return;
}
// 支付成功,商品订单和交易记录更新为支付成功
String tradeNo = transaction.getOutTradeNo();
int version = trade.getVersion();
trade.setStatus(2);
trade.setVersion(version + 1);
boolean updateResult = tradeService.update(trade,
Wrappers.<Trade>lambdaUpdate()
.eq(Trade::getId, trade.getId())
.eq(Trade::getVersion, version)
);
if (updateResult) {
log.info("支付成功,交易记录已更新");
PaySuccessMessage successMessage = new PaySuccessMessage();
successMessage.setChannel(trade.getChannel());
successMessage.setMethod(trade.getMethod());
successMessage.setOrderNo(trade.getOrderNo());
successMessage.setTradeNo(trade.getId());
amqpTemplate.convertAndSend(trade.getQueue(), trade.getRoutingKey(), JSON.toJSONString(successMessage));
log.info("向内部系统广播成功状态");
// 交易记录已被更新,继续重试
} else {
boolean retry = true;
while (retry) {
log.info("交易记录已被更新,继续重试");
Trade lockTrade = tradeService.getById(tradeNo);
// 继续更新
if (lockTrade.getStatus().equals(1)) {
version = lockTrade.getVersion();
lockTrade.setStatus(2);
lockTrade.setVersion(version + 1);
updateResult = tradeService.update(lockTrade,
Wrappers.<Trade>lambdaUpdate()
.eq(Trade::getId, trade.getId())
.eq(Trade::getVersion, version)
);
if (updateResult) {
log.info("支付成功,交易记录已更新");
PaySuccessMessage successMessage = new PaySuccessMessage();
successMessage.setChannel(trade.getChannel());
successMessage.setMethod(trade.getMethod());
successMessage.setOrderNo(trade.getOrderNo());
successMessage.setTradeNo(trade.getId());
amqpTemplate.convertAndSend(trade.getQueue(), trade.getRoutingKey(), JSON.toJSONString(successMessage));
log.info("向内部系统广播成功状态");
break;
}
// 交易记录被其他线程更新成功了
} else if (lockTrade.getStatus().equals(2)) {
log.info("交易记录已被其他线程更新为支付成功,跳过处理");
break;
// 订单被关闭
} else if (lockTrade.getStatus().equals(3)) {
log.info("订单关闭,走退款");
wxRefundService.create(trade, "交易已关闭,资金原路退回");
break;
} else {
log.info("数据已被其他线程更新,执行跳过");
break;
}
}
}
} catch (AmqpException e) {
/* 如果处理失败,将消息保存到队列 */
amqpTemplate.convertAndSend(PayQueueEnum.QUEUE_PAY_FAILURE, PayQueueEnum.KEY_PAY_FAILURE, JSON.toJSONString(transaction));
throw new RuntimeException(e);
}
} else {
// 微信支付失败
// 不用在意微信通道的状态,只要没有成功,就跳过,系统定时器会关闭超时交易订单
log.info("微信支付失败,description:{}", JSON.toJSONString(transaction));
}
// 释放资源
redisLocker.unlock(lockKey);
}
退款结果通知
/**
* 退款结果-微信通知
* 一共有两层保险,第一个是分布式锁,第二个是数据库乐观锁
* @param id 交易编号
* @param request http 请求对象
*/
@Inner(value = false)
@ApiOperation("退款结果-微信通知")
@PostMapping("/transaction/{id}/refund/result/wx") @Transactional
public void WxRefundResult(@PathVariable("id") long id, HttpServletRequest request) {
Trade trade = tradeService.getById(id);
// 加入分布式锁
String lockKey = "pay_".concat(trade.getOrderNo());
redisLocker.lockUp(lockKey, 60);
String body = getRequestBody(request);
TenantPaySetup paySetting = tenantPaySetupCall.getByTenantId(trade.getTenantId());
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(body)
.build();
NotificationConfig config = wxConfigManager.getMerchantConfig(trade.getTenantId());
NotificationParser parser = new NotificationParser(config);
RefundNotification refundNotification = null;
try {
refundNotification = parser.parse(requestParam, RefundNotification.class);
} catch (Exception e) {
log.error("支付结果验证失败,拒绝处理。");
throw new RuntimeException(e);
}
// 微信支付成功
if (refundNotification.getRefundStatus().equals(Status.SUCCESS)) {
try {
if (trade.getStatus().equals(4)) {
log.info("数据已被更新,执行跳过");
}
// 支付成功,商品订单和交易记录更新为支付成功
String tradeNo = refundNotification.getOutTradeNo();
int version = trade.getVersion();
trade.setStatus(4);
trade.setVersion(version + 1);
boolean updateResult = tradeService.update(trade,
Wrappers.<Trade>lambdaUpdate()
.eq(Trade::getId, trade.getId())
.eq(Trade::getVersion, version)
);
if (updateResult) {
log.info("退款成功,交易记录已更新");
// 被其他线程更新了
} else {
boolean retry = true;
while (retry) {
log.info("交易记录已被更新,继续重试");
Trade lockTrade = tradeService.getById(tradeNo);
// 继续更新
if (!lockTrade.getStatus().equals(4)) {
version = lockTrade.getVersion();
lockTrade.setStatus(2);
lockTrade.setVersion(version + 1);
updateResult = tradeService.update(lockTrade,
Wrappers.<Trade>lambdaUpdate()
.eq(Trade::getId, trade.getId())
.eq(Trade::getVersion, version)
);
if (updateResult) {
log.info("退款成功,交易记录已更新");
break;
}
// 交易记录被其他线程更新成功了
} else {
log.info("数据已被更新,执行跳过");
break;
}
}
}
} catch (AmqpException e) {
/* 如果处理失败,将消息保存到队列 */
amqpTemplate.convertAndSend(PayQueueEnum.QUEUE_PAY_FAILURE, PayQueueEnum.KEY_PAY_FAILURE, JSON.toJSONString(refundNotification));
throw new RuntimeException(e);
}
} else {
// 微信支付失败
// 不用在意微信通道的状态,只要没有成功,就跳过,系统定时器会关闭超时交易订单
log.info("微信退款失败,description:{}", JSON.toJSONString(refundNotification));
}
// 释放资源
redisLocker.unlock(lockKey);
}
JS 发起交易
/**
* 下单-js
* @param form 支付表单
* @return 预支付id
*/
@Inner
@ApiOperation("下单-jsapi")
@PostMapping("/transaction/wx/js") @Transactional
public String createTradeByJs(@RequestBody PayForm form) {
Trade trade = new Trade();
trade.setAmount(new BigDecimal(form.getAmount()));
trade.setTenantId(form.getTenantId());
trade.setChannel(form.getChannel());
trade.setMethod(form.getMethod());
trade.setOrderNo(form.getOrderNo());
trade.setVersion(1);
trade.setStatus(1);
trade.setUserId(form.getUserId());
trade.setQueue(form.getQueue());
trade.setRoutingKey(form.getRoutingKey());
tradeService.save(trade);
PrepayResponse response = wxJsPayService.prepay(trade);
return response.getPrepayId();
}
PC 二维码支付
/**
* 下单-native
* @param form 支付表单
* @return 二维码内容 url
*/
@Inner
@ApiOperation("下单-native")
@PostMapping("/transaction/wx/native") @Transactional
public String createTradeByNative(@RequestBody PayForm form) {
Trade trade = new Trade();
trade.setAmount(new BigDecimal(form.getAmount()));
trade.setTenantId(form.getTenantId());
trade.setChannel(form.getChannel());
trade.setMethod(form.getMethod());
trade.setOrderNo(form.getOrderNo());
trade.setVersion(1);
trade.setStatus(1);
trade.setUserId(form.getUserId());
trade.setQueue(form.getQueue());
trade.setRoutingKey(form.getRoutingKey());
tradeService.save(trade);
String codeUrl = wxNativePayService.prepay(trade).getCodeUrl();
return codeUrl;
}
微信接入文档
签名验证 pay.weixin.qq.com/wiki/doc/ap…
sdk github github.com/wechatpay-a…
下单接口 pay.weixin.qq.com/wiki/doc/ap…
开发注意的问题
商户证书配置只能存在一个,因为创建 RSAAutoCertificateConfig 会开启一个定时器,多个config会冲突。 github.com/wechatpay-a…