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


    内网穿透与通知

    一、内网穿透

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

    流程图

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

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

    1.1 工具下载

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

    在官网下载后解压

    image-20230917180955086

    命令行输入

    ngrok authtoken 你自己的token
    
    • 1

    token在这个地方复制即可

    image-20230917181644044

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

    ngrok http 8090
    
    • 1

    “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
    
    • 1
    • 2
    • 3

    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 map  = new HashMap<>();
    //        map.put("code","FAIL");
    //        map.put("message","失败");
    
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.2 签名的验证

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

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

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

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

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

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

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

    WxPayController

    @Autowired
    private ScheduledUpdateCertificatesVerifier verifier;
    
    • 1
    • 2
     //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("通知验签成功");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    工具类

    /**
     * @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-Serial
                String 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 last
            String[] 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-Timestamp
            String nonce = request.getHeader(WECHAT_PAY_NONCE);//WECHAT_PAY_NONCE:Wechatpay-Nonce
            return 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) : "";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    2.3 报文解密

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

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

    image-20231105225101061

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

    image-20231105225656730

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

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

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

    WxPayServiceImpl类

     @Override
        public 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=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
            AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes());
    //      第一个参数:associated_data附加数据,第二个参数:随机串,第三个参数:密文
    //      得到明文
            String plainText = aesUtil.decryptToString(associatedData.getBytes(), nonce.getBytes(), ciphertext);
            log.info("明文:plainText - " + plainText);
            return plainText;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2.4 更新订单状态

    @Override
    public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {
        log.info("处理订单");
    
        //解密数据,得到明文
        String plainText = decryptFromResource(bodyJson);
    
        //将明文转换成map,下面这个JSON数据其实就是bodyJson中的resource
        JSONObject plainTextJSON = JSONObject.parseObject(plainText);
    
        //更新订单状态
        String outTradeNo = plainTextJSON.getString("out_trade_no");
        orderInfoService.updateStatusByOrderNo(outTradeNo,OrderStatus.SUCCESS);
        //记录支付日志
        paymentInfoService.createPaymentInfo(plainText);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    PaymentInfoService

    @Slf4j
    @Service
    public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
    
        @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<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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2.5 处理重复通知

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

    image-20231105235618554

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

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

    @Override
    public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {
        log.info("处理订单");
    
        //解密数据,得到明文
        String plainText = decryptFromResource(bodyJson);
    
        //将明文转换成map,下面这个JSON数据其实就是bodyJson中的resource
        JSONObject 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);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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中的resource
        JSONObject plainTextJSON = JSONObject.parseObject(plainText);
        String outTradeNo = plainTextJSON.getString("out_trade_no");
    
        //在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
        //尝试获取锁,立即获取到锁就是true,获取失败则立即返回false,不会一直等待锁的释放
        //这个锁与synchronized的区别就是synchronized获取不到锁会一直等待,ReentrantLock获取不到锁就返回false
        if (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();
            }
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    三、处理通知完整代码

    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 map  = new HashMap<>();
    //        map.put("code","FAIL");
    //        map.put("message","失败");
    
    //      成功的时候什么也不返回,只要保证状态码是200或204
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    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-Serial
                String 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 last
            String[] 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-Timestamp
            String nonce = request.getHeader(WECHAT_PAY_NONCE);//WECHAT_PAY_NONCE:Wechatpay-Nonce
            return 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) : "";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    3.3 处理订单Service

    WxPayServiceImpl类

     public void processOrder(JSONObject bodyJson) throws GeneralSecurityException {
            log.info("处理订单");
    
            //解密数据,得到明文
            String plainText = decryptFromResource(bodyJson);
    
            //将明文转换成map,下面这个JSON数据其实就是bodyJson中的resource
            JSONObject plainTextJSON = JSONObject.parseObject(plainText);
            String outTradeNo = plainTextJSON.getString("out_trade_no");
    
            //在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
            //尝试获取锁,立即获取到锁就是true,获取失败则立即返回false,不会一直等待锁的释放
            //这个锁与synchronized的区别就是synchronized获取不到锁会一直等待,ReentrantLock获取不到锁就返回false
            if (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=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
            AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes());
    //      第一个参数:associated_data附加数据,第二个参数:随机串,第三个参数:密文
    //      得到明文
            String plainText = aesUtil.decryptToString(associatedData.getBytes(), nonce.getBytes(), ciphertext);
            log.info("明文:plainText - " + plainText);
            return plainText;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    3.4 更新订单状态

    //更新订单状态
    orderInfoService.updateStatusByOrderNo(outTradeNo, OrderStatus.SUCCESS);
    
    • 1
    • 2
    @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3.5 记录支付日志

    //记录支付日志
    paymentInfoService.createPaymentInfo(plainText);
    
    • 1
    • 2
    @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    四、查询用户订单

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

    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" 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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"}
    
    
    • 1
    • 2
       /**
         * 根据订单号查询微信支付查单接口,核实订单状态
         * 如果订单已支付,则更新商户端订单状态为已支付
         * 如果订单未支付,则调用关单接口关闭订单,并封信商户端订单状态
         *
         * @param orderNo 商户订单号
         */
        @Override
        public 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);
            }
    
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    定时任务事例

    @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 被执行");
    
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    //希望程序启动的时候能自动初始化出来
    @Component
    @Slf4j
    public class WxPayTask {
    
        @Autowired
        private OrderInfoService orderInfoService;
    
        @Autowired
        private 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);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.3 效果图

    image-20231107104907449

    image-20231107104849446

    image-20231107105332263

  • 相关阅读:
    javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection
    Hudi数据湖技术引领大数据新风口(三)解决spark模块依赖冲突
    数字资产革命:Web3带来的新商业机会
    最长公共前缀-字符串-分治/二分/暴力解决
    Java I/O(二)BIO, NIO, AIO
    自从学会了 CSS resize 属性,我也可以对美女背景大展身手
    Bytebase x Hacktoberfest 2023 黑客啤酒节开源挑战邀请
    web课程设计 基于html+css+javascript+jquery女性化妆品商城
    《目标检测蓝皮书》附录 | 详解 【分类】【回归】【检测】【分割】 损失函数与性能指标
    Apache Doris物化视图使用详解
  • 原文地址:https://blog.csdn.net/weixin_51351637/article/details/134264991