Java後臺實現小程式微信支付
阿新 • • 發佈:2018-12-23
本人第一篇部落格,之前筆記一直放在有道雲,想寫部落格很久了,一直沒下定決心,也沒靜下心好好寫,今天突然跟朋友談 到平時工作根本沒時間學習、整理、總結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、按照文中步驟,微信支付親測可用,文中如有錯誤或者不合理,歡迎留言,微信退款會在後續獻上