V3版 jsapi微信支付流程(Springboot)


一、开发准备

在这里插入图片描述

jsapi微信支付需要信息如下:

1.微信公众平台申请:AppId

2.微信支付平台申请:
(1)商户ID(mcdid)绑定APPID及mchid;

(2)商户API秘钥(apiV3Key):API v3密钥主要用于平台证书解密、回调信息解密;
登录微信商户平台,进入【账户中心 > API安全 > API安全】目录,点击【设置密钥】。
在这里插入图片描述

(3)商户Api证书、证书秘钥。商户API证书具体使用说明可参见接口规则文档中私钥和证书章节

商户可登录微信商户平台,在【账户中心】->【API安全】->【API证书】目录下载证书

详情请阅读微信支付光放文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml

二、开发步骤

1.引入库

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.2.2</version>
</dependency>

源码地址:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient

2.初始化httpClient

配置httpClient之后再调用之后的下单接口、查询接口后不需要配置签名可直接调用。

/**
     * 初始化微信支付httpClient,此方法通过自动更新微信支付平台证书实现(自动更新证书功能),无需下载微信支付平台证书
     * privateKey  商户API私钥,加载证书获取
     * apiv3  API v3密钥
     * mchid  商户id
     * serialNumber 商户API证书的证书序列号,加载证书获取
     *使用完成之后记得调用httpClient.close()方法。
     * @return
     */
    public CloseableHttpClient initWChatClient() {
        String apiv3 = ConstantWxPayPropertiesUtils.KEY;
        String mchid = ConstantWxPayPropertiesUtils.MCH_ID;
        //api证书序列号
        String serialNumber = ConstantWxPayPropertiesUtils.SERIAL_NO;
        //获取证书信息
        KeyPair keyPair = createPKCS12(ConstantWxPayPropertiesUtils.CERT_PATH, mchid);
        PrivateKey merchantPrivateKey = keyPair.getPrivate();
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(mchid, new PrivateKeySigner(serialNumber, merchantPrivateKey)),
                apiv3.getBytes(StandardCharsets.UTF_8));
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchid, serialNumber, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier));
        return builder.build();
    }
/**
     * //获取KeyPair
     *
     * @param keyPath 微信支付Api证书路径
     * @param keyPass 初始密码为商户号mchid
     * @return
     */
    public KeyPair createPKCS12(String keyPath, String keyPass) {
        String keyAlias = "Tenpay Certificate";// 证书的别名,固定为Tenpay Certificate 
        ClassPathResource resource = new ClassPathResource(keyPath);
        char[] pem = keyPass.toCharArray();
        try {
            synchronized (lock) {
                if (store == null) {
                    synchronized (lock) {
                        store = KeyStore.getInstance("PKCS12");
                        store.load(resource.getInputStream(), pem);
                    }
                }
            }
            X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
            certificate.checkValidity();
            //证书序列号,需要的可以自己写方法获取
            String serialNo = certificate.getSerialNumber().toString(16).toLowerCase();
            // 证书的 公钥
            PublicKey publicKey = certificate.getPublicKey();
            PrivateKey privateKey = (PrivateKey) store.getKey(keyAlias, pem);
            return new KeyPair(publicKey, privateKey);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot load keys from store: " + resource, e);
        }
    }

证书存放路径,可通过配置文件加载

在这里插入图片描述

3.jsapi调用下单请求

请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

请求方式:POST

/**
     * 微信支付,请求下单
     */
    @Override
    public CloseableHttpResponse createWxOrder(BigDecimal price, String desc, String openId,String orderNo) {
    	//初始化httpClient
        CloseableHttpClient httpClient =  rsaUtil.initWChatClient();
        try {
            //请求微信服务器下单接口,生成预付单
            //存入订单到数据库中
            //微信下单请求URL
            HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
            MapUtils requestBody = new MapUtils()
                    .put("appid",ConstantWxPropertiesUtils.WX_OPEN_APP_ID)
                    .put("mchid", ConstantWxPayPropertiesUtils.MCH_ID)
                    .put("description", desc)
                    .put("out_trade_no", orderNo)
                    .put("notify_url", ConstantWxPayPropertiesUtils.PAY_NOTIFY_URL);
            MapUtils amountMap = new MapUtils()
                    .put("total", 1)
                    .put("currency", "CNY");
            requestBody.put("amount", amountMap);
            MapUtils payerMap = new MapUtils().put("openid", openId);
            requestBody.put("payer", payerMap);
            String jsonString = JSONObject.toJSONString(requestBody);
            // 请求body参数
            StringEntity entity = new StringEntity(jsonString);
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");
            //完成签名并执行请求
            CloseableHttpResponse response = httpClient.execute(httpPost);
            rsaUtil.closeHttp(httpClient);
            return response;
        } catch (IOException e) {
            rsaUtil.closeHttp(httpClient);
            throw new ApiException("请求微信下单失败",500);
        }
    }

请求下单接口完成之后需返回如下信息:appId,timeStamp,nonceStr,package,signType,paySign供前端调起微信支付。注意:以下字段名字母大小写敏感
在这里插入图片描述


 /**
     * 生成签名
     * @param signStr 需要签名的字符串
     * @return
     */
    private String generateSign(String signStr) {
        try {
            String mchid = ConstantWxPayPropertiesUtils.MCH_ID;
            KeyPair keyPair = createPKCS12(ConstantWxPayPropertiesUtils.CERT_PATH, mchid);
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(keyPair.getPrivate());
            sign.update(signStr.getBytes(StandardCharsets.UTF_8));
            return Base64Utils.encodeToString(sign.sign());
        } catch (Exception e) {
            throw new ApiException("签名异常错误", 500);
        }
    }
     /**
     * 下单签名
     * @param packageStr
     * @return
     */
    public String paySign(String packageStr,long timestamp,String nonceStr) {
        String appId = ConstantWxPropertiesUtils.WX_OPEN_APP_ID;
        String signatureStr =
                Stream.of(appId, String.valueOf(timestamp), nonceStr, packageStr).collect(Collectors.joining("\n", ""
                        , "\n"));
        return generateSign(signatureStr);
    }

下单请求参与签名的字段如下:
在这里插入图片描述

4.jsapi调起支付(此过程无需后台参与)

请求参数就是第三部中下单操作返回的结果。
在这里插入图片描述

5.支付结果通知

@PostMapping("callback")
    public Map<String, String> callback(HttpServletRequest request) {
        log.debug("========================支付回调开始===============================");
        try {
            String requestBody = HttpRequestHelper.getRequestBody(request);
            if (verifiedSign(request, requestBody)) {
                WxPayCallBackDataVo wxPayCallBackDataVo = JSONObject.parseObject(requestBody,
                        WxPayCallBackDataVo.class);
                //如果支付成功
                if ("TRANSACTION.SUCCESS".equals(wxPayCallBackDataVo.getEvent_type())) {
                    //通知资源数据
                    WxPayCallBackDataVo.WxPayResourceDataVo resource = wxPayCallBackDataVo.getResource();
                    //解密后资源数据
                    String notifyResponseBody = rsaUtil.decryptResponseBody(ConstantWxPayPropertiesUtils.KEY,
                            resource.getAssociated_data(), resource.getNonce(), resource.getCiphertext());
                    //微信回调通知数据
                    NotifyResultVo resultVo = JSONObject.parseObject(notifyResponseBody, NotifyResultVo.class);
                    String outTradeNo = resultVo.getOut_trade_no();
                    OrderEntity orderEntity = orderService.getOrderInfoByOutTradeNo(outTradeNo);
                    if (orderEntity != null) {
                        if (orderEntity.getOrderStatus().equals(OrderStatusEnum.NOT_PAID.getStatus())) {
                        	//根据微信之后返回的结果对订单进行不同的处理
                            if ("SUCCESS".equals(resultVo.getTrade_state())) {
                                orderService.updateOrderPaySuccess(orderEntity, resultVo);
                            } else {
                                orderService.updateOrderPayFail(orderEntity, resultVo);
                            }
                        }
                    }
                } else {
                    log.error("微信返回支付错误摘要:" + wxPayCallBackDataVo.getSummary());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("callback...");
        Map<String, String> mapUtils = new HashMap<>();
        mapUtils.put("code", "SUCCESS");
        mapUtils.put("message", "");
        log.debug("========================支付回调结束===============================");
        return mapUtils;
    }
/**
     * 获取请求文体
     * @param request
     * @return
     * @throws IOException
     */
    public static String getRequestBody(HttpServletRequest request) throws IOException {
        BufferedReader reader = null;
        StringBuffer sb = new StringBuffer();
        try {
            ServletInputStream stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new ApiException("读取返回支付接口数据流出现异常!");
        } finally {
            reader.close();
        }
        return sb.toString();
    }
 /**
     * 解密响应体.
     *
     * @param apiV3Key       API V3 KEY  API v3密钥 商户平台设置的32位字符串
     * @param associatedData  response.body.data[i].encrypt_certificate.associated_data
     * @param nonce          response.body.data[i].encrypt_certificate.nonce
     * @param ciphertext     response.body.data[i].encrypt_certificate.ciphertext
     * @return the string
     * @throws GeneralSecurityException the general security exception
     */
    public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));

            byte[] bytes;
            try {
                bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
            } catch (GeneralSecurityException e) {
                throw new IllegalArgumentException(e);
            }
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

验证签名:

 // 定义全局容器 保存微信平台证书公钥  注意线程安全
    public static Map<String, X509Certificate> certificateMap = new ConcurrentHashMap<>();
    /**
     * 验证微信签名,必须要验证签名哦
     *
     * @param request
     * @param body
     * @return
     * @throws GeneralSecurityException
     * @throws IOException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws ParseException
     */
    private boolean verifiedSign(HttpServletRequest request, String body) throws GeneralSecurityException,
            ParseException {
        //微信返回的证书序列号
        String serialNo = request.getHeader("Wechatpay-Serial");
        //微信返回的随机字符串
        String nonceStr = request.getHeader("Wechatpay-Nonce");
        //微信返回的时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        //微信返回的签名
        String wechatSign = request.getHeader("Wechatpay-Signature");
        //组装签名字符串
        String signStr = Stream.of(timestamp, nonceStr, body)
                .collect(Collectors.joining("\n", "", "\n"));
        //当证书容器为空 或者 响应提供的证书序列号不在容器中时  就应该刷新了
        if (ConstantWxPayPropertiesUtils.certificateMap.isEmpty() || !ConstantWxPayPropertiesUtils.certificateMap.containsKey(serialNo)) {
            rsaUtil.refreshCertificate();
        }
        //根据序列号获取平台证书
        X509Certificate certificate = ConstantWxPayPropertiesUtils.certificateMap.get(serialNo);
        //获取失败 验证失败
        if (certificate == null) {
            return false;
        }
        //SHA256withRSA签名
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(certificate);
        signature.update(signStr.getBytes());
        //返回验签结果
        return signature.verify(Base64Utils.decodeFromString(wechatSign));
    }

6.订单状态查询

由于网络异常或者系统的波动,可能会导致用户支付成功,但是商户侧未能成功接收到支付结果通知,进而显示订单未支付的情况。商户侧的订单状态更新不及时,容易造成用户投诉,甚至是重复支付的情况发生。

在这里插入图片描述

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

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

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

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

后端服务处理:

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

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

此处采用第二种方式完成订单轮训查询方式:

 /**
     * 微信订单状态定时查询
     */
    @Scheduled(cron = "0/30 * * * * ?")//定时查询时间可根据自身业务进行修改
    public void queryPayStatus(){
        log.debug("===============订单状态查询定时任务已启动======================");
        int count = 5;查询次数,在订单表中新增字段用于记录查询次数
        int timeMin = 10; //查询10分钟之内的订单。
        //查询创建十分钟之内并且未未支付的订单,并且查单次数不超过5的数据
        List<OrderEntity> list = orderService.list(new QueryWrapper<OrderEntity>().eq("order_status",
                OrderStatusEnum.NOT_PAID.getStatus()).gt("create_dt", DateUtils.addMinute(new Date(), -timeMin)).lt("check_status_count",count));
        log.debug("未支付订单数据量"+list.size());
        list.stream().forEach(orderInfo->{
            log.debug("订单号:=================="+orderInfo.getOrderNo());
            NotifyResultVo notifyResultVo = orderService.queryOrderStatus(orderInfo);
            //如果付款成功
            if ("SUCCESS".equals(notifyResultVo.getTrade_state())) {
                //修改成功结果参数
                orderService.updateOrderPaySuccess(orderInfo, notifyResultVo);
            } else{
                //支付不成功则修改订单查询次数
                Integer checkStatusCount = orderInfo.getCheckStatusCount();
                checkStatusCount++;
                orderInfo.setCheckStatusCount(checkStatusCount);
                orderService.updateById(orderInfo);
                if(checkStatusCount == count){
                	//最后一次查询扔未成功,需调用关闭订单接口
                    orderService.updateOrderPayFail(orderInfo, notifyResultVo);
                }
            }
        });
        log.debug("=================订单状态查询定时任务已结束===================");
    }

7.关闭订单

订单超时未支付取消功能可通过redis中的key失效监听功能实现(此处采用),定时任务查询订单或者利用rabbitMq实现延迟消费
public void cancelOrder(Long id) throws IOException {
        log.debug("==========================订单取消log开始=========================");
        OrderEntity orderEntity = this.baseMapper.selectById(id);
        if(orderEntity.getOrderStatus()==OrderStatusEnum.NOT_PAID.getStatus()){
            orderEntity.setOrderStatus(OrderStatusEnum.CANCEL.getStatus());
            String orderDetailJson = orderEntity.getOrderDetailJson();
            JSONObject jsonObject = JSONObject.parseObject(orderDetailJson);
            long venueBookId = jsonObject.getLongValue("venueBookId");
            venueBookService.removeById(venueBookId);
            venueBookDetailService.remove(new QueryWrapper<VenueBookDetailEntity>().eq("venue_book_id", venueBookId));
            redisUtils.delete(Constant.ORDER_HEAD + id);
            this.baseMapper.updateById(orderEntity);
            PaymentInfoEntity paymentInfoEntity = paymentInfoDao.selectOne(new QueryWrapper<PaymentInfoEntity>().eq(
                    "order_id", orderEntity.getId()));
            String outTradeNo = paymentInfoEntity.getOutTradeNo();
            //发送微信请求取消订单
            CloseableHttpClient httpClient = rsaUtil.initWChatClient();
            //微信下单请求URL
            HttpPost httpPost =
                    new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + outTradeNo + "/close");
            MapUtils requestBody = new MapUtils()
                    .put("mchid", ConstantWxPayPropertiesUtils.MCH_ID);
            String jsonString = JSONObject.toJSONString(requestBody);
            // 请求body参数
            StringEntity entity = new StringEntity(jsonString);
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");
            //完成签名并执行请求
            CloseableHttpResponse response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            log.debug("订单取消响应码:" + statusCode);
        }

        log.debug("==========================订单取消log结束=========================");
    }

总结

微信支付主要是签名校验之类的比较繁琐,本身实现还是比较简单,初始化好httpClient,后面只要凭借参数直接调用微信接口就好了。
注意一个大坑:

如果签名中出现:
InvalidKeyException: Illegal key size错误,
解决方式是替换jdk里面的local_policy.jar,US_export_policy.jar,不过我发现只要jdk版本高于java8-251以上的基本上不会有这个问题,低于这个版本就老老实实修改local_policy.jar,US_export_policy.jar。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值