前言
公司最近在做微信小程序,被分配到做支付這一塊,現在對這一塊做一個簡單的總結和梳理。
支付,對於購物來說,可以說是占據了十分重要的一塊,畢竟能收到錢才是重點。
當然在開發之前,我們需要有下面這些東西:
- appId
- 密鑰(小程序配置界面)
- 商戶號
- api密鑰(商家后台自己設置)
當然這些是不用我們自己申請的,公司會有人申請好,然后要什么跟這個人說,讓他提供就可以了。
首先來看一下官方給出的業務流程時序圖
這個圖很清晰的表達了在小程序支付中的整個流程,每一步要做些什么。
一個完整的支付,一般情況下都是包含了下面三個主要的點;
- 支付(正常是支付平台提供的h5頁面讓用戶操作,主要是輸密碼)
- 通知(用戶完成一筆支付了,支付平台要通知商家支付結果,商家收到結果后進行一些相應的處理)
- 查詢(與第二點有點反過來的意思,商家自己主動去支付平台查詢支付的結果,然后根據結果做相應的處理)
下面就重點來簡單實現一下上面說的第一點,支付,也是可以進行下面兩步的在大前提。
支付的簡單實現
小程序的實現
簡單起見,在index.wxml中添加一個輸入框和一個button,綁定一下相應的事件,輸入框主要是用於輸入訂單號,按鈕用於模擬提交一個訂單並發起支付。
<!--index.wxml--> <view class="container"> <input type="text" bindinput="getOrderCode" style="border:1px solid #ccc;" /> <button bindtap="pay">立即支付</button> </view>
然后在index.js中寫上一小段代碼,主要是處理上面按鈕的點擊事件。
Page({
data: {
txtOrderCode: '' }, pay: function () { var ordercode = this.data.txtOrderCode; wx.login({ success: function (res) { if (res.code) { wx.request({ url: 'https://www.yourdomain.com/pay', data: { code: res.code,//要去換取openid的登錄憑證 ordercode: ordercode }, method: 'GET', success: function (res) { console.log(res.data) wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: 'MD5', paySign: res.data.paySign, success: function (res) { // success console.log(res); }, fail: function (res) { // fail console.log(res); }, complete: function (res) { // complete console.log(res); } }) } }) } else { console.log('獲取用戶登錄態失敗!' + res.errMsg) } } }); }, getOrderCode: function (event) { this.setData({ txtOrderCode: event.detail.value }); } })
可以看到,在這里Catcher先通過wx.login這個API先取到了登錄的憑證code,並把這個憑證code做為請求參數用wx.request這個API發起一個網絡請求。
在這個網絡請求處理后會返回小程序支付所需要的相關參數。拿到這些參數后,再調用wx.requestPayment這個支付API,此時才算是真正的發起支付。
至此,小程序這邊的事已經做完了,接下來就是要去處理接口那邊的事了,其實接口要做的就是返回小程序需要的幾個參數。但是要拿到這幾個參數還是需要做不少事情的。
接口的實現
據悉最新版的Senparc.Weixin.MP已經支付了小程序相關的內容,但是公司用的版本還是比較低
並且近期也沒有打算對這個組件進行升級。所以就從白紙一張開始了。
用的是mvc,所以這個小程序發起的網絡請求會由下面的action的執行,里面的實現,每一步做了什么應該也已經很清晰了。
public ActionResult Pay(string code, string ordercode) { var paramter = new Parameters(); paramter.out_trade_no = ordercode; //使用登錄憑證 code 獲取 session_key 和 openid var unifiedorderRes = GetOpenIdAndSessionKey(paramter.appid, paramter.secret, code); //反序列化session_key 和 openid成ChangeResponseEntity實體 var tmp = JsonConvert.DeserializeObject<ChangeResponseEntity>(unifiedorderRes); //統一下單的url和參數 var payUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder"; var param = GetUnifiedOrderParam(tmp.openid, paramter); //統一下單后拿到的xml結果 var payResXML = Helper.DoPost(param, payUrl); var payRes = XDocument.Parse(payResXML); var root = payRes.Element("xml"); //序列化相應參數返回給小程序 var res = GetPayRequestParam(root, paramter.appid, paramter.key); return Json(res, JsonRequestBehavior.AllowGet); }
由於只是一個演示的過程,不想這些數據經常以字符串的形式頻繁出現在代碼中,所以把相關的參數全部都放到了一個名為Parameters的類中(放到配置文件中也是可以的),除了訂單號是從小程序傳過來的,當然在實際中這是不合理的,畢竟像金額這些東西,不可能每次都是同一個!這點是要注意的。
下面先來看看這個Parameters類的定義:
public class Parameters { public string appid { get { return "申請的appid"; } } public string mchid { get { return "申請的商戶號"; } } public string nonce { get { return Helper.GetNoncestr(); } } public string notify_url { get { return "http://yourdomain.com/notifyurl"; } } public string body { get { return "testpay"; } } public string out_trade_no { get; set; } public string spbill_create_ip { get { return "IP地址"; } } public string total_fee { get { return "1"; } } public string trade_type { get { return "JSAPI"; } } public string key { get { return "在商家后台設置的密鑰"; } } public string secret { get { return "在配置小程序時的密鑰"; } } }
首先是獲取到登錄憑證后發起的這個網絡請求。這個網絡請求是決定了這次支付能否成功的第一步!
下面要做的是用登錄憑證去換我們要的openid。
/// <summary> /// 取openid和session_key /// </summary> /// <param name="appid"></param> /// <param name="secret"></param> /// <param name="js_code"></param> /// <returns></returns> private string GetOpenIdAndSessionKey(string appid, string secret, string js_code) { var url = string.Format("https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code" , appid,secret,js_code); var request = WebRequest.Create(url) as HttpWebRequest; var response = url.GetResponse(); var respStream = response.GetResponseStream(); var res = string.Empty; using (var reader = new StreamReader(respStream, Encoding.UTF8)) { res = reader.ReadToEnd(); } return res; }
要換取openid,就要向微信提供的地址發起一個網絡請求,並在URL帶上appid,secret和憑證code這三個參數。
然后就可以拿到一個下面形式的json字符串
{
"openid": "OPENID", "session_key": "SESSIONKEY" }
拿到之后自然就是要對這個字符串進行json的反序列化,這里用到了json.net這個包。
根據時序圖,下面要調用統一下單這個接口了。
上面的代碼,在統一下單這一塊,又分為下面幾個步驟
- 處理統一下單的參數(簽名和組裝xml)
- 發起POST請求
- 解析請求得到的結果
參數的處理:
具體規則參見:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
/// <summary> /// 取統一下單的請求參數 /// </summary> /// <param name="openid"></param> /// <param name="param"></param> /// <returns></returns> private string GetUnifiedOrderParam(string openid, Parameters param) { //參與統一下單簽名的參數,除最后的key外,已經按參數名ASCII碼從小到大排序 var unifiedorderSignParam = string.Format("appid={0}&body={1}&mch_id={2}&nonce_str={3}¬ify_url={4}&openid={5}&out_trade_no={6}&spbill_create_ip={7}&total_fee={8}&trade_type={9}&key={10}" , param.appid, param.body, param.mchid, param.nonce, param.notify_url , openid, param.out_trade_no, param.spbill_create_ip, param.total_fee, param.trade_type, param.key); //MD5 var unifiedorderSign = Helper.GetMD5(unifiedorderSignParam).ToUpper(); //構造統一下單的請求參數 return string.Format(@"<xml> <appid>{0}</appid> <body>{1}</body> <mch_id>{2}</mch_id> <nonce_str>{3}</nonce_str> <notify_url>{4}</notify_url> <openid>{5}</openid> <out_trade_no>{6}</out_trade_no> <spbill_create_ip>{7}</spbill_create_ip> <total_fee>{8}</total_fee> <trade_type>{9}</trade_type> <sign>{10}</sign> </xml> ", param.appid, param.body, param.mchid, param.nonce, param.notify_url, openid , param.out_trade_no, param.spbill_create_ip, param.total_fee, param.trade_type, unifiedorderSign); }
這里要注意一點,由於我們的傳的trade_type是JSAPI,所以這里必須是要加上openid進行處理的。
然后就是解析統一下單返回的XML了,說是解析,其實也就是要拿到我們需要的數據罷了。這里最后會得到一個小程序支付API需要的參數實體。
/// <summary> /// 獲取返回給小程序的支付參數 /// </summary> /// <param name="root"></param> /// <param name="appid"></param> /// <param name="key"></param> /// <returns></returns> private PayRequesEntity GetPayRequestParam(XElement root,string appid,string key) { //當return_code 和result_code都為SUCCESS時才有我們要的prepay_id if (root.Element("return_code").Value == "SUCCESS" && root.Element("result_code").Value == "SUCCESS") { var package = "prepay_id=" + root.Element("prepay_id").Value; var nonceStr = Helper.GetNoncestr(); var signType = "MD5"; var timeStamp = Convert.ToInt64((DateTime.Now - new DateTime(1970, 1, 1)).TotalSeconds).ToString(); var paySignParam = string.Format("appId={0}&nonceStr={1}&package={2}&signType={3}&timeStamp={4}&key={5}", appid, nonceStr, package, signType, timeStamp, key); var paySign = Helper.GetMD5(paySignParam).ToUpper(); var payEntity = new PayRequesEntity { package = package, nonceStr = nonceStr, paySign = paySign, signType = signType, timeStamp = timeStamp }; return payEntity; } return new PayRequesEntity(); }
支付參數實體對應的內容如下:
/// <summary> /// 小程序支付需要的參數 /// </summary> public class PayRequesEntity { /// <summary> /// 時間戳從1970年1月1日00:00:00至今的秒數,即當前的時間 /// </summary> public string timeStamp { get; set; } /// <summary> /// 隨機字符串,長度為32個字符以下。 /// </summary> public string nonceStr { get; set; } /// <summary> /// 統一下單接口返回的 prepay_id 參數值 /// </summary> public string package { get; set; } /// <summary> /// 簽名算法 /// </summary> public string signType { get; set; } /// <summary> /// 簽名 /// </summary> public string paySign { get; set; } }
需要注意的是,這里的簽名操作,一定是要配合appId,這也是Catcher在支付這一塊踩的唯一的一個坑,所以提醒一下各位讀者,希望能避開這個坑。
還有最后一步就是要返回一個序列化的對象給小程序,以供小程序使用。
到這里,后台接口也已經OK了,現在就用真機掃描二維碼,點擊立即支付按鈕,此時就會彈出要你輸入密碼的框框,輸入你的微信支付密碼,如下所示:
然后就會提示支付成功,如下所示:
幾秒鍾之后就會收到微信支付發來的消息,通知你在什么時候支出了多少錢。
通知的簡單說明
前面也提到了,通知是用戶支付成功后,微信的服務器會向我們統一下單指定的notify_url發起一個異步的回調。
下面用偽代碼來表示這一過程
public ActionResult Notify() { //1.獲取微信通知的參數 //2.更新訂單的相關狀態 //3.返回一個xml格式的結果給微信服務器 var res = @"<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> </xml>"; return Content(res); }
這里需要注意的是要處理好微信重復通知的情況!
查詢的簡單說明
通知和查詢本質上都是想知道訂單是否支付成功了。
它們的區別是:通知是微信主動通知商家; 查詢是商家主動向微信發起查詢;
這兩個動作的主體是不一樣的。
當微信能正常發起推送並且商家接收這個推送的服務器又沒有掛的時候,查詢的作用是微乎其微的。
當然,不可避免的會出現,微信不能正常發起推送或者商家的服務器掛了,這個時候查詢的作用就變得很重要了!!
這個時候我們就要建交起一個定時作業來專門處理這種情況了,可以選擇Quartz.Net,Hangfire等!
這個作業的內容具體如下:
public void QueryJob() { //1.找到要查詢的訂單號 //2.根據訂單號和appId等內容向https://api.mch.weixin.qq.com/pay/orderquery這個地址發起網絡請求 //3.拿到微信返回的結果 //4.根據結果進行相應的處理 }
至於多久執行一次這個作業,可能就要根據使用小程序進行購物的數量多不多來做一個大致的估計。
總結
小程序的支付還是算是比較簡單,畢竟文檔還算齊全,基本照着文檔的提示就能把這個支付做好。