Java接入微信native、jsapi支付

Java接入微信native、jsapi支付

一、说明

本文示例使用的微信支付版本为V2版本,是旧版本,旧版本与新版本的接口不一,并不通用。

微信官方接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/index.html

二、微信支付所需要的参数

appid、mchid、apikey

这些参数,需要公司去微信官方平台进行申请才行,通常来说,作为程序员不用操心。我没有申请过,具体的流程我就不做说明了。

申请地址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal#none

以上参数我放到了nacos中,放入配置文件中也行,使用时直接注入就可以了。

三、引入微信依赖

<dependency>
    <groupId>com.github.liyiorg</groupId>
    <artifactId>weixin-popular</artifactId>
    <version>2.8.16</version>
</dependency>

四、开发注意事项

  1. 微信旧版本的请求格式为xml格式,请求前需要进行转换。
  2. 回调接口的的数据在请求流中,需要将流中的数据读取出转换成map。
  3. 旧版本的小程序的支付与网页端的支付,几乎一样,只有两个参数不同:
    1. openid:网页支付(native)时不需要这个请求参数,小程序支付(JSAPI)时,需要将微信用户的openid加入到请求参数中去。
    2. appid:网页支付时就是所申请的appid,小程序支付时,为申请的小程序appid
  4. 做小程序支付时,需要给前端返回签名信息,生成签名时的map中的"appId"的"I"为大写,请求微信接口的时候为小写,此处一定要注意,否则,在微信小程序支付时会验签失败。
  5. 微信支付的回调地址必须是可以通过外网访问的地址,需要在线下测试时,可以用内网穿透做到自己本机的代码可以被外网访问,具体怎么操作后续会写一篇博客来说明。

五、微信支付流程说明

我只做过native支付和小程序支付(JSAPI),所以此处就只写这两种支付流程。

微信官方说明的比较详细,我写的流程说明我感觉比较通俗易懂一些。大家可以结合着理解。

1.native支付
  1. 前提,已经生成订单。
  2. 前端根据订单号、金额等参数(其他参数看具体的业务需要)请求后端的支付接口。
  3. 后端支付接口封装数据进行请求微信官方的接口。
  4. 微信接口验证成功,返回相关数据。
  5. 把微信接口返回数据中的“code_url”返回给前端。
  6. 前端拿着二维码链接进行展示。
  7. 用户扫码付款。
  8. 付款成功后,微信会根据回调地址通知后端系统。
  9. 后端系统判断付款是否成功,然后做相应的逻辑处理就行。

附一张微信官方流程图:

5_0.png (799×881) (qq.com)

2.小程序支付(JSAPI)
  1. 前提:订单生成。
  2. 前端根据订单号、金额、openid等参数(其他参数看具体的业务需要)请求后端的支付接口。
  3. 后端封装请求需要的参数,请求微信的接口。
  4. 根据微信接口返回数据中的prepay_id进行生成签名,并将前端需要的参数进行封装返回。
  5. 前端拿着后端返回的数据,请求微信的接口,调起微信支付。
  6. 用户支付。
  7. 支付成功,微信根据回调地址通知后端系统。
  8. 判断是否支付成功,并做出相应的逻辑处理。

附一张微信官方流程图:

https://pay.weixin.qq.com/wiki/doc/apiv3/assets/img/pay/wechatpay/6_2.png

六、代码:

1.Service层代码

以下代码为业务层实现类代码,直接在控制层调用,然后返回就行,我在控制层没有做逻辑处理。

    /**
     * 公众账号id
     */
    @Value("${wechat.cert.appid}")
    private String appid;
    /**
     * 小程序id
     */
    @Value("${wechat.cert.appletid}")
    private String appletid;
    /**
     * 小程序秘钥
     */
    @Value("${wechat.cert.appletSecret}")
    private String appletSecret;
    /**
     * 商户号
     */
    @Value("${wechat.cert.mchid}")
    private String mchid;
    /**
     * 商户key
     */
    @Value("${wechat.cert.apikey}")
    private String apikey;
    /**
     * 回调地址
     */
    @Value("${wechat.pay.notifyUrl}")
    private String notifyUrl;
    /**
     * 微信支付的请求地址
     */
    private static final String unifiedOrderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    /**
     * 微信支付订单的查询接口地址
     */
    private static final String orderQueryUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
    /**
     * 关闭微信支付订单的接口地址
     */
    private static final String closeOrder = "https://api.mch.weixin.qq.com/pay/closeorder";
    /**
     * 成功标识
     */
    private static final String SUCCESS = "SUCCESS";
    /**
     * 远程调用shop模块接口
     */
    @Resource
    private RemoteShopService remoteShopService;
    /**
     * 支付共用业务
     */
    @Resource
    private PaymentCommon paymentCommon;
    /**
     * redis封装工具类
     */
    @Resource
    private RedisService redisService;


    /**
     * 微信(native、jsapi)付款,这里加入分布式锁来进行幂等性校验,防止用户多次点击支付按钮
     * @param payInfo 支付信息
     * @return String 支付二维码url,小程序支付时是prepay_id,有效期两个小时。
     */
    @Override
    public Object payment(PayInfo payInfo) {
        // 参数校验,校验当前系统支付时所需要的参数信息
        paymentCommon.checkParam(payInfo);
        // 获取订单号
        String outTradeNo = payInfo.getOutTradeNo();
        log.info("用户:" + "请求微信支付!" + "订单号:" + outTradeNo);
        try {
			// 获取锁
            if (redisService.getCacheObject(CacheConstants.PAYMENT_LOCK + outTradeNo) == null) {
                // 加锁
                redisService.setCacheObject(CacheConstants.PAYMENT_LOCK + outTradeNo, 1, 5L, TimeUnit.SECONDS);
                // 校验订单状态
                paymentCommon.chechStatus(payInfo);
                // 封装请求微信接口的公共参数
                Map<String, String> map = this.packPublicParam(outTradeNo);
                
                /*
					封装付款接口独有参数
                 */
                map.put("body", subject);
                // 微信支付的金额单位为分
                double totalFee = Double.parseDouble(payInfo.getTotalAmount()) * 100;
                map.put("total_fee", (int)totalFee + "");
                map.put("spbill_create_ip", IpUtils.getHostIp());
                map.put("notify_url", notifyUrl);
                map.put("trade_type", paymentCommon.getTradeType(payInfo.getMethod()));
                // 设置起止时间
                Date timeStart = new Date();
                Date timeExpire = org.apache.commons.lang3.time.DateUtils.addMinutes(timeStart, 5);
                map.put("time_start", DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, timeStart));
                map.put("time_expire", DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, timeExpire));
                
                // 初始化返回值,微信小程序支付与网页支付前端需要的数据不同,所以定义一个Object类型。
                Object result;
                if (!StringUtils.isEmpty(payInfo.getUserid())) {
                    // 用户id不为空,为jsapi(小程序支付),返回前端调用微信接口时的参数信息
                    map.put("openid", payInfo.getUserid());
                    map.put("appid", appletid);
                    // 请求微信接口,获取返回值
                    Map<String, String> wxAddress = this.getWxAddress(unifiedOrderUrl, map);
                    result = this.sign(wxAddress.get("prepay_id"));
                } else {
					// 用户id为空,则为native支付,返回codeUrl,
                    result = this.getWxAddress(unifiedOrderUrl, map).get("code_url");
                }
                // 添加支付流水信息
                paymentCommon.packPayInfo(payInfo);
                log.info("用户:" + payInfo.getUserid() + "请求微信支付成功!");
                return result;
            }
        } finally {
            // 释放锁
            redisService.deleteObject(CacheConstants.PAYMENT_LOCK + outTradeNo);
        }
        return null;
    }


    /**
     * 查询微信订单
     * @param ordersn 订单编号
     * @return Map<String, String>
     */
    @Override
    public Map<String, String> orderQuery(String ordersn) {
        //封装参数发起请求
        return this.getWxAddress(orderQueryUrl, this.packPublicParam(ordersn));
    }

    /**
     * 关闭微信支付订单
     * 
     * @param ordersn 订单编号
     * @return Map<String, String>
     */
    @Override
    public Map<String, String> closeOrder(String ordersn) {
        return this.getWxAddress(closeOrder, this.packPublicParam(ordersn));
    }

    /**
     * 微信支付后的回调方法
     *
     * @param request 请求流
     * @return String 回调后的信息,返回给微信官方
     */
    @Override
    public String callback(HttpServletRequest request) throws Exception {
        // 将微信请求的流信息转换成map集合
        Map<String, String> map = this.getMap(request);
        // 初始化返回码与返回信息
        String code = "SUCCESS", msg = "OK";
        String orderId = map.get("out_trade_no");
        log.info("用户" + map.get("openid") + "微信支付后开始修改订单状态,订单编号是:" + orderId);

        try {
            if (redisService.getCacheObject(CacheConstants.PAY_CALLBACK_LOCK + orderId) == null) {
                // 异步加锁,5秒过期
                redisService.setCacheObject(CacheConstants.PAY_CALLBACK_LOCK + orderId, 1, 5L, TimeUnit.SECONDS);
                // 做请求幂等性处理,校验订单的状态
                String status = remoteShopService.getOrderStatusById(orderId);
                if (!"待发货".equals(status)) {
                    // 判断用户是否支付成功
                    if (map.get("return_code").equals(SUCCESS) && map.get("result_code").equals(SUCCESS)) {
                        // 用户支付成功,修改订单状态s
                        if (remoteShopService.updateOrderStatus(orderId, 2) != 1) {
                            if (remoteShopService.updateOrderStatus(orderId, 2) != 1) {
                                // 两次调用失败,记录日志,抛出异常
                                log.error("用户" + map.get("openid") + "微信支付后修改订单状态失败,订单编号是:" + orderId);
                            }
                        }
                        // 查询支付流水信息,进行信息匹配
                        PayInfo payInfo = remoteShopService.getPayInfoByOutTradeNo(orderId);
                        assert payInfo != null;
                        if (!payInfo.getTotalAmount().equals(map.get("total_fee"))) {
                            // 支付信息不匹配
                            code = "FAIL";
                            msg = "支付信息不匹配";
                        } else {
                            // 支付信息匹配,修改支付流水信息
                            payInfo.setTradeNo(map.get("transaction_id"));
                            if (remoteShopService.updatePayInfo(JSONObject.toJSONString(payInfo))!=1) {
                                if (remoteShopService.updatePayInfo(JSONObject.toJSONString(payInfo))!=1) {
                                    code = "FAIL";
                                    msg = "修改用户支付信息失败";
                                    log.error("用户" + map.get("openid") + "微信支付后修改支付信息失败,订单编号是:" + orderId);
                                }
                            }
                        }
                    } else {
                        // 用户付款失败,将失败信息记入日志
                        log.error("用户" + map.get("openid") + "微信支付失败----" +
                                map.get("err_code") + ":" + map.get("err_code_des"));
                        code = "FAIL";
                        msg = "用户付款失败";
                    }
                }
            }
        } finally {
            // 释放锁
            redisService.deleteObject(CacheConstants.PAY_CALLBACK_LOCK + orderId);
        }

        // 封装返回参数,并返回给微信官方
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("return_code", code);
        hashMap.put("return_msg", msg);
        return WXPayUtil.mapToXml(hashMap);
    }


    /*===================================================私有方法===================================================*/

    /**
     * 封装公共的请求微信接口参数
     * @param ordersn 订单编号
     * @return Map<String, String> 封装请求微信接口的公共参数后的map集合
     */
    private Map<String, String> packPublicParam(String ordersn){
        // 初始化封装请求参数
        HashMap<String, String> map = new HashMap<>();
        // 封装请求微信接口的参数
        map.put("appid", appid);
        map.put("mch_id", mchid);
        map.put("nonce_str", WXPayUtil.generateNonceStr());
        map.put("out_trade_no", ordersn);
        return map;
    }

    /**
     * 请求微信接口
     * @param url 请求的地址
     * @param param 请求参数
     * @return Map<String, String> 请求微信接口后,微信返回的数据
     */
    private Map<String, String> getWxAddress(String url, Map<String, String> param){
        try {
            // 将参数转换为xml
            String s = WXPayUtil.generateSignedXml(param, apikey);
            // 请求微信地址
            HttpClient httpClient = new HttpClient(url);
            httpClient.setXmlParam(s);
            httpClient.post();
            // 获取请求结果:结果为xml格式,需转换为map格式
            String content = httpClient.getContent();
            Map<String, String> map = WXPayUtil.xmlToMap(content);
            if (map.get("return_code").equals(SUCCESS) && map.get("result_code").equals(SUCCESS)) {
                // 请求成功,将请求结果返回
                return map;
            } else {
                String s1 = "";
                switch (url) {
                    case unifiedOrderUrl:
                        s1 = "微信支付";
                        break;
                    case orderQueryUrl:
                        s1 = "微信支付的订单查询";
                        break;
                    case closeOrder:
                        s1 = "关闭微信支付订单";
                        break;
                }
                //请求失败,将失败信息记入日志
                log.error("请求微信接口失败(" + s1 + "----)" +
                        map.get("err_code") +
                        ":" +
                        map.get("err_code_des"));
                throw new BaseException("请求微信支付失败,请刷新重试");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new BaseException("请求微信支付失败,请刷新重试");
        }
    }

    /**
     * 将微信请求的流信息转换成map集合
     * @param request 请求流
     * @return Map<String, String>
     */
    private Map<String, String> getMap(HttpServletRequest request) throws Exception {
        // 获取输入流,微信的回调信息是通过信息流的方式传输的
        ServletInputStream inputStream = request.getInputStream();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        // 定义缓冲区
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, len);
        }

        // 从输出流获取数据,转换为map
        String string = outputStream.toString();
        return WXPayUtil.xmlToMap(string);
    }

    /**
     * 计算签名值,在微信小程序支付时用到,计算后,返回前端
     * @param prepayId 预支付交易会话标识
     * @return 签名值
     */
    private Map<String, String> sign(String prepayId) {
        TreeMap<String, String> map = new TreeMap<>(String::compareTo);
        // 注意,此处的appId的“I”为大写,在请求微信接口的时候是小写,注意区分,否则会验签失败!
        map.put("appId", appletid);
        map.put("timeStamp", System.currentTimeMillis() / 1000 +"");
        map.put("nonceStr", WXPayUtil.generateNonceStr());
        map.put("package", "prepay_id=" + prepayId);
        map.put("signType", "MD5");
        try {
			// 调用微信的工具类方法,生成签名
            map.put("paySign", generateSignature(map, apikey));
            // 为了信息的安全,将appId删除后返回前端
            map.remove("appId");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

}

2.请求工具类代码

以下代码直接拿着用就行。

public class HttpClient {
    /**
     * 地址
     */
    private String url;
    /**
     * 参数
     */
    private Map<String, Object> param;
    /**
     * 状态码
     */
    private int statusCode;
    /**
     * 响应内容
     */
    private String content;
    /**
     * xml类型参数
     */
    private String xmlParam;
    /**
     * 是不是HTTPS请求(默认是true)
     */
    private boolean isHttps = true;

    public boolean isHttps() {
        return isHttps;
    }

    public void setHttps(boolean isHttps) {
        this.isHttps = isHttps;
    }

    public String getXmlParam() {
        return xmlParam;
    }

    public void setXmlParam(String xmlParam) {
        this.xmlParam = xmlParam;
    }

    public HttpClient(String url, Map<String, Object> param) {
        this.url = url;
        this.param = param;
    }

    public HttpClient(String url) {
        this.url = url;
    }

    public void setParameter(Map<String, Object> map) {
        param = map;
    }

    public void addParameter(String key, String value) {
        if (param == null)
            param = new HashMap<String, Object>();
        param.put(key, value);
    }

    public void post() throws ClientProtocolException, IOException {
        HttpPost http = new HttpPost(url);
        setEntity(http);
        execute(http);
    }

    public void put() throws ClientProtocolException, IOException {
        HttpPut http = new HttpPut(url);
        setEntity(http);
        execute(http);
    }

    public void get() throws ClientProtocolException, IOException {
        if (param != null) {
            StringBuilder url = new StringBuilder(this.url);
            boolean isFirst = true;
            for (String key : param.keySet()) {
                if (isFirst) {
                    url.append("?");
                }else {
                    url.append("&");
                }
                url.append(key).append("=").append(param.get(key));
            }
            this.url = url.toString();
        }
        HttpGet http = new HttpGet(url);
        execute(http);
    }

    /**
     * set http post,put param
     */
    private void setEntity(HttpEntityEnclosingRequestBase http) {
        if (param != null) {
            List<NameValuePair> nvps = new LinkedList<NameValuePair>();
            for (String key : param.keySet()) {
                nvps.add(new BasicNameValuePair(key, param.get(key).toString())); // 参数
            }
            http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
        }
        if (xmlParam != null) {
            http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
        }
    }

    private void execute(HttpUriRequest http) throws ClientProtocolException,
            IOException {
        CloseableHttpClient httpClient = null;
        try {
            if (isHttps) {
                SSLContext sslContext = new SSLContextBuilder()
                        .loadTrustMaterial(null, new TrustStrategy() {
                            // 信任所有
                            @Override
                            public boolean isTrusted(X509Certificate[] chain,
                                                     String authType)
                                    throws CertificateException {
                                return true;
                            }
                        }).build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslContext);
                httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
                        .build();
            } else {
                httpClient = HttpClients.createDefault();
            }
            CloseableHttpResponse response = httpClient.execute(http);
            try {
                if (response != null) {
                    if (response.getStatusLine() != null) {
                        statusCode = response.getStatusLine().getStatusCode();
                    }
                    HttpEntity entity = response.getEntity();
                    // 响应内容
                    content = EntityUtils.toString(entity, Consts.UTF_8);
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            httpClient.close();
        }
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getContent() throws ParseException, IOException {
        return content;
    }
}

七、Native测试说明

1.请求微信支付后返回数据说明

具体的参数说明,详见微信官方文档:

说明:在V2版本中,微信native支付与JSAPI支付接口说明,都是这篇文档,接口也是一致的。

URL:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

2.付款测试

新建“WxPay.html”文件,然后将"qrious.js"文件(根据链接生成二维码用的)放在同一目录下,这个js文件在网上下载就行。

2.1 WxPay.html代码:
<html>
    <head>
    	<title>二维码</title>
    </head>

    <body>
        <img id="abc">
        <script src="qrious.js"></script>
        <script>
         var qr = new QRious({
                element:document.getElementById('abc'),
                size:400, 	   
                level:'L',
                value:"weixin://wxpay/bizpayurl?pr=qxB37vLzz"
            });
        </script>
    </body>
</html>
2.2 说明

代码中的value值就是请求微信接口后数据中的“code_url”,将此值复制替换上边代码中的值就行。

替换后,保存修改,用浏览器打后开就会有一个二维码,用自己的微信扫描二维码会出现付款的界面,付款成功就是OK了。

3.回调测试

线下的回调测试需要用到内网穿透,具体的方法,后续会出一篇博客来说明。

以上是我个人做支付时的代码,可能还有一些不完善,不成熟的地方,欢迎各位讨论!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值