前言
公司最近在做微信小程式,被配置設定到做支付這一塊,現在對這一塊做一個簡單的總結和梳理。
支付,對于購物來說,可以說是占據了十分重要的一塊,畢竟能收到錢才是重點。
當然在開發之前,我們需要有下面這些東西:
- 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 = request.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請求
- 解析請求得到的結果
參數的處理:
/// <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.根據結果進行相應的處理
}
至于多久執行一次這個作業,可能就要根據使用小程式進行購物的數量多不多來做一個大緻的估計。
總結
小程式的支付還是算是比較簡單,畢竟文檔還算齊全,基本照着文檔的提示就能把這個支付做好。