今天來分享一下之前做微信小程序微信支付遇到的一些坑,博主這里是微信小程序支付功能,因此選擇的微信支付方式是JSAPI支付方式(溫馨提示左下角有音樂哦)。
首先我們肯定是要在小程序后台綁定一個商戶號的,接下來我們看一下整個開發流程如下圖(微信官方圖):
由此我們就可以得出下面這個支付的大致流程:
首先,選擇商品和數量等,點擊下單,然后后台將這些參數生成數字簽名並以xml的方式傳遞,並調用微信統一下訂單接口生成一張微信預支付訂單表(此時也可以添加上自己業務邏輯),訂單有效期都在半小時內,半小時后該條下單數據就失效了,因此應該在半小時內完成支付,簽名成功后將微信返回的prepay_id等數據返回給前端,再由前端調起收銀台完成支付。
由上面我們大概清楚了兩點:
1.生成數字簽名;
2.調用微信統一下訂單;
3.小程序支付;
那么我們再來看看微信支付AIP接口文檔:傳送門











在返回數據中return_code 和result_code都為SUCCESS的為簽名成功,返回參數含義可以通過傳送門了解,這里博主就不再說明了。
通過AIP接口文檔我們發現要想調用微信統一下訂單接口那么我們就需要傳遞以下必填參數:
字段名 | 變量名 | 必填 | 類型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信分配的小程序ID |
商戶號 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商戶號 |
隨機字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 隨機字符串,長度要求在32位以內。推薦隨機數生成算法 |
簽名 | sign | 是 | String(64) | C380BEC2BFD727A4B6845133519F3AD6 | 通過簽名算法計算得出的簽名值,詳見簽名生成算法 |
商品描述 | body | 是 | String(128) | 騰訊充值中心-QQ會員充值 | 商品簡單描述,該字段請按照規范傳遞,具體請見參數規定 |
商戶訂單號 | out_trade_no | 是 | String(32) | 20150806125346 | 商戶系統內部訂單號,要求32個字符內,只能是數字、大小寫字母_-|*且在同一個商戶號下唯一。詳見商戶訂單號 |
標價金額 | total_fee | 是 | Int | 88 | 訂單總金額,單位為分,詳見支付金額 |
終端IP | spbill_create_ip | 是 | String(64) | 123.12.12.123 | 支持IPV4和IPV6兩種格式的IP地址。調用微信支付API的機器IP |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 異步接收微信支付結果通知的回調地址,通知url必須為外網可訪問的url,不能攜帶參數。 |
交易類型 | trade_type | 是 | String(16) | JSAPI | 小程序取值如下:JSAPI,詳細說明見參數規定 |
以上參數請仔細查看API接口文檔說明,需注意隨機字符轉生成方法及位數,簽名生成算法(博主采用MD5),商戶訂單號也就是自己平台生
成的訂單號,金額是以分為單位;
下面我們重點來說一說數字簽名的生成如圖:
以上是java后台簽名完調用微信統一下訂單的步驟,下面我們來看小程序端完成支付前需要獲取那些數據,官方文檔:傳送門
通過以上我們看到它是需要這些必填參數:
參數 | 類型 | 必填 | 說明 |
---|---|---|---|
timeStamp | String | 是 | 時間戳從1970年1月1日00:00:00至今的秒數,即當前的時間 |
nonceStr | String | 是 | 隨機字符串,長度為32個字符以下。 |
package | String | 是 | 統一下單接口返回的 prepay_id 參數值,提交格式如:prepay_id=* |
signType | String | 是 | 簽名類型,默認為MD5,支持HMAC-SHA256和MD5。注意此處需與統一下單的簽名類型一致 |
paySign | String | 是 | 簽名,具體簽名方案參見微信公眾號支付幫助文檔; |
其中我們需要注意的是paySign這個參數,根據文檔我看可以看到,生成這個參數是需要以下這些必填參數:
字段名 | 變量名 | 必填 | 類型 | 示例值 | 描述 |
---|---|---|---|---|---|
小程序ID | appId | 是 | String | wxd678efh567hg6787 | 微信分配的小程序ID |
時間戳 | timeStamp | 是 | String | 1490840662 | 時間戳從1970年1月1日00:00:00至今的秒數,即當前的時間 |
隨機串 | nonceStr | 是 | String | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 隨機字符串,不長於32位。推薦隨機數生成算法 |
數據包 | package | 是 | String | prepay_id=wx2017033010242291fcfe0db70013231072 | 統一下單接口返回的 prepay_id 參數值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072 |
簽名方式 | signType | 是 | String | MD5 | 簽名類型,默認為MD5,支持HMAC-SHA256和MD5。注意此處需與統一下單的簽名類型一致 |
而剛好這些參數都是調用統一下訂單接口成功后返回的參數,並將這些數據以數字簽名的方式合成paySign字段,如有不清楚簽名方式可以通過表格傳送門了解,完成完這些將數據返回給前端,並由前端調用wx.requesetPayment()方法完成支付。
根據以上的了解咱們話不多說上代碼:
Controller層
@RestController public class WeixinController { public final Logger log = Logger.getLogger(this.getClass()); @Autowired OrdersService ordersService; //微信支付的商戶密鑰(換成自己的) public static final String key = "1111111111111111"; //微信統一下單接口地址 public static final String pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; @RequestMapping(value="/pay/placeanorder" ,method = RequestMethod.POST) private Map<String, String> goPay(@RequestParam Map<String, Object> maps, HttpServletRequest request) throws Exception { String openid=(String) maps.get("openid"); PaymentPo paymentPo=new PaymentPo(); paymentPo.setOpenid(openid); //生成的隨機字符串 String nonce_str = StringUtils.getRandomStringByLength(32); paymentPo.setNonce_str(nonce_str); //商品名稱 String body = (String) maps.get("body"); paymentPo.setBody(body); //獲取本機的ip地址 String spbill_create_ip = IpUtils.getIpAddr(request); paymentPo.setSpbill_create_ip(spbill_create_ip); //小程序金額不是元,單位是分(如:100=1元) paymentPo.setTotal_fee((String) maps.get("totalfee")); String total_fee = String.valueOf(new BigDecimal(paymentPo.getTotal_fee()).multiply(new BigDecimal(100)).intValue()); //商戶訂單號(自己寫邏輯生成) paymentPo.setOut_trade_no(ordersService.getOrderNoByAtomics()); //組裝參數,用戶生成統一下單接口的簽名 Map<String, String> packageParams = new HashMap<String, String>(); packageParams.put("appid", paymentPo.getAppid());//小程序ID packageParams.put("body", paymentPo.getBody());//商品描述 packageParams.put("mch_id", paymentPo.getMch_id());//商戶號 packageParams.put("nonce_str", paymentPo.getNonce_str());//隨機字符串 packageParams.put("notify_url", paymentPo.getNotify_url());//支付成功后的回調地址 packageParams.put("openid", paymentPo.getOpenid()); packageParams.put("out_trade_no", paymentPo.getOut_trade_no());//商戶訂單號 packageParams.put("spbill_create_ip", paymentPo.getSpbill_create_ip());//終端IP packageParams.put("total_fee", total_fee);//支付金額,這邊需要轉成字符串類型,否則后面的簽名會失敗 packageParams.put("trade_type", paymentPo.getTrade_type());//支付方式 String prestr = PayUtil.createLinkString(packageParams); // 把數組所有元素,按照“參數=參數值”的模式用“&”字符拼接成字符串(第一步) log.info("第一次簽名:" + prestr); //MD5運算生成簽名,這里是第一次簽名,用於調用統一下單接口(第二步) String mysign = PayUtil.sign(prestr, key, "utf-8").toUpperCase(); //拼接統一下單接口使用的xml數據,要將上一步生成的簽名一起拼接進去(第三步) String xml = "<xml><appid>" + paymentPo.getAppid() + "</appid>" + "<body>" + paymentPo.getBody() + "</body>" + "<mch_id>" + paymentPo.getMch_id() + "</mch_id>" + "<nonce_str>" + paymentPo.getNonce_str() + "</nonce_str>" + "<notify_url>" + paymentPo.getNotify_url() + "</notify_url>" + "<openid>" + paymentPo.getOpenid() + "</openid>" + "<out_trade_no>" + paymentPo.getOut_trade_no() + "</out_trade_no>" + "<spbill_create_ip>" + paymentPo.getSpbill_create_ip() + "</spbill_create_ip>" + "<total_fee>" + Integer.valueOf(total_fee) + "</total_fee>" + "<trade_type>" + paymentPo.getTrade_type() + "</trade_type>" + "<sign>" + mysign + "</sign>" + "</xml>"; log.info("調試模式_統一下單接口 請求XML數據:" + xml); //調用統一下單接口,並接受返回的結果 String res = PayUtil.httpRequest(pay_url, "POST", xml); log.info("調試模式_統一下單接口 返回XML數據:" + res); // 將解析結果存儲在HashMap中 Map map = PayUtil.doXMLParse(res); String return_code = (String)map.get("return_code");//返回狀態碼 String result_code = (String)map.get("result_code");//結果狀態碼 Map<String, String> result = new HashMap<String, String>();//返回給小程序端需要的參數 String prepay_id = null; if ("SUCCESS".equals(return_code) && return_code.equals(result_code)) { prepay_id = (String) map.get("prepay_id");//返回的預付單信息 result.put("nonceStr", paymentPo.getNonce_str()); result.put("package", "prepay_id=" + prepay_id); Long timeStamp = System.currentTimeMillis() / 1000; result.put("timeStamp", timeStamp + "");//這邊要將返回的時間戳轉化成字符串,不然小程序端調用wx.requestPayment方法會報簽名錯誤 //拼接簽名需要的參數 String stringSignTemp = "appId=" + paymentPo.getAppid() + "&nonceStr=" + paymentPo.getNonce_str() + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp; //再次簽名,這個簽名用於小程序端調用wx.requesetPayment方法完成支付(生成paySign字段) String paySign = PayUtil.sign(stringSignTemp, key, "utf-8").toUpperCase(); result.put("paySign", paySign); result.put("signType", "MD5"); result.put("moneyorderid", paymentPo.getOut_trade_no()); } result.put("appid", paymentPo.getAppid()); log.info("結束1" + res); return result; } }
PaymentPo類
@Setter @Getter public class PaymentPo { private static final long serialVersionUID = 1712467669291115101L; private String appid="1233444";//小程序ID(改為自己的) private String mch_id="123333";//商戶號(改為自己的) private String device_info;//設備號 private String nonce_str;//隨機字符串 private String sign;//簽名 private String body;//商品描述 private String detail;//商品詳情 private String attach;//附加數據 private String out_trade_no;//商戶訂單號 private String fee_type;//貨幣類型 private String spbill_create_ip;//終端IP private String time_start;//交易起始時間 private String time_expire;//交易結束時間 private String goods_tag;//商品標記 private String total_fee;//總金額 private String notify_url="http://11111111";//通知地址(改為自己的) private String trade_type="JSAPI";//交易類型 private String limit_pay;//指定支付方式 private String openid;//用戶標識 private String code;//用戶標識 private String placeId;//用戶標識 private Integer carSum;//用戶購買了多少次 private Integer type;//用戶購卡類型 private String key = "11111111";//微信支付的商戶密鑰(改為自己的) private String pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信統一下單接口地址 }
StringUtils工具類
public class StringUtils extends org.apache.commons.lang3.StringUtils{ /** * StringUtils工具類方法 * 獲取一定長度的隨機字符串,范圍0-9,a-z * @param length:指定字符串長度 * @return 一定長度的隨機字符串 */ public static String getRandomStringByLength(int length) { String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } }
PayUtil工具類
public class PayUtil { /** * 簽名字符串 * * @param text需要簽名的字符串 * @param key 密鑰 * @param input_charset編碼格式 * @return 簽名結果 */ public static String sign(String text, String key, String input_charset) { text = text + "&key=" + key; System.out.println(text); return DigestUtils.md5Hex(getContentBytes(text, input_charset)); } /** * 簽名字符串 * * @param text需要簽名的字符串 * @param sign 簽名結果 * @param key密鑰 * @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()); //對key鍵值按字典升序排序 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; } public String getSignToken(HashMap<String, String> map) { String result = ""; Collection<String> keyset= map.keySet(); List<String> list = new ArrayList<String>(keyset); //對key鍵值按字典升序排序 Collections.sort(list); // 構造簽名鍵值對的格式 StringBuilder sb = new StringBuilder(); for (int i = 0; i < list.size(); i++) { if (list.get(i) != null || list.get(i) != "") { String key = list.get(i); String val = map.get(list.get(i)); if (!(val == "" || val == null)) { sb.append(key + "=" + val + "&"); } } } return result; } /** * @param requestUrl請求地址 * @param requestMethod請求方法 * @param outputStr參數 */ 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; } /** * 向指定 URL 發送POST方法的請求 * * @param url 發送請求的 URL * @param param 請求參數,請求參數應該是 name1=value1&name2=value2 的形式。 * @return 所代表遠程資源的響應結果 */ public static String sendPost(String param,String pay_url) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(pay_url); // 打開和URL之間的連接 URLConnection conn = realUrl.openConnection(); // 設置通用的請求屬性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // conn.setRequestProperty("Pragma:", "no-cache"); // conn.setRequestProperty("Cache-Control", "no-cache"); // conn.setRequestProperty("Content-Type", "text/xml;charset=utf-8"); // 發送POST請求必須設置如下兩行 conn.setDoOutput(true); conn.setDoInput(true); // 獲取URLConnection對象對應的輸出流 out = new PrintWriter(conn.getOutputStream()); // 發送請求參數 out.print(param); // flush輸出流的緩沖 out.flush(); // 定義BufferedReader輸入流來讀取URL的響應 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("發送 POST 請求出現異常!" + e); e.printStackTrace(); } //使用finally塊來關閉輸出流、輸入流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.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; } Map m = new HashMap(); InputStream in = String2Inputstream(strxml); 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 = 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(getChildrenText(list)); } sb.append(value); sb.append("</" + name + ">"); } } return sb.toString(); } public static InputStream String2Inputstream(String str) { return new ByteArrayInputStream(str.getBytes()); } }
那么以上代碼部分就是后端完成前支付的全部過程,支付成功后的業務邏輯我就不再分享,可以通過前端異步通知后端支付成功,並執行平台自己的業務;
當然這其中可能會出現簽名返回錯誤,以及該訂單已支付問題;
· 訂單已支付可能是因為商戶訂單號沒有更新,導致第二次支付是傳遞的是同一個訂單號所導致,可以沿着這個方向去排查;
· 簽名錯誤請仔細查看簽名生成方法,可通過驗證工具去驗證,傳送門
如出現下面問題簽名驗證正確,但是調用微信統一下訂單返回簽名錯誤;
那就請返回查看一下商戶key,需注意商戶key設置官方說明如圖:
仔細看其實它的意思是要32個字符,其中包含數字,大寫字母和小寫字母的組合,一定要有這三個,切記,切記,切記。
下面就是我的測試成功圖片
這里商戶單號就是自己平台所產生的訂單號。
好了今天的分享就到這里了
這里是小宋
如果問題請聯系qq郵箱:1757441379@qq.com或者留言板留言
感謝大家觀看,祝大家生活學習和工作愉快!