微信掃碼支付
- 2019 年 12 月 16 日
- 筆記
一,需要申請公司的微信公眾號,以及商戶號。然後在商戶號中關聯微信公眾APPID。在商戶平台添加掃碼支付功能。
二.根據微信支付掃碼開發文檔進行開發
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
在application.yml封裝屬性
##微信公眾號的appid app.wx-pay-appId=xxxxxxxx ###微信公眾號的appSecret app.wx-pay-appSecret=xxxxxxxxx ##微信商戶號 app.wx-pay-mchId=xxxx ##微信商戶號apikey app.wx-pay-apiKey=xxxxxx ###統一下單介面(微信文檔中有) app.wx-pay-ufdoderUrl=https://api.mch.weixin.qq.com/pay/unifiedorder ###通知驗證簽名的介面路徑 app.wx-pay-notifyUrl=http://xxxx/api/wx/get app.wx-pay-refundUrl=https://api.mch.weixin.qq.com/secapi/pay/refund
工具類 HttpUtil
/** * http工具類,負責發起post請求並獲取的返回 */ public class HttpUtil { private final static int CONNECT_TIMEOUT = 5000; // in milliseconds private final static String DEFAULT_ENCODING = "UTF-8"; public static String postData(String urlStr, String data){ return postData(urlStr, data, null); } public static String postData(String urlStr, String data, String contentType){ BufferedReader reader = null; try { URL url = new URL(urlStr); URLConnection conn = url.openConnection(); conn.setDoOutput(true); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(CONNECT_TIMEOUT); if(contentType != null) conn.setRequestProperty("content-type", contentType); OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING); if(data == null) data = ""; writer.write(data); writer.flush(); writer.close(); reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING)); StringBuilder sb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); sb.append("rn"); } return sb.toString(); } catch (IOException e) { //logger.error("Error connecting to " + urlStr + ": " + e.getMessage()); }finally { try { if (reader != null) reader.close(); } catch (IOException e) { } } return null; } //發送get請求方法 public static String sendGet(String url, String param) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; System.out.println(urlNameString); URL realUrl = new URL(urlNameString); // 打開和URL之間的連接 URLConnection connection = realUrl.openConnection(); // 設置通用的請求屬性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 建立實際的連接 connection.connect(); // 獲取所有響應頭欄位 Map<String, List<String>> map = connection.getHeaderFields(); // 遍歷所有的響應頭欄位 for (String key : map.keySet()) { System.out.println(key + "--->" + map.get(key)); } // 定義 BufferedReader輸入流來讀取URL的響應 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("發送GET請求出現異常!" + e); e.printStackTrace(); } // 使用finally塊來關閉輸入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } /** * 獲取用戶實際ip * @param request * @return */ public static String getIpAddr(HttpServletRequest request){ String ipAddress = request.getHeader("X-Real-IP"); if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("x-forwarded-for"); } // String ipAddress = request.getHeader("x-forwarded-for"); if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if(ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){ //根據網卡取本機配置的IP InetAddress inet=null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress= inet.getHostAddress(); } } //對於通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割 if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15 if(ipAddress.indexOf(",")>0){ ipAddress = ipAddress.substring(0,ipAddress.indexOf(",")); } } return ipAddress; } public static String getIpAddress(HttpServletRequest request) throws IOException { // 獲取請求主機IP地址,如果通過代理進來,則透過防火牆獲取真實IP地址 String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } else if (ip.length() > 15) { String[] ips = ip.split(","); for (int index = 0; index < ips.length; index++) { String strIp = (String) ips[index]; if (!("unknown".equalsIgnoreCase(strIp))) { ip = strIp; break; } } } return ip; } /** * 複雜條件下取得ip地址 */ public InetAddress getLocalHostLANAddress() throws Exception { try { InetAddress candidateAddress = null; // 遍歷所有的網路介面 for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) { NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); // 在所有的介面下再遍歷IP for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) { InetAddress inetAddr = (InetAddress) inetAddrs.nextElement(); if (!inetAddr.isLoopbackAddress()) {// 排除loopback類型地址 if (inetAddr.isSiteLocalAddress()) { // 如果是site-local地址,就是它了 return inetAddr; } else if (candidateAddress == null) { // site-local類型的地址未被發現,先記錄候選地址 candidateAddress = inetAddr; } } } } if (candidateAddress != null) { return candidateAddress; } // 如果沒有發現 non-loopback地址.只能用最次選的方案 InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); return jdkSuppliedAddress; } catch (Exception e) { e.printStackTrace(); } return null; } }
創建簽名工具類 PayToolUtil
public class PayToolUtil { /** * 是否簽名正確,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。 * @return boolean */ public static boolean isTenpaySign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.entrySet(); Iterator it = es.iterator(); while(it.hasNext()) { Map.Entry entry = (Map.Entry)it.next(); String k = (String)entry.getKey(); String v = (String)entry.getValue(); if(!"sign".equals(k) && null != v && !"".equals(v)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + API_KEY); //算出摘要 String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase(); String tenpaySign = ((String)packageParams.get("sign")).toLowerCase(); //System.out.println(tenpaySign + " " + mysign); return tenpaySign.equals(mysign); } /** * @author * @date 2016-4-22 * @Description:sign簽名 * @param characterEncoding * 編碼格式 * @return */ public static String createSign(String characterEncoding, SortedMap<Object, Object> packageParams, String API_KEY) { StringBuffer sb = new StringBuffer(); Set es = packageParams.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) { sb.append(k + "=" + v + "&"); } } sb.append("key=" + API_KEY); String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase(); return sign; } /** * @author * @date 2016-4-22 * @Description:將請求參數轉換為xml格式的string * @param parameters * 請求參數 * @return */ public static String getRequestXml(SortedMap<Object, Object> parameters) { StringBuffer sb = new StringBuffer(); sb.append("<xml>"); Set es = parameters.entrySet(); Iterator it = es.iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String k = (String) entry.getKey(); String v = (String) entry.getValue(); if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) { sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">"); } else { sb.append("<" + k + ">" + v + "</" + k + ">"); } } sb.append("</xml>"); return sb.toString(); } /** * 取出一個指定長度大小的隨機正整數. * * @param length * int 設定所取出隨機數的長度。length小於11 * @return int 返回生成的隨機數。 */ public static int buildRandom(int length) { int num = 1; double random = Math.random(); if (random < 0.1) { random = random + 0.1; } for (int i = 0; i < length; i++) { num = num * 10; } return (int) ((random * num)); } /** * 獲取當前時間 yyyyMMddHHmmss * * @return String */ public static String getCurrTime() { Date now = new Date(); SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); String s = outFormat.format(now); return s; } /** * 時間戳 */ public static long getTimeStamp() { Date d = new Date(); long timeStamp = d.getTime() / 1000; //getTime()得到的是微秒, 需要換算成秒 return timeStamp; } public static Map<String, String> xmlToMap(String strXML) throws Exception { try { Map<String, String> data = new HashMap<String, String>(); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); org.w3c.dom.Document doc = documentBuilder.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; } } }
XMLUtil4jdom類
public class XMLUtil4jdom { /** * 解析xml,返回第一級元素鍵值對。如果第一級元素有子節點,則此節點的值是子節點的xml數據。 * @param strxml * @return */ public static Map doXMLParse(String strxml) throws JDOMException, IOException { strxml = strxml.replaceFirst("encoding=".*"", "encoding="UTF-8""); if(null == strxml || "".equals(strxml)) { return null; } Map<String, String> m = new HashMap<String, String>(); InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8")); SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(in); Element root = doc.getRootElement(); List list = root.getChildren(); Iterator it = list.iterator(); while(it.hasNext()) { Element e = (Element) it.next(); String k = e.getName(); String v = ""; List children = e.getChildren(); if(children.isEmpty()) { v = e.getTextNormalize(); } else { v = XMLUtil4jdom.getChildrenText(children); } m.put(k, v); } //關閉流 in.close(); return m; } /** * 獲取子結點的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(XMLUtil4jdom.getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } }
二維碼工具類
public class QRUtil { public static String createQrCode(String url, String path, String fileName) { try { Map<EncodeHintType, String> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, 400, 400, hints); File file = new File(path, fileName); if (file.exists() || ((file.getParentFile().exists() || file.getParentFile().mkdirs()) && file.createNewFile())) { writeToFile(bitMatrix, "jpg", file); System.out.println("搞定:" + file); return file.toString(); } } catch (Exception e) { e.printStackTrace(); } return null; } public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, file)) { throw new IOException("Could not write an image of format " + format + " to " + file); } } public static void writeToStream(BitMatrix matrix, String format, OutputStream stream) throws IOException { BufferedImage image = toBufferedImage(matrix); if (!ImageIO.write(image, format, stream)) { throw new IOException("Could not write an image of format " + format); } } private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; private static BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } public static void main(String[] args) { createQrCode("www.baidu.com","D:\","code.jpg"); } }
seviceImpl的類。統一下單
@Value("${app.wx-pay-appId}") private String appid; @Value("${app.wx-pay-appSecret}") private String appsecret; @Value("${app.wx-pay-mchId}") private String mch_id; @Value("${app.wx-pay-apiKey}") private String key; @Value("${app.wx-pay-ufdoderUrl}") private String ufdoderUrl; @Value("${app.wx-pay-notifyUrl}") private String notify_url; @Value("${app.wx-pay-refundUrl}") private String refundUrl; String requestId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); String out_trade_no =""+System.currentTimeMillis(); //訂單號,先生成隨機數。後修改為正式的 String currTime = PayToolUtil.getCurrTime(); String strTime = currTime.substring(8, currTime.length()); String strRandom = PayToolUtil.buildRandom(4) + ""; //隨機字元串生成 String nonce_str = strTime + strRandom; //交易類型 String trade_type = "NATIVE"; SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>(); packageParams.put("appid", appid); packageParams.put("mch_id", mch_id); packageParams.put("nonce_str", nonce_str);//時間戳 //測試 packageParams.put("body", "哈哈哈哈哈"); packageParams.put("out_trade_no", out_trade_no); //價格的單位為分 packageParams.put("total_fee", "1"); //正式的處理現在一定是小數點兩位的 // packageParams.put("total_fee", 正式的價格.replace(".","")); packageParams.put("spbill_create_ip", "127.0.0.1"); //正式獲取真實IP //packageParams.put("spbill_create_ip", HttpUtil.getIpAddress(request)); packageParams.put("notify_url", notify_url); packageParams.put("trade_type", trade_type); //簽名 String sign = PayToolUtil.createSign("UTF-8", packageParams,key); packageParams.put("sign", sign); String requestXML = PayToolUtil.getRequestXml(packageParams); System.out.println(requestXML); logger.info("requestId:{},function:{},request:{},response:{}",requestId ,"微信產品參數",packageParams.toString(),requestXML); String resXml = HttpUtil.postData(ufdoderUrl, requestXML); logger.info("requestId:{},userId:{},function:{}",requestId ,"請求微信支付"); logger.info("requestId:{},function:{},request:{},response:{}",requestId ,"微信請求支付url",requestXML,resXml); Map map = null; try { map = XMLUtil4jdom.doXMLParse(resXml); } catch (JDOMException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } String urlCode = (String) map.get("code_url"); logger.info("requestId:{},function:{},response:{}",requestId(),"微信獲取支付url",urlCode);
controller層
public void pay(HttpServletRequest request, HttpServletResponse response) { String requestId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); //此處執行付款 try { //得到二維碼鏈接 String text = iserver.xxxx(); logger.info("function:{},response:{}" "微信pc掃碼支付", text); System.out.println(text); //根據url來生成生成二維碼 int width = 300; int height = 300; //二維碼的圖片格式 String format = "jpg"; Hashtable hints = new Hashtable(); //內容所使用編碼 hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); BitMatrix bitMatrix; bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints); QRUtil.writeToStream(bitMatrix, format, response.getOutputStream()); } catch (WriterException e) { logger.info("requestId:{} err:{}", requestId, e); logger.error("requestId:{} err:{}", requestId, e); } catch (IOException e) { logger.info("requestId:{} err:{}", requestId, e); logger.error("requestId:{} err:{}", requestId, e); } }
微信回調
//微信回調 @PostMapping(value = "/get") @SkipUserAuth public void get(HttpServletRequest request, HttpServletResponse response) throws IOException, ServiceException { //讀取參數 InputStream inputStream; StringBuffer sb = new StringBuffer(); inputStream = request.getInputStream(); String s; BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } while ((s = in.readLine()) != null) { sb.append(s); } in.close(); inputStream.close(); //解析xml成map Map<String, String> m = new HashMap<String, String>(); try { m = XMLUtil4jdom.doXMLParse(sb.toString()); } catch (JDOMException e) { e.printStackTrace(); } logger.info("requestId:{},function:{},response:{}" , requestId , "微信回調介面" , sb.toString()); //過濾空 設置 TreeMap SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>(); Iterator it = m.keySet().iterator(); while (it.hasNext()) { String parameter = (String) it.next(); String parameterValue = m.get(parameter); String v = ""; if (null != parameterValue) { v = parameterValue.trim(); } packageParams.put(parameter, v); } // 帳號資訊 String key = apiKey; //判斷簽名是否正確 if (PayToolUtil.isTenpaySign("UTF-8", packageParams, key)) { //------------------------------ //處理業務開始 //------------------------------ String resXml = ""; if ("SUCCESS".equals((String) packageParams.get("result_code"))) { // 這裡是支付成功 String mch_id = (String) packageParams.get("mch_id"); String openid = (String) packageParams.get("openid"); String is_subscribe = (String) packageParams.get("is_subscribe"); String out_trade_no = (String) packageParams.get("out_trade_no"); String total_fee = (String) packageParams.get("total_fee"); //////////執行自己的業務邏輯//////////////// //暫時使用最簡單的業務邏輯來處理:只是將業務處理結果保存到session中 //(根據自己的實際業務邏輯來調整,很多時候,我們會操作業務表,將返回成功的狀態保留下來) request.getSession().setAttribute("_PAY_RESULT", "OK"); System.out.println("支付成功"); //通知微信.非同步確認成功.必寫.不然會一直通知後台.八次之後就認為交易失敗了. resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> "; logger.info("requestId:{},function:{},response:{}" , requestId , "微信回調列印success資訊" , resXml); } else { resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[報文為空]]></return_msg>" + "</xml> "; logger.info("requestId:{},function:{},response:{}" , requestId , "微信回調報文為空" , resXml); } //------------------------------ //處理業務完畢 //------------------------------ BufferedOutputStream out = new BufferedOutputStream( response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); } else { logger.info("requestId:{},function:{}" , requestId , "微信回調簽名驗證失敗"); System.out.println("通知簽名驗證失敗"); } }