小程序的業務流程以下php
商戶系統和微信支付系統主要交互說明:java
步驟1:用戶在商戶APP中選擇商品,提交訂單,選擇微信支付。git
步驟2:商戶後臺收到用戶支付單,調用微信支付統一下單接口。參見【統一下單API】。redis
步驟3:統一下單接口返回正常的prepay_id,再按簽名規範從新生成簽名後,將數據傳輸給APP。參與簽名的字段名爲appid,partnerid,prepayid,noncestr,timestamp,package。注意:package的值格式爲Sign=WXPay算法
步驟4:商戶APP調起微信支付。api參見本章節【app端開發步驟說明】apache
步驟5:商戶後臺接收支付通知。api參見【支付結果通知API】json
步驟6:商戶後臺查詢支付結果。,api參見【查詢訂單API】小程序
API連接:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3api
支付的流程爲:先調用統一下單API---->接着在小程序wx.requestPayment發起支付---->支付完以後會調用支付結果通知微信
小程序端代碼
/** * 支付 */ var pay = function (event, that) { if (that.data.detail.fee.indexOf("免費")>-1){ goApply(event, that) }else{ wx.request({ url: app.globalData.server + 'requestPay/', method: "POST", data: { activityId: event.currentTarget.dataset.activityid, userId: app.globalData.userInfo.id, sessionThirdKey: wx.getStorageSync('sessionThirdKey'), money: that.data.detail.fee, describe: that.data.detail.name, detail: '報名活動費用' }, header: { "Content-Type": "application/x-www-form-urlencoded" }, success: function (res) { console.info(res); //發起微信支付 wx.requestPayment({ 'timeStamp': res.data.timeStamp, 'nonceStr': res.data.nonceStr, 'package': res.data.package_, 'signType': 'MD5', 'paySign': res.data.paySign, success: function (res) { console.info(res) //報名 goApply(event, that) }, fail: function (res) { console.info(res) }, complete: function (res) { console.info(res) } }) } }) } }
後臺Java代碼:(基於SpringBoot)
@RestController public class PayApi { @Value("${wxapp.appid}") private String appId; @Value("${wxapp.secret}") private String secret; @Value("${wxapp.url.jscode2session}") private String jscode2session; @Value("${wx.mch.id}") private String mchId; @Value("${wx.unified.order.url}") private String createOrderURL; @Value("${wx.pay.api.key}") private String key; @Value("${wx.bill.create.ip}") private String spBillCreateIp; @Value("${wx.server.url}") private String baseUrl; @Autowired private RedisClient redisClient; @Resource(name = "wxappUserServiceImpl") private IWxappUserService wxappUserService; @Resource(name = "wxappActivityServiceImpl") private IWxappActivityService wxappActivityService; @Resource(name = "wxappActivityApplyServiceImpl") private IWxappActivityApplyService wxappActivityApplyService; @Resource(name = "wxappPayServiceImpl") private IWxappPayService wxappPayService; @RequestMapping(value = "/requestPay",method = RequestMethod.POST) public WxappPayDto requestPay(String userId, String activityId, String sessionThirdKey, String money, String describe, String detail) throws Exception { WxappPayDto dto = new WxappPayDto(); //獲取保存的sessionThirdKey(裏面保存了openId) String sessionKey = redisClient.get(sessionThirdKey); String openId = sessionKey.split("w#w#w")[0]; //訂單號 String orderNo="wx"+userId+"_"+System.currentTimeMillis(); dto = prePay(userId,activityId,openId,orderNo,money,describe,detail); return dto; } /** * 統一下單 * @param userId * @param activityId * @param openId * @param orderNo * @param money * @param describe * @param detail * @return */ private WxappPayDto prePay(String userId,String activityId,String openId,String orderNo,String money,String describe,String detail){ money = String.valueOf(Long.valueOf(money.substring(0, money.length()-1))*100); String currTime = PayUtils.getCurrTime(); //8位日期 String strTime = currTime.substring(8, currTime.length()); //四位隨機數 String strRandom = PayUtils.buildRandom(4) + ""; //10位序列號,能夠自行調整。 String nonceStr = strTime + strRandom; //這裏notify_url是 支付完成後微信發給該連接信息,能夠判斷會員是否支付成功,改變訂單狀態等。 String notifyUrl = baseUrl+"/notify"; //附加數據,以必定格式保存userId和activityId。原樣返回。 String attach = userId+"#wx#"+activityId; SortedMap<String, String> packageParams = new TreeMap<String, String>(); packageParams.put("appid", appId); packageParams.put("attach", attach);//附加數據 packageParams.put("body", describe);//商品描述 packageParams.put("detail", detail); packageParams.put("mch_id", mchId);//商戶號 packageParams.put("nonce_str", nonceStr);//隨機數 packageParams.put("notify_url", notifyUrl); packageParams.put("openid", openId); packageParams.put("out_trade_no", orderNo);//商戶訂單號 packageParams.put("spbill_create_ip", spBillCreateIp);//訂單生成的機器 IP packageParams.put("total_fee", money);//總金額 packageParams.put("trade_type", "JSAPI"); String sign = PayUtils.createSign(packageParams,key); String xml="<xml>"+ "<appid>"+appId+"</appid>"+ "<attach>"+attach+"</attach>"+ "<body><![CDATA["+describe+"]]></body>"+ "<detail><![CDATA["+detail+"]]></detail>"+ "<mch_id>"+mchId+"</mch_id>"+ "<nonce_str>"+nonceStr+"</nonce_str>"+ "<sign>"+sign+"</sign>"+ "<notify_url>"+notifyUrl+"</notify_url>"+ "<openid>"+openId+"</openid>"+ "<out_trade_no>"+orderNo+"</out_trade_no>"+ "<spbill_create_ip>"+spBillCreateIp+"</spbill_create_ip>"+ "<total_fee>"+money+"</total_fee>"+ "<trade_type>JSAPI</trade_type>"+ "</xml>"; String prepay_id=""; try { prepay_id = PayUtils.getPayNo(createOrderURL, xml); if(prepay_id.equals("")){ //錯誤提示 System.out.println("統一支付接口獲取預支付訂單出錯"); } } catch (Exception e1) { e1.printStackTrace(); } SortedMap<String, String> finalpackage = new TreeMap<String, String>(); String timestamp = PayUtils.getTimeStamp(); String packages = "prepay_id="+prepay_id; finalpackage.put("appId", appId); finalpackage.put("nonceStr", nonceStr); finalpackage.put("package", packages); finalpackage.put("signType", "MD5"); finalpackage.put("timeStamp", timestamp); String finalsign = PayUtils.createSign(finalpackage,key); WxappPayDto dto = new WxappPayDto(); dto.setNonceStr(nonceStr); dto.setPackage_(packages); dto.setPaySign(finalsign); dto.setSignType("MD5"); dto.setTimeStamp(timestamp); return dto; } /** * 支付完成通知 * @param request * @param response * @return * @throws Exception */ @RequestMapping(value = "/notify",method = RequestMethod.POST) public String notify(HttpServletRequest request, HttpServletResponse response) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream)request.getInputStream())); String line = null; StringBuilder sb = new StringBuilder(); while((line = br.readLine())!=null){ sb.append(line); } //解析並給微信發回收到通知確認 Map map = PayUtils.doXMLParse(sb.toString()); String returnCode = map.get("return_code").toString(); if(returnCode.equals("SUCCESS")){ String resultCode = map.get("result_code").toString(); if(resultCode.equals("SUCCESS")){ SortedMap<String, String> packageParams = new TreeMap<String, String>(); packageParams.put("appid", map.get("appid").toString()); packageParams.put("attach", map.get("attach").toString()); packageParams.put("bank_type", map.get("bank_type").toString()); packageParams.put("cash_fee", map.get("cash_fee").toString()); packageParams.put("fee_type", map.get("fee_type").toString()); packageParams.put("is_subscribe", map.get("is_subscribe").toString()); packageParams.put("mch_id", map.get("mch_id").toString()); packageParams.put("nonce_str", map.get("nonce_str").toString()); packageParams.put("openid", map.get("openid").toString()); packageParams.put("out_trade_no", map.get("out_trade_no").toString()); packageParams.put("result_code", map.get("result_code").toString()); packageParams.put("return_code", map.get("return_code").toString()); packageParams.put("time_end", map.get("time_end").toString()); packageParams.put("total_fee", map.get("total_fee").toString()); packageParams.put("trade_type", map.get("trade_type").toString()); packageParams.put("transaction_id", map.get("transaction_id").toString()); String sign = PayUtils.createSign(packageParams,key); String originSign = map.get("sign").toString(); if(sign.equals(originSign)){ //簽名一致,保存支付流水 String xml="<xml>" +"<return_code>SUCCESS</return_code>" +"<return_msg>OK</return_msg>" +"</xml>"; ShopPayLog payLog = new ShopPayLog(); payLog.setCreatedAt(new Date()); payLog.setSource(Source.WeiXin); DecimalFormat df = new DecimalFormat("######0.00"); payLog.setTotalFee(String.valueOf(df.format((Double.valueOf(map.get("total_fee").toString())/100)))); payLog.setTradeNo(map.get("out_trade_no").toString()); payLog.setTransactionId(map.get("transaction_id").toString()); String attach = map.get("attach").toString();//userId+"#wx#"+activityId payLog.setUserId(attach.split("#wx#")[0]); payLog = wxappPayService.save(payLog); WxappUser user = wxappUserService.find(Long.valueOf(attach.split("#wx#")[0])); WxappActivity activity = wxappActivityService.find(Long.valueOf(attach.split("#wx#")[1])); WxappActivityApply activityApply = wxappActivityApplyService.findActivityApplyByUserAndActivity(user, activity); //在活動申請表中關聯上支付流水的id activityApply.setPayLogId(String.valueOf(payLog.getId())); wxappActivityApplyService.save(activityApply); return xml; }else{ String xml="<xml>" +"<return_code>FAIL</return_code>" +"<return_msg>簽名不一致</return_msg>" +"</xml>"; return xml; } }else{ String xml="<xml>" +"<return_code>FAIL</return_code>" +"<return_msg>支付通知失敗</return_msg>" +"</xml>"; return xml; } } else { String xml="<xml>" +"<return_code>FAIL</return_code>" +"<return_msg>支付通知失敗</return_msg>" +"</xml>"; return xml; } }
PayUtils.java
import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.KeyStore; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import javax.net.ssl.SSLContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import org.jdom.Document; import org.jdom.Element; import org.jdom.input.SAXBuilder; import com.pro.profwxappapi.api.PayApi; @SuppressWarnings("deprecation") public class PayUtils { private static Object Server; @SuppressWarnings("deprecation") public static DefaultHttpClient httpclient; private static SortedMap parameters; static { httpclient = new DefaultHttpClient(); httpclient = (DefaultHttpClient) HttpClientConnectionManager.getSSLInstance(httpclient); parameters = new TreeMap(); } /** * 把對象轉換成字符串 * * @param obj * @return String 轉換成字符串,若對象爲null,則返回空字符串. */ public static String toString(Object obj) { if (obj == null) return ""; return obj.toString(); } /** * 把對象轉換爲int數值. * * @param obj * 包含數字的對象. * @return int 轉換後的數值,對不能轉換的對象返回0。 */ public static int toInt(Object obj) { int a = 0; try { if (obj != null) { a = Integer.parseInt(obj.toString()); } } catch (Exception e) { e.printStackTrace(); } return a; } /** * 獲取從1970年開始到如今的秒數 * * @param date * @return */ public static String getTimeStamp() { long seconds = System.currentTimeMillis() / 1000; return String.valueOf(seconds); } /** * 獲取當前時間 yyyyMMddHHmmss * @return String */ public static String getCurrTime() { Date now = new Date(); SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); String s = outFormat.format(now); return s; } /** * 獲取當前日期 yyyyMMdd * @param date * @return String */ public static String formatDate(Date date) { SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd"); String strDate = formatter.format(date); return strDate; } /** * 取出一個指定長度大小的隨機正整數. * @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)); } /** * 獲取編碼字符集 * @param request * @param response * @return String */ public static String getCharacterEncoding(HttpServletRequest request, HttpServletResponse response) { if (null == request || null == response) { return "utf-8"; } String enc = request.getCharacterEncoding(); if (null == enc || "".equals(enc)) { enc = response.getCharacterEncoding(); } if (null == enc || "".equals(enc)) { enc = "utf-8"; } return enc; } public static String URLencode(String content) { String URLencode; URLencode = replace(Server.equals(content), "+", "%20"); return URLencode; } private static String replace(boolean equals, String string, String string2) { return null; } /** * 獲取unix時間,從1970-01-01 00:00:00開始的秒數 * @param date * @return long */ public static long getUnixTime(Date date) { if (null == date) { return 0; } return date.getTime() / 1000; } public static String QRfromGoogle(String chl) { int widhtHeight = 300; String EC_level = "L"; int margin = 0; String QRfromGoogle; chl = URLencode(chl); QRfromGoogle = "http://chart.apis.google.com/chart?chs=" + widhtHeight + "x" + widhtHeight + "&cht=qr&chld=" + EC_level + "|" + margin + "&chl=" + chl; return QRfromGoogle; } /** * 時間轉換成字符串 * @param date 時間 * @param formatType 格式化類型 * @return String */ public static String date2String(Date date, String formatType) { SimpleDateFormat sdf = new SimpleDateFormat(formatType); return sdf.format(date); } public static String getNonceStr() { Random random = new Random(); return MD5Utils.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8"); } /** * 建立簽名SHA1 * @param signParams * @return * @throws Exception */ public static String createSHA1Sign(SortedMap<String, String> signParams) throws Exception { StringBuffer sb = new StringBuffer(); Set es = signParams.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(); sb.append(k + "=" + v + "&"); // 要採用URLENCODER的原始值! } String params = sb.substring(0, sb.lastIndexOf("&")); return getSha1(params); } /** * Sha1簽名 * @param str * @return */ public static String getSha1(String str) { if (str == null || str.length() == 0) { return null; } char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; try { MessageDigest mdTemp = MessageDigest.getInstance("SHA1"); mdTemp.update(str.getBytes("UTF-8")); byte[] md = mdTemp.digest(); int j = md.length; char buf[] = new char[j * 2]; int k = 0; for (int i = 0; i < j; i++) { byte byte0 = md[i]; buf[k++] = hexDigits[byte0 >>> 4 & 0xf]; buf[k++] = hexDigits[byte0 & 0xf]; } return new String(buf); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 得到預支付訂單號 * @param url * @param xmlParam * @return */ public static String getPayNo(String url, String xmlParam) { String prepay_id = ""; try { String jsonStr = postWithXmlParams(url, xmlParam); if (jsonStr.indexOf("FAIL") != -1) { return prepay_id; } Map<String, Object> map = doXMLParse(jsonStr); prepay_id = (String) map.get("prepay_id"); System.out.println("prepay_id:" + prepay_id); } catch (Exception e) { e.printStackTrace(); } return prepay_id; } /** * 發送請求 * @param url 請求路徑 * @param xmlParams xml字符串 * @return */ public static String postWithXmlParams(String url, String xmlParams) { HttpPost httpost = HttpClientConnectionManager.getPostMethod(url); try { httpost.setEntity(new StringEntity(xmlParams, "UTF-8")); HttpResponse response = httpclient.execute(httpost); return EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { e.printStackTrace(); return ""; } } /** * 解析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()); } public String getParameter(String parameter) { String s = (String) this.parameters.get(parameter); return (null == s) ? "" : s; } /** * 特殊字符處理 * @param src * @return * @throws UnsupportedEncodingException */ public String UrlEncode(String src) throws UnsupportedEncodingException { return URLEncoder.encode(src, "UTF-8").replace("+", "%20"); } /** * 獲取package的簽名包 * @param packageParams * @param key * @return * @throws UnsupportedEncodingException */ public String genPackage(SortedMap<String, String> packageParams, String key) throws UnsupportedEncodingException { String sign = createSign(packageParams, 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(); sb.append(k + "=" + UrlEncode(v) + "&"); } // 去掉最後一個& String packageValue = sb.append("sign=" + sign).toString(); return packageValue; } /** * 建立md5摘要,規則是:按參數名稱a-z排序,遇到空值的參數不參加簽名。 */ public static String createSign(SortedMap<String, String> packageParams, String 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=" + key); System.out.println("md5:" + sb.toString()); String sign = MD5Utils.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); System.out.println("packge簽名:" + sign); return sign; } /** * 建立package簽名 */ public boolean createMd5Sign(String signParams) { StringBuffer sb = new StringBuffer(); Set es = this.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 (!"sign".equals(k) && null != v && !"".equals(v)) { sb.append(k + "=" + v + "&"); } } // 算出摘要 String sign = MD5Utils.MD5Encode(sb.toString(), "utf-8").toLowerCase(); String paySign = this.getParameter("sign").toLowerCase(); return paySign.equals(sign); } /** * 輸出XML * @return */ public String parseXML() { StringBuffer sb = new StringBuffer(); sb.append("<xml>"); Set es = this.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 (null != v && !"".equals(v) && !"appkey".equals(k)) { sb.append("<" + k + ">" + getParameter(k) + "</" + k + ">\n"); } } sb.append("</xml>"); return sb.toString(); } public static String post(String url, String xmlParam){ StringBuilder sb = new StringBuilder(); try { KeyStore keyStore = KeyStore.getInstance("PKCS12"); FileInputStream instream = new FileInputStream(new File(PayApi.class.getClassLoader().getResource("apiclient_cert.p12").getPath())); try { keyStore.load(instream, "1344023801".toCharArray()); } finally { instream.close(); } // 證書 SSLContext sslcontext = SSLContexts.custom() .loadKeyMaterial(keyStore, "1344023801".toCharArray()) .build(); // 只容許TLSv1協議 SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext, new String[] { "TLSv1" }, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); //建立基於證書的httpClient,後面要用到 CloseableHttpClient client = HttpClients.custom() .setSSLSocketFactory(sslsf) .build(); HttpPost httpPost = new HttpPost(url);//退款接口 StringEntity reqEntity = new StringEntity(xmlParam); // 設置類型 reqEntity.setContentType("application/x-www-form-urlencoded"); httpPost.setEntity(reqEntity); CloseableHttpResponse response = client.execute(httpPost); try { HttpEntity entity = response.getEntity(); System.out.println(response.getStatusLine()); if (entity != null) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(),"UTF-8")); String text=""; while ((text = bufferedReader.readLine()) != null) { sb.append(text); } } EntityUtils.consume(entity); } catch(Exception e){ e.printStackTrace(); }finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); }finally { httpclient.close(); } return sb.toString(); } }
public class WxappPayDto { private String appId; private String timeStamp; private String nonceStr; private String package_; private String signType; private String paySign; public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public String getTimeStamp() { return timeStamp; } public void setTimeStamp(String timeStamp) { this.timeStamp = timeStamp; } public String getNonceStr() { return nonceStr; } public void setNonceStr(String nonceStr) { this.nonceStr = nonceStr; } public String getPackage_() { return package_; } public void setPackage_(String package_) { this.package_ = package_; } public String getSignType() { return signType; } public void setSignType(String signType) { this.signType = signType; } public String getPaySign() { return paySign; } public void setPaySign(String paySign) { this.paySign = paySign; } }
先從小程序端請求後臺,在後臺發出請求先得到prepay_id,而後再組裝成參數返回到小程序端,再在小程序端發出支付請求。
注意點:
1,算法https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3很重要,必定要遵照規範。這個算法彷佛全部微信支付都會用到。
2,參數https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4,必需的參數一個不能少,並且還要按字典順序。