1. 程式人生 > >Java後臺實現小程式微信支付

Java後臺實現小程式微信支付

本人第一篇部落格,之前筆記一直放在有道雲,想寫部落格很久了,一直沒下定決心,也沒靜下心好好寫,今天突然跟朋友談 到平時工作根本沒時間學習、整理、總結balabala,工作中遇到的問題很多會經常遇到,不整理總結第二次碰到又要半天,就這麼扯吧扯吧,扯完之後,不知道哪來的決心,就下手了,哈哈,廢話不多說,進入正題啦,有不對的或者更好的建議,希望各位看官指點指點小白~

1、 前述

首先還是要來看下微信官方的api文件,官網的開發步驟以及流程,稍後我會把自己程式碼的步驟和流程簡述,這裡就不一一貼圖出來,連結獻上:微信支付官方文件

2、步驟如下:

組裝統一下單引數
按照引數名ASCII碼從小到大排序(字典序)
第一次簽名->拼	接XML
發起統一下單請求
解析統一下單返回的結果(xml形式)
(支付成功)將解析得到的預付單資訊拼接,再次簽名
返回前端簽名字串

3、後臺程式碼:

public Map<String, Object> weixinPrepay(FirstSignDto data, HttpServletRequest request) {
     	//CommonSystemConfig為配置類
        CommonSystemConfig commonSystemConfig = SysConfigUtils.get(CommonSystemConfig.class);
        try {
            //準備下單所需要的引數
            String noStr = IdGenerator.getRandomString(32);//生成隨機的字串
            String notifyUrl = commonSystemConfig.getNotifyUrl(); //回撥地址,注意這個地址不要被攔截
            String appId = commonSystemConfig.getAppId();//小程式appid
            String tradeType = commonSystemConfig.getTradeType();//型別 JSAPI
            String merchantNo = commonSystemConfig.getMerchantNo();//商戶號
            String key = commonSystemConfig.getKey();//支付金鑰,在商戶那裡可以獲取

            String orderNo = data.getOrderNo();//業務訂單號
            Double tradeMoney = data.getTradeMoney();//交易金額
            String openid = data.getOpenid();//openid

            logger.info("---- 訂單 {} 預支付 ----", orderNo);
	        //驗證業務訂單號是否合法
            Order order =orderService.selectOne(new EntityWrapper<Order>().eq("order_no", orderNo));

            if (ParamUtil.isNullOrEmptyOrZero(order)) {
                LeaseException.throwSystemException(LeaseExceEnums.ORDER_EXCEPTION);
            }
            //校驗該訂單的支付金額跟前端傳過來的金額是否一致
            if (order.getTradeMoney() == tradeMoney) {
                LeaseException.throwSystemException(LeaseExceEnums.ORDER_EXCEPTION);
            }
            //獲取終端的ip
            String ipAddr = WxUtil.getIpAddr(request);
            //頁面傳來的交易金額單位是 元,轉換成 分
            String fee = String.valueOf((int) (tradeMoney * 100));

            //微信支付必須大於0.01,所以這裡個人做了對應業務處理,根據各自需求來處理
            if (tradeMoney == 0.00) {
         	    .....................
            }

            String bodyStr = new String("WashinFUN".getBytes("UTF-8"));

            //組裝引數,生成下單所需要的引數map進行第一次簽名
            Map<String, String> packageParams = new HashMap<>();
            packageParams.put("appid", appId);
            packageParams.put("mch_id", merchantNo);
            packageParams.put("nonce_str", noStr);
            packageParams.put("notify_url", notifyUrl);
            packageParams.put("out_trade_no", orderNo);
            packageParams.put("spbill_create_ip", ipAddr);
            packageParams.put("total_fee", fee);
            packageParams.put("trade_type", tradeType);
            packageParams.put("openid", openid);
            packageParams.put("body", bodyStr);
            //所有元素排序,並按照“引數=引數值”的模式用“&”字元拼接成字串,用來簽名
            String preStr = PayUtils.createLinkString(packageParams);
            
            logger.info("微信支付preStr:{}", preStr);

            //第一次簽名
            String sign = PayUtils.sign(preStr, key, "utf-8").toUpperCase();

            logger.info("微信支付簽名sign:{}", sign);
            //拼接xml
            String xml = "<xml>" + "<appid>" + appId + "</appid>"
                    + "<body>" + bodyStr + "</body>"
                    + "<mch_id>" + merchantNo + "</mch_id>"
                    + "<nonce_str>" + noStr + "</nonce_str>"
                    + "<notify_url>" + notifyUrl + "</notify_url>"
                    + "<openid>" + openid + "</openid>"
                    + "<out_trade_no>" + orderNo + "</out_trade_no>"
                    + "<spbill_create_ip>" + ipAddr + "</spbill_create_ip>"
                    + "<total_fee>" + fee + "</total_fee>"
                    + "<trade_type>" + tradeType + "</trade_type>"
                    + "<sign>" + sign + "</sign>"
                    + "</xml>";

            logger.info("統一下單介面 XML資料:{}", xml);

            String result = PayUtils.httpRequest(PAY_URL, "POST", xml);

            logger.info("統一下單結果:{}", result);
	        //解析xml
            Map map = PayUtils.doXMLParse(result);
            String return_code = (String) map.get("return_code");//返回狀態碼

            Map<String, Object> response = new HashMap<String, Object>();//返回給小程式端需要的引數map
            if (return_code.equals("SUCCESS")) {
                String prepay_id = (String) map.get("prepay_id");//返回的預付單資訊
                response.put("nonceStr", noStr);
                response.put("package", "prepay_id=" + prepay_id);
                
                Long timeStamp = System.currentTimeMillis() / 1000;
                response.put("timeStamp", timeStamp + "");//這邊要將返回的時間戳轉化成字串,不然小程式端呼叫wx.requestPayment方法會報簽名錯誤

                //拼接簽名需要的引數
                String stringSignTemp = "appId=" + appId + "&nonceStr=" + noStr + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;
                //第二次簽名,這個簽名的結果用於小程式呼叫介面(wx.requesetPayment)
                String paySign = PayUtils.sign(stringSignTemp, key, "utf-8").toUpperCase();

                Order order = orderService.selectOne(new EntityWrapper<Order>().eq("order_no", orderNo));
                if (!ParamUtil.isNullOrEmptyOrZero(order)) {
                    Date tradeTime = new Date();
                    order.setTradeTime(tradeTime);
                    order.setTradeScene(commonSystemConfig.getTradeType());
                    orderService.updateById(order);
                    logger.info("支付成功,設定訂單表 {} 的交易時間 {} 交易場景 {} :", orderNo, tradeTime, tradeType);
                }
                response.put("paySign", paySign);
            }
            response.put("appid", appId);
            response.put("bypassPayStatus", 0);//這是業務需要所返回的欄位
            return response;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

PayUtils

public class PayUtils {
    /**
     * 簽名字串
     *
     * @param
     * @param key 金鑰
     * @param
     * @return 簽名結果
     */
    public static String sign(String text, String key, String input_charset) {
        text = text + "&key=" + key;
        return DigestUtils.md5Hex(getContentBytes(text, input_charset));
    }

    public static String signature(Map<String, String> map, String key) {
        Set<String> keySet = map.keySet();
        String[] str = new String[map.size()];
        StringBuilder tmp = new StringBuilder();
        // 進行字典排序
        str = keySet.toArray(str);
        Arrays.sort(str);
        for (int i = 0; i < str.length; i++) {
            String t = str[i] + "=" + map.get(str[i]) + "&";
            tmp.append(t);
        }
        if (null != key) {
            tmp.append("key=" + key);
        }
        return DigestUtils.md5Hex(tmp.toString()).toUpperCase();
    }

    /**
     * 簽名字串
     *
     * @param
     * @param sign          簽名結果
     * @param
     * @param input_charset 編碼格式
     * @return 簽名結果
     */
    public static boolean verify(String text, String sign, String key, String input_charset) {
        text = text + key;
        String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
        if (mysign.equals(sign)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param content
     * @param charset
     * @return
     * @throws SignatureException
     * @throws UnsupportedEncodingException
     */
    public static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5簽名過程中出現錯誤,指定的編碼集不對,您目前指定的編碼集是:" + charset);
        }
    }

    private static boolean isValidChar(char ch) {
        if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
            return true;
        if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))
            return true;// 簡體中文漢字編碼
        return false;
    }

    /**
     * 除去陣列中的空值和簽名引數
     *
     * @param sArray 簽名引數組
     * @return 去掉空值與簽名引數後的新簽名引數組
     */
    public static Map<String, String> paraFilter(Map<String, String> sArray) {
        Map<String, String> result = new HashMap<String, String>();
        if (sArray == null || sArray.size() <= 0) {
            return result;
        }
        for (String key : sArray.keySet()) {
            String value = sArray.get(key);
            if (value == null || value.equals("") || key.equalsIgnoreCase("sign")
                    || key.equalsIgnoreCase("sign_type")) {
                continue;
            }
            result.put(key, value);
        }
        return result;
    }

    /**
     * 把陣列所有元素排序,並按照“引數=引數值”的模式用“&”字元拼接成字串
     *
     * @param params 需要排序並參與字元拼接的引數組
     * @return 拼接後字串
     */
    public static String createLinkString(Map<String, String> params) {
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);
        String prestr = "";
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            if (i == keys.size() - 1) {// 拼接時,不包括最後一個&字元
                prestr = prestr + key + "=" + value;
            } else {
                prestr = prestr + key + "=" + value + "&";
            }
        }
        return prestr;
    }

    /**
     * @param
     * @param
     * @param
     */
    public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
        // 建立SSLContext
        StringBuffer buffer = null;
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(requestMethod);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.connect();
            //往伺服器端寫內容
            if (null != outputStr) {
                OutputStream os = conn.getOutputStream();
                os.write(outputStr.getBytes("utf-8"));
                os.close();
            }
            // 讀取伺服器端返回的內容
            InputStream is = conn.getInputStream();
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            BufferedReader br = new BufferedReader(isr);
            buffer = new StringBuffer();
            String line = null;
            while ((line = br.readLine()) != null) {
                buffer.append(line);
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }

    public static String urlEncodeUTF8(String source) {
        String result = source;
        try {
            result = java.net.URLEncoder.encode(source, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml資料。
     *
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map doXMLParse(String strxml) throws Exception {
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        /*=============  !!!!注意,修復了微信官方反饋的漏洞,更新於2018-10-16  ===========*/
        try {
            Map<String, String> data = new HashMap<String, String>();
            // TODO 在這裡更換
            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);

            InputStream stream = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilderFactory.newDocumentBuilder().parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }
    }

    /**
     * 獲取子結點的xml
     *
     * @param children
     * @return String
     */
    public static String getChildrenText(List children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }
    public static InputStream String2Inputstream(String str) {
        return new ByteArrayInputStream(str.getBytes());
    }
}

4、總結下支付中踩過的坑

1、簽名時一定要按照介面定義的規則簽名,字必須的欄位名稱,交易金額的單位,簽名的時候,可以使用微信的工具 微信介面除錯工具 來驗證,程式碼裡的簽名要跟工具生成的簽名一致
2、支付金鑰key是否有效正確,第一次拿錯了,博主在這裡卡了比較久,支付key錯誤導致統一下單報錯:<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[簽名錯誤]]></return_msg></xml>
3、按照文中步驟,微信支付親測可用,文中如有錯誤或者不合理,歡迎留言,微信退款會在後續獻上