Java版微信公众号支付实现过程

首先看一下微信官方对此的说明文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_4
Java版微信公众号支付实现过程

简单的描述就是:
根据自己的商品订单数据4,拼接下单签名参数,然后使用统一下单API向微信请求生成微信看得懂的订单5,然后生成支付参数及签名6,在支付页面根据支付配置及微信统一订单的prepay_id,传给前端发起微信支付7。用户输入密码支付后,微信会通过异步的方式,通知我们的系统,告诉支付的结果10。我们在系统后台处理根据支付的结果,处理我们的业务后,返回信息告诉向往,此订单已确定付款成功,然后进行业务处理11。

大体的开发流程:
1)获取用户授权(拿到openId)
2)调用统一下单接口获取预支付id
3)H5调起微信支付的内置JS
4)支付完成后,微信回调URL的处理

前期准备工作:
公众号支付在请求支付的时候会校验请求来源是否有在公众平台做了配置,所以必须确保支付目录已经正确的被配置,否则将验证失败,请求支付不成功。支付授权目录就是指支付方法的请求全路径,配置路径如下图所示:
Java版微信公众号支付实现过程
开发公众号支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。具体界面如下图所示:

Java版微信公众号支付实现过程
具体授权内容将另起一篇文章予以说明。这里贴出小程序内部通过code来实现获取openId的代码

/**
     * 根据code 获取openid
     *
     * @param code
     * @return
     */
    public String getOpenId(String code) throws Exception {
        //code换openid 接口
        String WX_URL = "https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code";

        String requestUrl = WX_URL.replace("APPID", appid).
                replace("SECRET", appSecret).replace("JSCODE", code).
                replace("authorization_code", "authorization_code");

        logger.info("请求前拼接url:{}", requestUrl);

        // 发起GET请求获取凭证
        JSONObject jsonObject = doGetStr(requestUrl);

        logger.info("请求后获取的参数:{}", jsonObject);

        String openid = jsonObject.getString("openid");
        //jsonObject.getString("session_key");

        return openid;
    }

有了openId之后再加上AppId以及mch_id以及等参数拼出下单参数,调用统一下单接口:

 // 处理真正发起支付的参数
    public SortedMap<Object, Object> callPay(HttpServletRequest request, String out_trade_no, String total_fee,String openId,JsPayResult results) throws Exception {
        
        String spbill_create_ip = HttpReqUtil.getRemortIP(request);

        logger.info("客户端支付IP:{}", spbill_create_ip);

//        String nonce_str = UUID.randomUUID().toString().replaceAll("-", ""); // 随机数据
        // 随机数据
        String nonce_str = WechatPayService.generateNonceStr();

        //下单签名参数组装
        Map<String, String> parameters = new HashMap<>();
//        SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
        parameters.put("appid", appid);
        parameters.put("mch_id", mch_id);
        parameters.put("nonce_str", nonce_str);
        parameters.put("body", "test");
        parameters.put("out_trade_no", out_trade_no);           //商户订单号
        parameters.put("total_fee", total_fee);                       //标价金额
        parameters.put("spbill_create_ip", spbill_create_ip);     //发起调用的终端IP 例如:192.168.189.2 
        parameters.put("notify_url", notifyUrl);        //通知地址
        parameters.put("trade_type", "JSAPI");      //交易类型
        parameters.put("openid", openId);

        String sign = WechatPayService.createSign(parameters);   //签名
        parameters.put("sign", sign);          //将签名put到对象中

        String requestXML = WechatPayService.getRequestXml(parameters);     ////签名并入service
        logger.info("签名"+requestXML);
        String result = HttpReqUtil.httpsRequest(unifiedOrder, "POST", requestXML);
        logger.info("返回<![CDATA[SUCCESS]]>格式的XML: " + result.toString());
        Map<String, String> map = new HashMap<String, String>();
        map = XmlUtil.xmlToMap(result);     //将xml转为map

        //获取时间戳
        String timeStamp = PublicHandleUtils.createTimeStamp();
        nonce_str = UUID.randomUUID().toString().replaceAll("-", "");
        results = new JsPayResult();
        results.setAppId(appid);
        results.setTimeStamp(timeStamp);
        results.setNonceStr(nonce_str);//直接用返回的
        results.setSignType("MD5");

        /**** prepay_id 2小时内都有效,再次支付方法自己重写 ****/
        results.setPackageStr("prepay_id=" + map.get("prepay_id"));
        /**** 用对象进行签名 ****/
        results.setResultCode("SUCCESS");

        //拿到prepay_id再次签名将数据传给前端
        Map<String, String> payMap = new HashMap<>();
        payMap.put("appId", results.getAppId());
        payMap.put("nonceStr", results.getNonceStr());
        payMap.put("package", results.getPackageStr());
        payMap.put("signType", results.getSignType());
        payMap.put("timeStamp", results.getTimeStamp());
        String paySign = WechatPayService.createSign(payMap);
        logger.info("second sign = {}", paySign);
        results.setPaySign(paySign);

        //---------------将参数(封装)驼峰式命名传给客户端--------
        SortedMap<Object, Object> payMaps = new TreeMap<Object, Object>();
        payMaps.put("nonceStr", payMap.get("nonceStr"));
        payMaps.put("timeStamp", payMap.get("timeStamp"));
        payMaps.put("package", payMap.get("package"));
        payMaps.put("signType", "MD5");
        payMaps.put("paySign", paySign);
        return payMaps;
    }

客户端拿到相应的参数后调用微信的方法发起真正的支付,弹出输入密码界面,等用户输入正确的密码之后,微信将调用回调方法,具体写法如下:

    /**
     * 支付成功后的回调函数
     * @throws Exception
     */
    @ResponseBody
    @RequestMapping("notify")
    public ResultState notify(HttpServletRequest req, HttpServletResponse resp) throws Exception {

        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//设置日期格式
        ResultState resultState = new ResultState();
        String resXml = "";
        logger.info("开始处理支付返回的请求:\n");
        BufferedReader reader = req.getReader();
        String line = "";
        String xmlString = null;
        StringBuffer inputString = new StringBuffer();

        while ((line = reader.readLine()) != null) {
            inputString.append(line);
        }
        xmlString = inputString.toString();
        req.getReader().close();
        logger.debug("----接收到的数据如下:---" + xmlString);

        Map<String, String> map = new HashMap<String, String>();
        String result_code = "";
        String return_code = "";
        String out_trade_no = "";
        String transaction_id = "";
        
        //将接受到的xml数据转换为map形式
        map = XmlUtil.xmlToMap(xmlString);
        logger.debug("数据转为map形式:{}", map);

        result_code = map.get("result_code");
        out_trade_no = map.get("out_trade_no");     //获取微信支付返回的订单号
        return_code = map.get("return_code");
        transaction_id = map.get("transaction_id");
        
        if ("SUCCESS".equals(result_code)) {
			 //具体业务处理逻辑写在这里
            resultState.setErrcode(0);// 表示成功,可以不写,int默认是0
            resultState.setErrmsg("success");

		// 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了
            resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
        } else {
            resultState.setErrcode(-1);// 支付失败
            resultState.setErrmsg(map.get("err_code_des"));
            logger.info("支付失败,错误信息:" + map.get("err_code_des"));
            resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[" + map.get("err_code_des") + "]]></return_msg>" + "</xml> ";
        }
        BufferedOutputStream out = new BufferedOutputStream(resp.getOutputStream());
        out.write(resXml.getBytes());
        out.flush();
        out.close();

        return resultState;
    }

除了上述的主体方法外,贴出相关工具类的实现方法:
签名方法:

  public static String createSign(Map<String, String> data)throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if (k.equals("sign")) {
                continue;
            }
            if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
        }
        sb.append("key=").append("agoisbgiawbgoasvzjdnbeogaviZjsns");
        System.out.println("连接商户key:"+sb);
        return MD5(sb.toString()).toUpperCase();
    }

获取随机字符串

 /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

将Map转换为XML格式的字符串

 /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String getRequestXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key: data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        }
        catch (Exception ex) {
        }
        return output;
    }
    
    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        documentBuilderFactory.setXIncludeAware(false);
        documentBuilderFactory.setExpandEntityReferences(false);

        return documentBuilderFactory.newDocumentBuilder();
    }
    
    public static Document newDocument() throws ParserConfigurationException {
        return newDocumentBuilder().newDocument();
    }

将xml转为map

 /**
     * 将xml转为map
     * */
    public static Map<String,String> xmlToMap(String strxml)throws Exception{
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

        Map<String,String> map = new HashMap<>();
        SAXReader reader = new SAXReader();
        InputStream ins = new ByteArrayInputStream(strxml.getBytes("UTF-8"));

        if(null == strxml || "".equals(strxml)) {
            return null;
        }

        Map m = new HashMap();
        Document document = reader.read(ins);
        Element root = document.getRootElement();
        List<Element> list = root.elements();
        
        for (Element e: list){
            map.put(e.getName(),e.getText());
        }
        ins.close();

        return map;
    }

跟HTTP相关的方法:

 /**
     * IP地址
     * @param request
     * @return
     */
    public static String getRemortIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isEmpty(ip)) {
            ip = request.getRemoteAddr();
        } else {
            if (StringUtils.contains(ip, ",")) {
                String[] ips = StringUtils.split(ip, ",");
                if (ips.length > 1) {
                    ip = ips[0];
                }
            }
        }
        return ip;
    }
    
  /**
     * 发送https请求
     *
     * @param requestUrl    请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param outputStr     提交的数据
     * @return 返回微信服务器响应的信息
     */
    public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = {new MyX509TrustManager()};
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            //sslContext.init(null, tm, new Java.security.SecureRandom());
            sslContext.init(null, tm, new SecureRandom());

// 从上述SSLContext对象中得到SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();
            URL url = new URL(requestUrl);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setSSLSocketFactory(ssf);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);

// 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
// 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
// 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
            log.error("连接超时:{}", ce);
        } catch (Exception e) {
            log.error("https请求异常:{}", e);
        }
        return null;
    }

基本上所有的代码都已经贴出来了,代码来源于经过测试可以使用的线上项目代码,如有遗漏请对照官方文档加以补充。
未经本人允许不得转载,如有问题请联系[email protected]