1.微信支付的官網對接手冊:
地址:https://pay.weixin.qq.com/wiki/doc/api/index.html
微信對接分多中方式,而native是調用微信支付服務在網頁生成二維碼,客戶掃描二維碼支付。
2.准備工作
(1)申請服務號,需要審核認證,300塊錢。
(2)服務號申請通過后,在微信公眾平台開通微信支付
開通參考地址:https://jingyan.baidu.com/album/e8cdb32b0bb7de37042bad7b.html?picindex=3
簡單說下開通流程,需要幾個重要信息,身份證正反面照片,一個對公的銀行賬戶和卡號,申請提交后等待3-5個工作日進行審核。開通的時候回填一個郵箱,開通成功后回把一些信息發到郵箱上面。
(3)開通微信支付的郵箱接收到的商品平台信息:
開通之后,APPID是知道了,商戶號也知道了。
(4)登錄微信商用平台
去設置API密匙,下載證書,密匙設置好后不會顯示,所以記得保存。
注意:證書是退款是用到的,不光退款,是你的錢往外出的時候用到證書,可以不用管,下面會講到退款怎么處理。
設置回調接口,就是微信支付成功之后,微信會回調,填寫一個回調地址。
此時,API密匙有了,簽名加密方式就用MD5就行,證書也有了。
(5)另外需要的參數
還剩一個開發者密碼:
這個應該是申請服務號之后設置的,設置之后不會顯示,所以提前需要保存。(我是用公司的服務號,都是開通好的,但是不知道開發者密碼,只能選擇重置)
重置:
(6)參數已經全了,說一些題外的。
微信分公眾平台,開放平台,商戶平台總共三個平台。了解一下三個平台的區別。
微信三個平台區分(開放,公眾,商戶平台):https://blog.csdn.net/atongmu2017/article/details/94728996
ps:微信開放平台中可以在查看移動應用中去開通微信支付功能,但是這個是對接微信支付的另一種方式,好像是app拉起微信支付,類似於外賣下單支付的時候跳到微信手機客戶端的支付界面。(注意這個區分)
查看界面:
3.下載 SDK
地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
下載的壓縮包解壓出來有幾個類:
扔到項目下:
我項目用的是jdk1.8,WXPayXmlUtil這個類會報錯,提示XMLConstants.FEATURE_SECURE_PROCESSING找不到。
解決辦法:把下面這個jar包從libraries中移除,報錯消失。消失之后,再次加入libraries中,也不再報錯。
4.生成訂單二維碼
頁面請求生成二維碼有關的類:
CommonUtil.java
有兩個方法,一個是發送http請求,用於發送xml參數給微信服務器,另一個是獲取ip,xml參數中有個參數是ip
package com.jeeplus.modules.wxpay; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import javax.net.ssl.HttpsURLConnection; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import third.wxpay.WXPayUtil; public class CommonUtil { public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) { try { URL url = new URL(requestUrl); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); 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 (Exception e) { e.printStackTrace(); } return null; } /** * 獲取ip * @param request * @return */ public static String getIp(HttpServletRequest request) { if (request == null) return ""; String ip = request.getHeader("X-Requested-For"); if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Forwarded-For"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
ConfigUtil.java
存放一些支付參數和回調接口信息
package com.jeeplus.modules.wxpay; public class ConfigUtil { /** * 服務號相關信息 */ public final static String APPID = "";//服務號的應用號 public final static String MCH_ID = "";//商戶號 public final static String APP_SECRECT = "";//服務號的應用密碼 public final static String API_KEY = "";//API密鑰 public final static String SIGN_TYPE = "MD5";//簽名加密方式 public final static String CERT_PATH = "D:/project/cert/apiclient_cert.p12";//微信支付證書存放路徑地址 public final static String TOKEN = "";//服務號的配置token //微信支付統一接口的回調action //填寫自己網站的接口 public final static String NOTIFY_URL = "http://www.baidu.com"; }
MatrixToImageWriter.java
地址變成二維碼圖片的類,用到 com.google.zxing.common.BitMatrix這個類,用到zxing這個jar包
package com.jeeplus.modules.wxpay; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import javax.imageio.ImageIO; import java.io.File; import java.io.OutputStream; import java.io.IOException; import java.util.Hashtable; import java.awt.image.BufferedImage; public final class MatrixToImageWriter { private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; private MatrixToImageWriter() {} public 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 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); } } public static void main(String[] args) throws Exception { String text = "www.baidu.com"; int width = 300; int height = 300; String format = "gif"; Hashtable hints = new Hashtable(); hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints); File outputFile = new File("d:"+File.separator+"new.gif"); MatrixToImageWriter.writeToFile(bitMatrix, format, outputFile); } }
創建訂單的方法:
private void doWx(HttpServletRequest request,HttpServletResponse response) throws Exception { String number=request.getParameter("number")==null?"":request.getParameter("number"); Productorder p = productorderService.findUniqueByProperty("number", number); Date date=new Date(); SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddHHmmss"); String timeStart=sdf.format(date); Calendar cal=Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 1); Date date1=cal.getTime(); String timeExpire=sdf.format(date1); SortedMap<String,String> parameters = new TreeMap<String,String>(); parameters.put("appid", ConfigUtil.APPID); parameters.put("body", p.getPname()); parameters.put("mch_id", ConfigUtil.MCH_ID); parameters.put("out_trade_no", number); parameters.put("spbill_create_ip",CommonUtil.getIp(request)); DecimalFormat df = new DecimalFormat("#"); parameters.put("total_fee", df.format(Double.parseDouble(p.getOrdermoney())*100)); parameters.put("trade_type", "NATIVE"); parameters.put("time_expire", CommonUtil.getOrderExpireTime(startData,5*60*1000L));//二維碼過期時間5分鍾 parameters.put("nonce_str", WXPayUtil.generateNonceStr()); parameters.put("notify_url", ConfigUtil.NOTIFY_URL);//支付成功后回調的action,與JSAPI相同 String generateSignature = WXPayUtil.generateSignature(parameters, ConfigUtil.API_KEY, SignType.MD5); parameters.put("sign", generateSignature); String generateSignedXml = WXPayUtil.generateSignedXml(parameters, ConfigUtil.API_KEY); System.out.println("微信支付預下單請求xml格式::"+generateSignedXml); String result =CommonUtil.httpsRequest(ConfigUtil.UNIFIED_ORDER_URL, "POST", generateSignedXml); System.out.println(result); Map<String, String> map; try { map = WXPayUtil.xmlToMap(result); String returnCode = map.get("return_code"); String resultCode = map.get("result_code"); if(returnCode.equalsIgnoreCase("SUCCESS")&&resultCode.equalsIgnoreCase("SUCCESS")){ String codeUrl = map.get("code_url"); //TODO 拿到codeUrl,寫代碼生成二維碼 System.out.println("codeUrl="+codeUrl); int width = 300; int height = 300; //二維碼的圖片格式 String format = "JPEG"; Hashtable hints = new Hashtable(); //內容所使用編碼 hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, width, height, hints); // response.setContentType("image/JPEG"); MatrixToImageWriter.writeToStream(bitMatrix, format, response.getOutputStream()); } } catch (Exception e) { e.printStackTrace(); }
可以把生成二維碼的方法寫成一個接口,測試調用接口,頁面會顯示二維碼
掃描二維碼:
5.創建訂單生成二維碼注意的問題
(1)簽名算法,需要參數拼接起來,加上密匙參數,拼接后MD5加密得到sign這個參數。而其他參數拼接的時候注意要按參數名ASCII碼從小到大排序(字典序),所以用到SortedMap
SortedMap<String,String> parameters = new TreeMap<String,String>();
(2)測試的時候生成二維碼,之后修改訂單金額,請求后顯示不出來二維碼,微信返回信息如下,原因是同一個訂單不允許前后兩次請求金額不同。
<err_code><![CDATA[INVALID_REQUEST]]></err_code><err_code_des><![CDATA[201 商戶訂單號重復]]></err_code_des>
(3)total_fee這個訂單金額參數需要注意,單位是分,所以需要把元轉換成分。
解決辦法:字符串金額轉換成double類型,乘以100,然后去掉小數點后的一個零
-
DecimalFormat df = new DecimalFormat("#");
-
parameters.put( "total_fee", df.format(Double.parseDouble(p.getOrdermoney())*100));
有個特別容易出錯的地方,如果金額是12.34,轉換成double類型,然后乘100,你以為最后返回的是1234這個數,其實是1234.0,然后傳給微信也是不成功的。必須是整數。
下面是金額不對的時候,微信返回的錯誤提示信息:
<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[invalid total_fee]]></return_msg>
6.回調接口
支付成功之后,微信會回調你在商用平台上設置的微信回調接口,里面處理支付成功后的業務邏輯。下面的提供參考:
/** * 微信回調接口 * http://127.0.0.1:8080/qcloud/a/payInterface/notifyWeiXinPay * @param request * @param response * @return * @throws Exception */ @RequestMapping(value="/notifyWeiXinPay",produces="text/html;charset=utf-8") @ResponseBody public String notifyWeiXinPay(HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String,String> return_data = new HashMap<String,String>(); //讀取參數 InputStream inputStream ; StringBuffer sb = new StringBuffer(); inputStream = request.getInputStream(); String s ; BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); while ((s = in.readLine()) != null){ sb.append(s); } in.close(); inputStream.close(); //解析xml成map Map<String, String> map = WXPayUtil.xmlToMap(sb.toString()); //判斷簽名是否正確 // if(true) { if(WXPayUtil.isSignatureValid(map, ConfigUtil.API_KEY)) { if(!map.get("return_code").toString().equals("SUCCESS")){ return_data.put("return_code", "FAIL"); return_data.put("return_msg", "return_code不正確"); }else{ if(!map.get("result_code").toString().equals("SUCCESS")){ return_data.put("return_code", "FAIL"); return_data.put("return_msg", "result_code不正確"); return WXPayUtil.mapToXml(return_data); } String orderno = (String)map.get("out_trade_no");//商戶訂單號 String transaction_id = (String)map.get("transaction_id");//微信支付訂單號 String time_end = (String)map.get("time_end");//支付完成時間yyyyMMddHHmmss BigDecimal total_fee = new BigDecimal(map.get("total_fee").toString()); //付款完成后,支付寶系統發送該交易狀態通知 Productorder order = productorderService.findUniqueByProperty("order_no", orderno); if(order==null) { System.out.println("訂單不存在"); return_data.put("return_code", "FAIL"); return_data.put("return_msg", "訂單不存在"); return WXPayUtil.mapToXml(return_data); } BigDecimal num = new BigDecimal("100"); BigDecimal ordermoney = new BigDecimal(order.getOrdermoney()); ordermoney = ordermoney.multiply(num); //訂單已經支付 if(order.getOrderstatus().equals("1")){ System.out.println("訂單已經支付"); return_data.put("return_code", "SUCCESS"); return_data.put("return_msg", "OK"); return WXPayUtil.mapToXml(return_data); } //如果支付金額不等於訂單金額返回錯誤 if(ordermoney.compareTo(total_fee)!=0){ System.out.println("資金異常"); return_data.put("return_code", "FAIL"); return_data.put("return_msg", "金額異常"); return WXPayUtil.mapToXml(return_data); } //更新訂單信息 try { System.out.println("更新訂單信息"); SimpleDateFormat sdf1=new SimpleDateFormat("yyyyMMddHHmmss"); SimpleDateFormat sdf2=new SimpleDateFormat("YYYY-MM-dd hh:mm:ss"); order.setOrderstatus("1"); order.setPaytime(sdf2.format(sdf1.parse(time_end))); order.setPaymentway("1"); productorderService.update(order); System.out.println("插入已經支付的訂單表"); //插入已經支付的訂單表(product_order_pay) List<String> goodsIds = productgoodsService.getGoodsIdsByOrderno(orderno); for (String gid : goodsIds) { Productorderpay productorderpay = new Productorderpay(); productorderpay.setUserid(order.getUserid()); productorderpay.setPpid(gid); productorderpay.setEndtime(order.getOrderendtime()); productorderpayService.insert(productorderpay); } return_data.put("return_code", "SUCCESS"); return_data.put("return_msg", "OK"); return WXPayUtil.mapToXml(return_data); } catch (Exception e) { e.printStackTrace(); return_data.put("return_code", "FAIL"); return_data.put("return_msg", "更新訂單失敗"); return WXPayUtil.mapToXml(return_data); } } } else{ System.out.println("通知簽名驗證失敗"); return_data.put("return_code", "FAIL"); return_data.put("return_msg", "簽名錯誤"); } return WXPayUtil.mapToXml(return_data); }
7.支付頁面
(1)顯示二維碼:頁面中img標簽來顯示二維碼,img的src指向的是生成二維碼的請求。
(2)查詢訂單支付狀態:同一個頁面,去寫一個定時異步請求方法,去查詢訂單是否支付成功,支付成功做下一步處理。
(3)頁面代碼參考:
<body> <!--nav--> <jsp:include page="/eb_headerhs2017.jsp" /> <div class="section"> <div class="section_div"> <div class="section_div_top clear"> <p class="section_div_p">訂單提交成功,請盡快付款!訂單號:${orderNo}</p> <p class="section_div_p2"> 應付金額 <span class="section_div_span">${orderMoney}</span> 元 </p> </div> <div class="section_div_bottom clear"> <p class="section_bottom_p">微信支付</p> <div class="QRcode"> <img class="QRcode_img" src="填寫生成二維碼接口?orderNo=${orderNo}" alt="二維碼"> <div class="QRcode_div clear"> <img class="QRcode_div_img" src="images/hs/weixinzhifuerweima_10.png" alt=""> <div class="QRcode_div_div"> <p>請使用微信掃一掃</p> <p>掃描二維碼支付</p> </div> </div> </div> <img class="phone_saosao" src="images/hs/phone_saosao_03.png" alt=""> <p class="section_bottom_p2">選擇其他支付方式</p> </div> </div> </div> <script type="text/javascript"> var wxTM = setInterval(function() { getWxPayStatus(); },5000) function getWxPayStatus(){ jQuery.ajax({ type: 'POST', url: "填寫查詢支付狀態接口?orderNo=${orderNo}", dataType: 'json', success: function(result){ if(result.res==1){ clearInterval(wxTM); window.location.href="ebHsPay.jsp?payStatus=1&orderNo=${orderNo}"; } } }); } </script> <div class="con_bot"></div> <!--bottom--> <jsp:include page="/eb_footerhs2017.jsp" /> </body>
8.訂單支付狀態接口
微信提供了查詢訂單狀態的接口,但是一般不去對接微信。對接微信的只涉及一個生產二維碼接口。
怎么去查詢支付狀態,在支付回調的接口中已經知道訂單支付成功還是失敗,把狀態更新到數據庫對應的訂單數據中。查詢訂單狀態是從數據庫差的,而不是非得去對接微信的查詢訂單狀態接口。
9.退款
上面提到自己的賬戶往外掏錢(退款、發紅包)會用到證書,但是退款一般也不對接微信。
退款的業務邏輯:可以創建一個退款申請表,記錄用戶退款申請,項目中有這么一個版塊。讓使用項目管理員去查看這些數據,聯系用戶為什么退款,讓管理員自己處理,真正退款是在微信商用平台上面去退款。平台上有退款的功能,而不是去寫代碼對接微信退款。
原因:一方面不用寫這塊代碼。另一方面,萬一你這個網站退款的接口被黑會是個問題。還有就是退款請求不會太多,何必走接口。
10.其他
自己也是第一次對接微信,有些細節需要注意的地方,我是向經常做商品支付項目的公司同事詢問了解的。
支付這塊考慮詳細點,代碼的可擴展,復用等。越詳細越好。不至於之后客戶一提需求就改動這塊代碼,或者這塊代碼根本用不了。
(1)建表:一般是訂單表和商品表,訂單只存訂單的信息,商品去關聯訂單。比如考慮是否有優惠折扣,創建對應的表。
訂單表字段參考
(2)查詢用戶是否訂購過這個訂單
訂單表是從一開始數據是不會刪除的,里面的數據只會越來越多。里面有支付成功的訂單,超時支付失效的訂單等等。
比如,用戶訂購了一個視頻,查詢用戶是否訂購了這個視頻,是否能播放。不可能去查詢原始的訂單表,到后面數據量大了,查詢速度特別慢。需要另建一個表,里面只保存支付成功的訂單,一些關鍵信息,如訂單編號,訂單用戶,到期時間。去查詢這個表,之后可以把這個表里的過期訂單刪除。
(3)訂單編號
一開始我的訂單編號是隨機生成的十六位數字,但是不要這樣做。
訂單號要一眼能看到這個訂單的信息,比如BOOK20190511xxxx,VIDEO20180613xxx,當看到這個訂單就知道這個訂單是訂購的什么,訂購的時間。在訂單編號里面加上用戶信息等等,到時候去查詢這個訂單的時候,看到這個訂單編號大致先了解這個訂單的信息,而不是一堆隨機數據,什么信息也看不出來。
(4)支付從頁面到后台的流程
我看到一個電商平台的項目代碼,里面的流程可以參考下。
頁面用img的src屬性去請求一個controller的pay方法,這個方法中什么都沒做,只是處理下請求參數,然后然后請求到原來的頁面。二維碼怎么生成,在請求返回的過程中,攔截器攔截了請求,根據參數生成二維碼返回原來頁面。
11.補充
(1)微信回調
上面因為我只有一個類型的訂單,回調寫在回調的方法里,但是這樣寫不好。
回調方法處理是個入口,根據不同的訂單類型再去跳轉不同的回調處理方法。便於之后的擴展,調整修改方便,而不是一改動就去改統一的那個回調方法。
(2)手機app接微信支付
因為這種支付方式注意是網站生成二維碼,用戶掃描支付的。
手機app接微信支付,最好使接像上面提到的,類似美團的,訂外賣支付直接跳轉到微信。
現在這種方式也可以,需要注意一些地方:
①微信創建訂單接口,會返回一個支付地址,之前是把這個地址生成二維碼圖片展示,而手機app是直接打開這個地址,手機會跳到微信支付。
②支付成功的頁面,需要判斷是從網站來的還是手機來的,分別顯示不同的頁面。
③支付成功之后的回調,手機app會跳到空白頁,蘋果的會打開賽風瀏覽器顯示支付成功的頁面。再跳回app很多余
原文鏈接:https://blog.csdn.net/atongmu2017/article/details/94725934