1.4 内网穿透与通知、查询用户订单

news/2025/2/12 16:52:34/

内网穿透与通知

文章目录

  • 内网穿透与通知
  • 一、内网穿透
    • 1.1 工具下载
  • 二、异步通知接收与应答
    • 2.1 支付通知
    • 2.2 签名的验证
    • 2.3 报文解密
    • 2.4 更新订单状态
    • 2.5 处理重复通知
    • 2.6 数据锁
  • 三、处理通知完整代码
    • 3.1 接收通知Controller
    • 3.2 验签工具类
    • 3.3 处理订单Service
    • 3.4 更新订单状态
    • 3.5 记录支付日志
  • 四、查询用户订单
    • 4.1 创建查单业务
    • 4.2 实现定时任务
      • 4.2.1 定时任务介绍
      • 4.2.2 实现定时查单核实订单状态
    • 4.3 效果图

一、内网穿透

支付完成之后,微信的客户端会给微信用户客户端发一个支付成功或失败的结果,也会给商户后台系统发送一个支付成功或失败的结果

流程图

所以微信支付系统怎么向我们的商户系统平台发送一个请求

绝大部分的开发机器都是基于局域网的内网地址的,所以微信的服务器没有办法直接通过我们的内网地址找到我们的开发机器,那我们必须要做一个内网穿透,这样的话我们就能获取到一个固定的外网地址,微信支付平台根据这个固定的外网地址就能访问到我们的请求了

1.1 工具下载

官方地址:Unified Application Delivery Platform for Developers (ngrok.com)下载即可

在官网下载后解压

image-20230917180955086

命令行输入

ngrok authtoken 你自己的token

token在这个地方复制即可

image-20230917181644044

启动ngrok,我们希望内网穿透到我们服务的8090端口

ngrok http 8090

“connecting”表示正在连接的状态

image-20230917182056400

“online”表示在线,已经连接成功

image-20230917182147015

我们可以看到内网穿透的地址有两个,一个是http,另一个是https

为了保证支付的安全,我们建议使用https的方式

# 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://a90b-101-27-21-172.ngrok-free.app/api/ali-pay/trade/notify

ngrok帮我们开通了一个专门的隧道,帮助我们和外网建立一个访问的通道

二、异步通知接收与应答

2.1 支付通知

微信支付通过支付通知接口将用户支付成功消息通知给商户:支付通知

支付通知注意事项支付通知注意事项

应答不规范和应答超时都会导致后续会发送重复通知

image-20231105175856964

设计Controller

我们之前在调用Native统一下单API的时候告诉过微信支付平台访问商户支付平台的哪个url,所以我们编写controller的时候,路径一定要对应上

image-20231105143613986

下面是成功的情况,最终会总结完整版代码

    @PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {//TODO 处理通知参数//自己封装的工具类处理通知参数,最后会得到一个字符串形式的通知参数String body = HttpUtils.readData(request);//转换成JSON类型JSONObject bodyJson = JSONObject.parseObject(body);log.info("通知唯一id - "+ bodyJson.getString("id"));//通知唯一id - cb0f0048-ddc2-5f1d-a516-ec307a16c40c
//      ciphertext里面是加密的数据,我们验签之后再进行解密log.info("通知的完整数据 - "+body);//通知的完整数据 - {"id":"cb0f0048-ddc2-5f1d-a516-ec307a16c40c","create_time":"2023-11-05T17:35:22+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"/VgIvB6fU1CJK8GLRCv/hVfaV2T3/nTTTdApaNu4HAidhB+KG9z0Zb9l1utkAJK6GAXfiXTvvcSvTX6/4vyyt8ob4PtArElMN5wHbgPJvZecvA8UFqvLdK84sCaCrPdZognImMEu3pnIBA4tQZWNUQOoFVAGWJ6vetrK6+KT1L5lqgr4Pvpgwa6GSsmS44Fxw1L31Sj2EQYmRWWf4FZSCmo1t+mmWYluB/Gk0CFXLyk1orAMSEat7+TXxg/3AQGIEco4nASl8Ox0xK8LV9x6lEbd90XsdMpGPS4TFqwxLHwip/aLsteYN0BuMsbmFOoU9zPTAa4sRa5/+CMPQEQiD57YCeeWiJagQPASOjYmrFCQy5j8IL8WAP+NVDGDphvtu8S3ZrRRMNQNju6vBAnGzlr46P6meLBqlDg/zJJixwKrmli1ZZdJmJJlcn0U85GiXqjFa98Y1NY+9uqs3CqLkct8O+ilrsF8JmHkKvqr/UG04XtyX6RmsK/MRR5ksYOw4lC53roXUK29gPOLsFMBkdcNNPMaqzqF3REbHwbrA1MVQLFADx3r+jmjciWub9oMl+CnRqhvSQ==","associated_data":"transaction","nonce":"kO0hZuBY9B1H"}}//TODO 验签//TODO 处理订单//TODO 向微信支付平台应答//接收成功: HTTP应答状态码需返回200或204,无需返回应答报文response.setStatus(200);//接收失败: HTTP应答状态码需返回5XX或4XX,同时需返回应答报文
//        Map<String,String> map  = new HashMap<>();
//        map.put("code","FAIL");
//        map.put("message","失败");return null;}

2.2 签名的验证

签名再微信支付中有两种场景

  1. 商户向微信平台发送请求然后微信平台给我们一个响应,我们商户平台要对这个响应进行签名验证

  2. 微信服务器端向商户平台发送一个通知,我们要对这个通知进行签名验证

这两次的认证的不同是,第一种是针对response签名认证,第二种是针对request进行签名认证

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

对于微信支付平台请求的处理我们首先应该拿到请求的数据,获取数据中的 ciphertext字段然后进行解密,就能找到我们想要的数据了

微信支付端并没有给我们提供默认的集成在SDK内部的不用我们手动编写的签名认证,所以这个地方我们要自己写

WxPayController

@Autowired
private ScheduledUpdateCertificatesVerifier verifier;
 //TODO 验签WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, bodyJson.getString("id"), body);if (!wechatPay2ValidatorForRequest.validate(request)) {
//          验签不通过,返回一个失败的应答log.info("通知验签失败");Map<String, String> map = new HashMap<>();map.put("code", "FAIL");map.put("message", "失败");return JSONObject.toJSONString(map);}log.info("通知验签成功");

工具类

/*** @author xy-peng*/
public class WechatPay2ValidatorForRequest {protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);/*** 应答超时时间,单位为分钟*/protected static final long RESPONSE_EXPIRED_MINUTES = 5;protected final Verifier verifier;protected final String requestId;protected final String body;public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {this.verifier = verifier;this.requestId = requestId;this.body = body;}protected static IllegalArgumentException parameterError(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("parameter error: " + message);}protected static IllegalArgumentException verifyFail(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("signature verify fail: " + message);}public final boolean validate(HttpServletRequest request) throws IOException {try {//处理请求参数validateParameters(request);//构造验签名串String message = buildMessage(request);String serial = request.getHeader(WECHAT_PAY_SERIAL); //WECHAT_PAY_SERIAL:Wechatpay-SerialString signature = request.getHeader(WECHAT_PAY_SIGNATURE);//WECHAT_PAY_SIGNATURE:Wechatpay-Signature//验签if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",serial, message, signature, requestId);}} catch (IllegalArgumentException e) {log.warn(e.getMessage());return false;}return true;}protected final void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};String header = null;for (String headerName : headers) {header = request.getHeader(headerName);if (header == null) {throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);}}//判断请求是否过期String timestampStr = header;try {Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));// 拒绝过期请求if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);}} catch (DateTimeException | NumberFormatException e) {throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);}}protected final String buildMessage(HttpServletRequest request) throws IOException {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);//WECHAT_PAY_TIMESTAMP:Wechatpay-TimestampString nonce = request.getHeader(WECHAT_PAY_NONCE);//WECHAT_PAY_NONCE:Wechatpay-Noncereturn timestamp + "\n"+ nonce + "\n"+ body + "\n";}protected final String getResponseBody(CloseableHttpResponse response) throws IOException {HttpEntity entity = response.getEntity();return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";}}

2.3 报文解密

微信支付平台给我们发送消息之前,会用APIv3密钥进行参数加密,加密之后又对请求进行了签名,签名之后给商户平台发送了通知,下面我们要做的就是要用密钥对参数进行解析,从通知参数当中拿到支付结果详情

APIv3密钥就是我们在商户后台申请的对称加密的密钥

image-20231105225101061

解释一下associateted_data附加数据,在Native下单时我们可以传一个attach字段,当支付完成后,微信平台向商户平台发送通知的时候还会携带这些附加数据

image-20231105225656730

只不过在支付通知接口变成了associated_data字段而已

将resource.ciphertext进行解密,得到JSON形式的资源对象

在SDK给我们提供了一个AwsUtil.java中提供了需要的方法进行解密

WxPayServiceImpl类

 @Overridepublic void processOrder(JSONObject bodyJson) throws GeneralSecurityException {log.info("处理订单");//解密数据,得到明文String  plainText = decryptFromResource(bodyJson);}/*** 对称解密*/private String decryptFromResource(JSONObject bodyJson) throws GeneralSecurityException {log.info("密文解密");JSONObject resource = bodyJson.getJSONObject("resource");
//      额外数据String associatedData = resource.getString("associated_data");
//      密文String ciphertext = resource.getString("ciphertext");log.info("密文 - "+ciphertext);
//      随机串String nonce = resource.getString("nonce");//      参数需要一个byte形式的对称加密的密钥
//      wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8BAesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes());
//      第一个参数:associated_data附加数据,第二个参数:随机串,第三个参数:密文
//      得到明文String plainText = aesUtil.decryptToString(associatedData.getBytes(), nonce.getBytes(), ciphertext);log.info("明文:plainText - " + plainText);return plainText;}

2.4 更新订单状态

@Override
public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {log.info("处理订单");//解密数据,得到明文String plainText = decryptFromResource(bodyJson);//将明文转换成map,下面这个JSON数据其实就是bodyJson中的resourceJSONObject plainTextJSON = JSONObject.parseObject(plainText);//更新订单状态String outTradeNo = plainTextJSON.getString("out_trade_no");orderInfoService.updateStatusByOrderNo(outTradeNo,OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);}

OrderInfoService

/*** 根据订单号更新订单状态*/
@Override
public void updateStatusByOrderNo(String outTradeNo, OrderStatus orderStatus) {log.info("更新订单状态 ===> {}", orderStatus.getType());QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", outTradeNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());baseMapper.update(orderInfo, queryWrapper);
}

PaymentInfoService

@Slf4j
@Service
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {@Overridepublic void createPaymentInfo(String plainText) {log.info("记录支付日志");Map plainTextMap = JSONObject.parseObject(plainText, Map.class);//订单号String orderNo = (String)plainTextMap.get("out_trade_no");//业务编号String transactionId = (String)plainTextMap.get("transaction_id");//支付类型String tradeType = (String)plainTextMap.get("trade_type");//交易状态String tradeState = (String)plainTextMap.get("trade_state");//用户实际支付金额Map<String, Object> amount = (Map)plainTextMap.get("amount");int payerTotal = (int) amount.get("payer_total");PaymentInfo paymentInfo = new PaymentInfo();paymentInfo.setOrderNo(orderNo);paymentInfo.setPaymentType(PayType.WXPAY.getType());//微信paymentInfo.setTransactionId(transactionId);paymentInfo.setTradeType(tradeType);paymentInfo.setTradeState(tradeState);paymentInfo.setPayerTotal(payerTotal);paymentInfo.setContent(plainText);baseMapper.insert(paymentInfo);}
}

2.5 处理重复通知

如果我们的应答在网络传输中处理问题,导致在规定时间内没有响应回微信平台,那此时微信平台就会认为通知失败,过段时间会再次发起相同的通知。但是其实我们已经收到了微信支付平台的通知并且根据通知的参数对订单进行了正确的处理。

image-20231105235618554

所以我们要处理一下重复的通知

那我们记录支付日志的时候,只记录一笔就好了,千万别因为重复的通知记录了多笔支付日志

@Override
public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {log.info("处理订单");//解密数据,得到明文String plainText = decryptFromResource(bodyJson);//将明文转换成map,下面这个JSON数据其实就是bodyJson中的resourceJSONObject plainTextJSON = JSONObject.parseObject(plainText);String outTradeNo = plainTextJSON.getString("out_trade_no");//处理重复通知String orderStatus = orderInfoService.getOrderStatus(outTradeNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//说明是已支付,我们直接返回订单状态即可return;}//更新订单状态orderInfoService.updateStatusByOrderNo(outTradeNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);}

OrderInfoService

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

2.6 数据锁

image-20231106000317523

我们虽然采用了方法对通知进行了一个重复的处理,假如有两个通知(一个订单的两个相同的通知)同时进入了processOrder方法来调用rderInfoService.getOrderStatus(outTradeNo)进行重复处理通知(也就是说出现了多线程并发安全问题),一笔订单依然会出现两笔相同的订单处理日志

//  可重入锁
private final ReentrantLock lock = new ReentrantLock();@Override
public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {log.info("处理订单");//解密数据,得到明文String plainText = decryptFromResource(bodyJson);//将明文转换成map,下面这个JSON数据其实就是bodyJson中的resourceJSONObject plainTextJSON = JSONObject.parseObject(plainText);String outTradeNo = plainTextJSON.getString("out_trade_no");//在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱//尝试获取锁,立即获取到锁就是true,获取失败则立即返回false,不会一直等待锁的释放//这个锁与synchronized的区别就是synchronized获取不到锁会一直等待,ReentrantLock获取不到锁就返回falseif (lock.tryLock()) {try {//处理重复通知String orderStatus = orderInfoService.getOrderStatus(outTradeNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//说明是已支付,我们直接返回订单状态即可return;}//更新订单状态orderInfoService.updateStatusByOrderNo(outTradeNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);} finally {//主动释放锁lock.unlock();}}}

三、处理通知完整代码

3.1 接收通知Controller

访问这个路径是在我们在Native统一下单的时候告诉微信支付平台的,后面微信平台就会向这个路径发起通知

image-20231106150435772

    /*** @param request  微信中的请求是在HttpServletRequest里面* @param response 要给微信的服务器返回响应* @return 因为通知的接口要求响应的是JSON字符串的格式*/@PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {//TODO 处理通知参数//自己封装的工具类处理通知参数,最后会得到一个字符串形式的通知参数String body = HttpUtils.readData(request);//转换成JSON类型JSONObject bodyJson = JSONObject.parseObject(body);log.info("通知唯一id - " + bodyJson.getString("id"));//通知唯一id - cb0f0048-ddc2-5f1d-a516-ec307a16c40c
//      ciphertext里面是加密的数据,我们验签之后再进行解密log.info("通知的完整数据 - " + body);//通知的完整数据 - {"id":"cb0f0048-ddc2-5f1d-a516-ec307a16c40c","create_time":"2023-11-05T17:35:22+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"/VgIvB6fU1CJK8GLRCv/hVfaV2T3/nTTTdApaNu4HAidhB+KG9z0Zb9l1utkAJK6GAXfiXTvvcSvTX6/4vyyt8ob4PtArElMN5wHbgPJvZecvA8UFqvLdK84sCaCrPdZognImMEu3pnIBA4tQZWNUQOoFVAGWJ6vetrK6+KT1L5lqgr4Pvpgwa6GSsmS44Fxw1L31Sj2EQYmRWWf4FZSCmo1t+mmWYluB/Gk0CFXLyk1orAMSEat7+TXxg/3AQGIEco4nASl8Ox0xK8LV9x6lEbd90XsdMpGPS4TFqwxLHwip/aLsteYN0BuMsbmFOoU9zPTAa4sRa5/+CMPQEQiD57YCeeWiJagQPASOjYmrFCQy5j8IL8WAP+NVDGDphvtu8S3ZrRRMNQNju6vBAnGzlr46P6meLBqlDg/zJJixwKrmli1ZZdJmJJlcn0U85GiXqjFa98Y1NY+9uqs3CqLkct8O+ilrsF8JmHkKvqr/UG04XtyX6RmsK/MRR5ksYOw4lC53roXUK29gPOLsFMBkdcNNPMaqzqF3REbHwbrA1MVQLFADx3r+jmjciWub9oMl+CnRqhvSQ==","associated_data":"transaction","nonce":"kO0hZuBY9B1H"}}//TODO 验签WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, bodyJson.getString("id"), body);if (!wechatPay2ValidatorForRequest.validate(request)) {
//          验签不通过,返回一个失败的应答log.info("通知验签失败");Map<String, String> map = new HashMap<>();map.put("code", "FAIL");map.put("message", "失败");return JSONObject.toJSONString(map);}log.info("通知验签成功");//TODO 处理订单// bodyJson是微信平台给我们的参数wxPayService.processOrder(bodyJson);//TODO 向微信支付平台应答//接收成功: HTTP应答状态码需返回200或204,无需返回应答报文response.setStatus(200);//接收失败: HTTP应答状态码需返回5XX或4XX,同时需返回应答报文
//        Map<String,String> map  = new HashMap<>();
//        map.put("code","FAIL");
//        map.put("message","失败");//      成功的时候什么也不返回,只要保证状态码是200或204return null;}

3.2 验签工具类

public class WechatPay2ValidatorForRequest {protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);/*** 应答超时时间,单位为分钟*/protected static final long RESPONSE_EXPIRED_MINUTES = 5;protected final Verifier verifier;protected final String requestId;protected final String body;public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {this.verifier = verifier;this.requestId = requestId;this.body = body;}protected static IllegalArgumentException parameterError(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("parameter error: " + message);}protected static IllegalArgumentException verifyFail(String message, Object... args) {message = String.format(message, args);return new IllegalArgumentException("signature verify fail: " + message);}public final boolean validate(HttpServletRequest request) throws IOException {try {//处理请求参数validateParameters(request);//构造验签名串String message = buildMessage(request);String serial = request.getHeader(WECHAT_PAY_SERIAL); //WECHAT_PAY_SERIAL:Wechatpay-SerialString signature = request.getHeader(WECHAT_PAY_SIGNATURE);//WECHAT_PAY_SIGNATURE:Wechatpay-Signature//验签if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",serial, message, signature, requestId);}} catch (IllegalArgumentException e) {log.warn(e.getMessage());return false;}return true;}protected final void validateParameters(HttpServletRequest request) {// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at lastString[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};String header = null;for (String headerName : headers) {header = request.getHeader(headerName);if (header == null) {throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);}}//判断请求是否过期String timestampStr = header;try {Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));// 拒绝过期请求if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);}} catch (DateTimeException | NumberFormatException e) {throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);}}protected final String buildMessage(HttpServletRequest request) throws IOException {String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);//WECHAT_PAY_TIMESTAMP:Wechatpay-TimestampString nonce = request.getHeader(WECHAT_PAY_NONCE);//WECHAT_PAY_NONCE:Wechatpay-Noncereturn timestamp + "\n"+ nonce + "\n"+ body + "\n";}protected final String getResponseBody(CloseableHttpResponse response) throws IOException {HttpEntity entity = response.getEntity();return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";}}

3.3 处理订单Service

WxPayServiceImpl类

 public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {log.info("处理订单");//解密数据,得到明文String plainText = decryptFromResource(bodyJson);//将明文转换成map,下面这个JSON数据其实就是bodyJson中的resourceJSONObject plainTextJSON = JSONObject.parseObject(plainText);String outTradeNo = plainTextJSON.getString("out_trade_no");//在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱//尝试获取锁,立即获取到锁就是true,获取失败则立即返回false,不会一直等待锁的释放//这个锁与synchronized的区别就是synchronized获取不到锁会一直等待,ReentrantLock获取不到锁就返回falseif (lock.tryLock()) {try {//处理重复通知String orderStatus = orderInfoService.getOrderStatus(outTradeNo);if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//说明是已支付,我们直接返回订单状态即可return;}//更新订单状态orderInfoService.updateStatusByOrderNo(outTradeNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(plainText);} finally {//主动释放锁lock.unlock();}}}/*** 对称解密*/private String decryptFromResource(JSONObject bodyJson) throws GeneralSecurityException {log.info("密文解密");JSONObject resource = bodyJson.getJSONObject("resource");
//      额外数据String associatedData = resource.getString("associated_data");
//      密文String ciphertext = resource.getString("ciphertext");log.info("密文 - " + ciphertext);
//      随机串String nonce = resource.getString("nonce");//      参数需要一个byte形式的对称加密的密钥
//      wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8BAesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes());
//      第一个参数:associated_data附加数据,第二个参数:随机串,第三个参数:密文
//      得到明文String plainText = aesUtil.decryptToString(associatedData.getBytes(), nonce.getBytes(), ciphertext);log.info("明文:plainText - " + plainText);return plainText;}

3.4 更新订单状态

//更新订单状态
orderInfoService.updateStatusByOrderNo(outTradeNo, OrderStatus.SUCCESS);
@Override
public String getOrderStatus(String orderNo) {QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", orderNo);OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);if (orderInfo == null) {return null;}return orderInfo.getOrderStatus();
}/*** 根据订单号更新订单状态*/
@Override
public void updateStatusByOrderNo(String outTradeNo, OrderStatus orderStatus) {log.info("更新订单状态 ===> {}", orderStatus.getType());QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("order_no", outTradeNo);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());baseMapper.update(orderInfo, queryWrapper);
}

3.5 记录支付日志

//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
@Override
public void createPaymentInfo(String plainText) {log.info("记录支付日志");Map plainTextMap = JSONObject.parseObject(plainText, Map.class);//订单号String orderNo = (String)plainTextMap.get("out_trade_no");//业务编号String transactionId = (String)plainTextMap.get("transaction_id");//支付类型String tradeType = (String)plainTextMap.get("trade_type");//交易状态String tradeState = (String)plainTextMap.get("trade_state");//用户实际支付金额Map amount = (Map)plainTextMap.get("amount");int payerTotal = (int) amount.get("payer_total");PaymentInfo paymentInfo = new PaymentInfo();paymentInfo.setOrderNo(orderNo);paymentInfo.setPaymentType(PayType.WXPAY.getType());//微信paymentInfo.setTransactionId(transactionId);paymentInfo.setTradeType(tradeType);paymentInfo.setTradeState(tradeState);paymentInfo.setPayerTotal(payerTotal);paymentInfo.setContent(plainText);baseMapper.insert(paymentInfo);
}

四、查询用户订单

如果商户后台迟迟没有收到异步通知结果的时候,商户应该主动去调用微信支付的查单接口,查询是否支付成功,我们根据支付成功还是不成功,就可以修改我们订单的状态了

image-20231107085036502

我们需要在我们商户端的后台设置一个定时任务,例如5分钟后没有收到异步通知的结果,那我们商户平台就需要主动去查询用户订单

查询支付订单的方式有两种:根据微信支付订单号查询和根据商户订单号查询

image-20231107091816612

其中微信支付订单号是在我们订单支付成功之后,微信平台向商户平台发送的通知中的transaction_id字段

image-20231107092001453

4.1 创建查单业务

查单的业务是定时任务调用的,不是通过接口调用的

我们选择根据商户订单号查询订单

image-20231107093224422

image-20231107093307080

请求参数实例

curl -X GET \https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/1217752501201407033233368018?mchid=1230000109 \-H "Authorization: WECHATPAY2-SHA256-RSA2048 mchid=\"1900000001\",..." \-H "Accept: application/json" 

image-20231107094436848

响应数据模板

{"amount":{"currency":"CNY","payer_currency":"CNY","payer_total":1,"total":1},"appid":"wx74862e0dfcf69954","attach":"","bank_type":"OTHERS","mchid":"1558950191","out_trade_no":"ORDER_20231106165639372","payer":{"openid":"oHwsHuOAS1JWiRAnLKwq5qTg4UkQ"},"promotion_detail":[],"success_time":"2023-11-06T16:56:51+08:00","trade_state":"SUCCESS","trade_state_desc":"支付成功","trade_type":"NATIVE","transaction_id":"4200002050202311069075905809"}
   /*** 根据订单号查询微信支付查单接口,核实订单状态* 如果订单已支付,则更新商户端订单状态为已支付* 如果订单未支付,则调用关单接口关闭订单,并封信商户端订单状态** @param orderNo 商户订单号*/@Overridepublic void checkOrderStatus(String orderNo) throws IOException {log.info("根据订单号查询微信支付查单接口,核实订单状态 - " + orderNo);//向微信平台发起查单接口String result = this.queryOrder(orderNo);JSONObject resultJSON = JSONObject.parseObject(result);//获取微信支付端的订单状态String tradeState = resultJSON.getString("trade_state");log.info("订单状态 - " + tradeState);//判断订单状态if (WxTradeState.SUCCESS.getType().equals(tradeState)) {log.info("核实订单已支付 - " + orderNo);//更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(result);} else if (WxTradeState.NOTPAY.getType().equals(tradeState)) {log.info("核实订单未支付 - " + orderNo);//调用微信关单接口this.closeOrder(orderNo);//更新本地订单状态,超时已关闭orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);}}

4.2 实现定时任务

4.2.1 定时任务介绍

springboot—任务—整合quartz与task----定时任务(详细)_quartz和task-CSDN博客

开启定时任务注解

//引入SpringTask
@EnableScheduling
@SpringBootApplication
public class PaymentApplication {public static void main(String[] args) {SpringApplication.run(PaymentApplication.class, args);}}

定时任务事例

@Component
@Slf4j
public class WxPayTask {/*** cron表达式由6部分组成,分别是秒、分、时、日、月、周。* 其中日和周是互斥的,不能同时指定,指定其中一个则另一个设置为“?”即可。* 假如“1-3”在秒位置,则表示从第1秒开始执行,到第3秒结束执行* 假如“0/3”在秒位置,则表示第0秒开始,每隔3秒执行一次* 假如“1,2,3”在秒位置,则表示在第1秒、第2秒,第3秒开始执行* “?”表示不指定参数* “*”表示每秒/分/时....,如果在秒位置就是每秒都执行,如果是在分位置就是每分钟都执行* 比如 cron = "* * * * * ?" 表示每个月每日没时每分没秒都要执行这个定时任务*/@Scheduled(cron = "* * * * * ?")public void task1(){log.info("task1 被执行");}}

4.2.2 实现定时查单核实订单状态

//希望程序启动的时候能自动初始化出来
@Component
@Slf4j
public class WxPayTask {@Autowiredprivate OrderInfoService orderInfoService;@Autowiredprivate WxPayService wxPayService;/*** 从第0秒开始,每隔30秒执行查单一次,查询创建超过5分钟并且未支付的订单*/@Scheduled(cron = "0/30 * * * * ?")public void orderConfirm() throws IOException {log.info("微信支付定时查单");//查找超过五分钟未支付的订单List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);//获取商户系统数据库中未支付的超时订单,向微信支付平台发送请求确认是否是未支付for (OrderInfo orderInfo : orderInfoList) {String orderNo = orderInfo.getOrderNo();log.warn("超时订单 - {}",orderNo);//核实订单状态,调用微信支付查单接口wxPayService.checkOrderStatus(orderNo);}}
}

获取超过五分钟未支付的订单

@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes) {//五分钟之前Instant instant = Instant.now().minus(Duration.ofMinutes(5));QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();//订单未支付queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());//创建的订单要早于5分钟之前queryWrapper.le("create_time",instant);return baseMapper.selectList(queryWrapper);
}

4.3 效果图

image-20231107104907449

image-20231107104849446

image-20231107105332263


http://www.ppmy.cn/news/1202030.html

相关文章

Linux 实现原理 — NUMA 多核架构中的多线程调度开销与性能优化

前言 NOTE&#xff1a;本文中所指 “线程” 均为可执行调度单元 Kernel Thread。 NUMA 体系结构 NUMA&#xff08;Non-Uniform Memory Access&#xff0c;非一致性存储器访问&#xff09;的设计理念是将 CPU 和 Main Memory 进行分区自治&#xff08;Local NUMA node&#x…

AVL树节点插入方式解析(单旋转和双旋转)

AVL树的规则 在学习AVL树插入节点的方式之前&#xff0c;我们首先要理解为什么要出现AVL树&#xff0c;首先我们要知道的是AVL树是在二叉搜索树的基础上增加一些限制条件才完成的。那么AVL树就是为了处理二叉搜索树的缺点而出现的一棵树&#xff0c;那么普通的二叉搜索树的缺点…

Vue使用depcheck进行依赖检查

全局安装 npm i -g depcheck 输入命令 depcheck 结果中出现Missing dependencies(缺失依赖)

如何使用java实现第三方支付

下面是一个简单的Java代码实现沙箱第三方支付的示例&#xff1a; import java.math.BigDecimal;public class SandboxPayment {public static void main(String[] args) {try {// 模拟接收客户端请求BigDecimal amount new BigDecimal(1000);String accountNo "622202*…

EPLAN-P8软件技术分享文章

EPLAN公司成立于1984年德国。EPLAN最初的产品是基于DOS平台&#xff0c;然后经历了Windows3.1、Windows95、Windows98、Windows2000、Windows Vista等、Windows7、Windows8等平台发展历史。EPLAN是以电气设计为基础的跨专业的设计平台&#xff0c;包括电气设计、流体设计、仪表…

C语言之认识柔性数组(flexible array)

在学习之前&#xff0c;我们首先要了解柔性数组是放在结构体当中的&#xff0c;知道这一点&#xff0c;我们就开始今天的学习吧&#xff01; 1.柔性数组的声明 在C99中&#xff0c;结构中的最后一个元素允许是未知大小的数组&#xff0c;这就叫做柔性数组成员 这里的结构是结构…

快速了解什么是跳跃表(skip list)

什么是跳跃表&#xff08;skip list&#xff09; 跳跃表&#xff08;Skip List&#xff09;是一种概率性的数据结构&#xff0c;它通过在多层链表的基础上添加“快速通道”来提高搜索效率。跳跃表的效率可以与平衡树相媲美&#xff0c;即在平均和最坏的情况下&#xff0c;查找…

html将复选框变为圆形样例

html将复选框变为圆形样例 说明目录使用对勾图标实现圆形复选框原复选框html代码及默认样式取消复选框未勾选前的样式新增复选框未勾选前的样式新增复选框勾选后的样式获取复选框选中后的value值 使用CSS样式写对勾图标实现圆形复选框 说明 这里记录下用原生html实现将原复选框…