最近做個對接微信服務商平臺的小程式專案,大概要實現的流程是:a)特約商戶進件 > b)生成帶引數的小程式碼 > c)小程式支付 > d)分賬,記錄一下,希望能對需要的朋友有所幫助
開始
在開始之前建議仔細讀微信官方文件,介面規則及api文件
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay-1.shtml
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml
目錄
整個流程開發步驟如下:
一、(簽名)
二、(獲取證書、敏感資訊加密)
三、(上傳圖片)
四、(特約商戶進件)
五、(生成小程式碼)
六、(微信小程式支付)
七、(分賬)
正文
在開始之前請確保你已經獲取商戶號、證書、祕鑰、小程式appid、appsecret
一、簽名
文件地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
1、生成簽名
/// <param name="url">微信的介面地址</param> /// <param name="method">請求的方式GET,POST,PUT</param> /// <param name="jsonParame">post請求的資料,json格式 ,get時傳空</param> /// <param name="privateKey">apiclient_key.pem中的內容,不要-----BEGIN PRIVATE KEY----- -----END PRIVATE KEY-----</param> /// <param name="merchantId">發起請求的商戶(包括直連商戶、服務商或渠道商)的商戶號 mchid</param> /// <param name="serialNo">商戶證書號</param> /// <returns></returns> protected string GetAuthorization(string url, string method, string jsonParame, string privateKey, string merchantId, string serialNo) { var uri = new Uri(url); string urlPath = uri.PathAndQuery; string nonce = Guid.NewGuid().ToString(); var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); //資料簽名 HTTP請求方法\n介面地址的url\n請求時間戳\n請求隨機串\n請求報文主體\n method = string.IsNullOrEmpty(method) ? "" : method; string message = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n", method, urlPath, timestamp, nonce, jsonParame); string signTxt = Sign(message, privateKey); //Authorization和格式 string authorzationTxt = string.Format("WECHATPAY2-SHA256-RSA2048 mchid=\"{0}\",nonce_str=\"{1}\",timestamp=\"{2}\",serial_no=\"{3}\",signature=\"{4}\"", merchantId, nonce, timestamp, serialNo, signTxt ); return authorzationTxt; } protected string Sign(string message, string privateKey) { byte[] keyData = Convert.FromBase64String(privateKey); using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob)) using (RSACng rsa = new RSACng(cngKey)) { byte[] data = System.Text.Encoding.UTF8.GetBytes(message); return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); } }
2、放到請求頭
string Authorization = GetAuthorization(url, method, postData, privateKey, merchantId, serialNo); request.Headers.Add("Authorization", Authorization);
3、完整的請求方法
/// <param name="url">微信的介面地址</param> /// <param name="postData">post請求的資料,json格式 </param> /// <param name="privateKey">apiclient_key.pem中的內容,不要-----BEGIN PRIVATE KEY----- -----END PRIVATE KEY-----</param> /// <param name="merchantId">發起請求的商戶(包括直連商戶、服務商或渠道商)的商戶號 mchid</param> /// <param name="serialNo">商戶證書號</param> /// <returns></returns> public string postJson(string url, string postData, string privateKey, string merchantId, string serialNo, string method = "POST") { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; request.ContentType = "application/json;charset=UTF-8"; request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36"; request.Accept = "application/json"; string Authorization = GetAuthorization(url, method, postData, privateKey, merchantId, serialNo); request.Headers.Add("Authorization", Authorization); if (!string.IsNullOrEmpty(postData)) { byte[] paramJsonBytes; paramJsonBytes = System.Text.Encoding.UTF8.GetBytes(postData); request.ContentLength = paramJsonBytes.Length; Stream writer; try { writer = request.GetRequestStream(); } catch (Exception) { writer = null; Console.Write("連線伺服器失敗!"); } writer.Write(paramJsonBytes, 0, paramJsonBytes.Length); writer.Close(); } HttpWebResponse response; try { response = (HttpWebResponse)request.GetResponse(); } catch (WebException ex) { response = ex.Response as HttpWebResponse; } Stream resStream = response.GetResponseStream(); StreamReader reader = new StreamReader(resStream); string text = reader.ReadToEnd(); return text; }
二、獲取證書、敏感資訊加密
呼叫特約商戶進件介面之前需要做三個工作:獲取證書、敏感資訊加密、上傳圖片
獲取證書的目的是敏感資訊加密需要用證書裡解密得到的pubkey,然後用pubkey去對敏感資訊進行加密
1、獲取證書
獲取證書介面比較簡單,直接呼叫上邊的請求方法
public static certModel GetCert() { string url = "https://api.mch.weixin.qq.com/v3/certificates"; string merchantId = WxPayConfig.MCHID; //商戶號 string serialNo = WxPayConfig.SERIAL_NO; //證書編號 string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私鑰不包括私鑰檔案起始的-----BEGIN PRIVATE KEY----- 亦不包括結尾的-----END PRIVATE KEY----- string transactionsResponse = postJson(url, string.Empty, privateKey, merchantId, serialNo,"GET"); var result = JsonConvert.DeserializeObject<certModel>(transactionsResponse); return result; }
用到的model
public class certModel { public List<Data> data { get; set; } } public class Data { public string serial_no { get; set; } public string effective_time { get; set; } public string expire_time { get; set; } public Encrypt_certificate encrypt_certificate { get; set; } } public class Encrypt_certificate { public string algorithm { get; set; } public string nonce { get; set; } public string associated_data { get; set; } public string ciphertext { get; set; } }
呼叫成功直接返回證書list,我們需要用v3祕鑰解密得到公鑰
var cmodel = GetCert().data.OrderByDescending(t => t.expire_time).FirstOrDefault(); string pubkey = AesGcmHelper.AesGcmDecrypt(cmodel.encrypt_certificate.associated_data, cmodel.encrypt_certificate.nonce, cmodel.encrypt_certificate.ciphertext); pubkey = pubkey.Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", ""); //解密方法 public class AesGcmHelper { private static string ALGORITHM = "AES/GCM/NoPadding"; private static int TAG_LENGTH_BIT = 128; private static int NONCE_LENGTH_BYTE = 12; private static string AES_KEY = WxPayConfig.V3KEY;//你的v3祕鑰 public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext) { GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine()); AeadParameters aeadParameters = new AeadParameters( new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)), 128, Encoding.UTF8.GetBytes(nonce), Encoding.UTF8.GetBytes(associatedData)); gcmBlockCipher.Init(false, aeadParameters); byte[] data = Convert.FromBase64String(ciphertext); byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)]; int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0); gcmBlockCipher.DoFinal(plaintext, length); return Encoding.UTF8.GetString(plaintext); } }
2、敏感資訊加密
我們上一步得到了pubkey,然後對一些敏感資訊欄位(如使用者的住址、銀行卡號、手機號碼等)進行加密
//text 為要加密的欄位值 RSAEncrypt(text, UTF8Encoding.UTF8.GetBytes(pubkey)); public static string RSAEncrypt(string text, byte[] publicKey) { using (var x509 = new X509Certificate2(publicKey)) { using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key) { var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), RSAEncryptionPadding.OaepSHA1); return Convert.ToBase64String(buff); } } }
這一步需要注意
//使用OaepSHA1 var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), RSAEncryptionPadding.OaepSHA1);
三、上傳圖片
特約商戶進件需要上傳身份證、營業執照、銀行卡等,這就需要通過圖片上傳API預先生成MediaID
先看介面文件https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter2_1_1.shtml
我封裝了一個方法,直接上程式碼
public string UploadImg(string imgPath) { string filePath = HttpContext.Current.Server.MapPath(imgPath); var filename = Path.GetFileName(filePath); FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); Byte[] imgBytesIn = new Byte[fs.Length]; fs.Read(imgBytesIn, 0, imgBytesIn.Length); fs.Close(); byte[] hash = SHA256Managed.Create().ComputeHash(imgBytesIn); StringBuilder builder = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { builder.Append(hash[i].ToString("x2")); } var sha256 = builder.ToString(); string metaStr = "{\"filename\":\""+ filename + "\",\"sha256\":\"" + sha256 + "\"}"; string media_id = UploadImgApi(metaStr, imgBytesIn, filename); return media_id; } public static string UploadImgApi(string metaStr, Byte[] imgBytesIn,string filename) { string url = "https://api.mch.weixin.qq.com/v3/merchant/media/upload"; string merchantId = WxPayConfig.MCHID; //商戶號 string serialNo = WxPayConfig.SERIAL_NO; //證書編號 string privateKey = WxPayConfig.PRIVATEKEY; #region 定義請求體中的內容 並轉成二進位制 string boundary = "lc199aecd61b4653ef"; string Enter = "\r\n"; string campaignIDStr1 = "--" + boundary + Enter + "Content-Disposition: form-data; name=\"meta\";" + Enter + "Content-Type:application/json;" + Enter + Enter + metaStr + Enter + "--" + boundary + Enter + "Content-Disposition:form-data;name=\"file\";filename=\""+ filename + "\";" + Enter + "Content-Type:image/jpeg" + Enter + Enter; byte[] byteData2 = imgBytesIn; string campaignIDStr3 = Enter + "--" + boundary + Enter; var byteData1 = System.Text.Encoding.UTF8.GetBytes(campaignIDStr1); var byteData3 = System.Text.Encoding.UTF8.GetBytes(campaignIDStr3); #endregion string transactionsResponse = UploadImg_postJson(url, byteData1, byteData2, byteData3, metaStr, privateKey, merchantId, serialNo, boundary, "POST"); var result=JsonConvert.DeserializeObject<uploadModel>(transactionsResponse); Thread.Sleep(500); return result.media_id; } public class uploadModel { public string media_id { get; set; } }
上傳圖片api需要注意請求主體型別、參與簽名的字串及body格式
我又單獨寫了個請求方法
public string UploadImg_postJson(string url, byte[] b1, byte[] b2, byte[] b3, string metaStr, string privateKey, string merchantId, string serialNo, string boundary, string method = "POST") { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; //request.ContentType = "application/json;charset=UTF-8"; request.ContentType = "multipart/form-data;boundary=" + boundary; request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36"; request.Accept = "application/json"; string Authorization = GetAuthorization(url, method, metaStr, privateKey, merchantId, serialNo); request.Headers.Add("Authorization", Authorization); Stream writer; try { writer = request.GetRequestStream(); } catch (Exception) { writer = null; } writer.Write(b1, 0, b1.Length); writer.Write(b2, 0, b2.Length); writer.Write(b3, 0, b3.Length); writer.Close(); HttpWebResponse response; try { response = (HttpWebResponse)request.GetResponse(); } catch (WebException ex) { response = ex.Response as HttpWebResponse; } Stream resStream = response.GetResponseStream(); StreamReader reader = new StreamReader(resStream); string text = reader.ReadToEnd(); return text; }
最終返回media_id,存入合適的位置,方便使用
{ "media_id": "H1ihR9JUtVj-J7CJqBUY5ZOrG_Je75H-rKhTG7FUmg9sxNTbRN54dFiUHnhg rBQ6EKeHoGcHTJMHn5TAuLVjHUQDBInSWXcIHYXOeRa2OHA" }
四、特約商戶進件
上邊步驟通了之後,到這就很簡單了,這一步的主要難點是請求引數太多了,很容易出錯
注意:
• 商戶上送敏感資訊時使用微信支付平臺公鑰加密,證書序列號包含在請求HTTP頭部的Wechatpay-Seria
需要在請求介面helder里加入Wechatpay-Seria
request.Headers.Add("Wechatpay-Serial", serial_no);
另外一點注意的這裡的serial_no是GetCert()介面裡獲取到的serial_no
呼叫成功返回微信支付申請單號
{ "applyment_id": 2000002124775691 }
這一步完成之後可以拿到applyment_id請求查詢申請單介面獲取結果
成功返回sign_url簽約連線發給特約商戶相應負責人完成簽約,即結束商戶進件流程
五、生成帶引數的小程式碼
特約商戶簽約完成之後,服務商平臺便可以代商戶發起收款,此時我們需要分別給不同的商戶生成不同的收款碼,其實只需要傳入商家的id即可區別處理
//storeid是商家唯一Id public static string CreateQR(string storeid) { { var page = "pages/custom/index";//掃碼開啟頁面 var scene = storeid;//引數 //獲取小程式的appid和secret var appId = WxPayConfig.XCXAPPID; var secret = WxPayConfig.XCXKEY; string result = HttpGet($"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={secret}"); tokenModel rb = JsonConvert.DeserializeObject<tokenModel>(result); var url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + rb.access_token; var strUrl = url; var request = (HttpWebRequest)WebRequest.Create(strUrl); request.Method = "POST"; request.ContentType = "application/json;charset=UTF-8"; var data = new JsonData { ["page"] = page, ["scene"] = scene }; string jso = data.ToJson(); var payload = Encoding.UTF8.GetBytes(jso); request.ContentLength = payload.Length; var writer = request.GetRequestStream(); writer.Write(payload, 0, payload.Length); writer.Close(); var response = (HttpWebResponse)request.GetResponse(); var s = response.GetResponseStream(); var qrCodeImgBase64 = StreamToBytes(s); //將流儲存 string filename= storeid + ".png"; string returnpath= "/UpLoadFiles/StoreQR/" + filename; string filepath = HttpContext.Current.Server.MapPath("/UpLoadFiles/StoreQR/") + filename; System.IO.File.WriteAllBytes(filepath, qrCodeImgBase64); return returnpath; } }
六、小程式支付JSAPI下單介面
有了商家碼就能分別為商家發起支付申請,簽約商戶都有一個分配的商戶號sub_mchid,注意請求引數裡的商戶號即這個,需要根據二維碼的引數去獲取
public static string V3Pay(string sub_mchid,string openid,int amount,string ordernumber,string description) { string url = "https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi"; string appid = WxPayConfig.XCXAPPID; string merchantId = WxPayConfig.MCHID; //商戶號 string serialNo = WxPayConfig.SERIAL_NO; //證書編號 string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私鑰不包括私鑰檔案起始的-----BEGIN PRIVATE KEY----- 亦不包括結尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData(); postData.SetValue("sp_appid", appid); postData.SetValue("sp_mchid", merchantId); postData.SetValue("sub_mchid", sub_mchid); postData.SetValue("description", description); postData.SetValue("out_trade_no", ordernumber); postData.SetValue("notify_url", WxPayConfig.NOTIFY_URL); WxPayData settle_info = new WxPayData(); settle_info.SetValue("profit_sharing",true); postData.SetValue("settle_info", settle_info); WxPayData _amount = new WxPayData(); _amount.SetValue("total", amount); _amount.SetValue("currency", "CNY"); postData.SetValue("amount", _amount); WxPayData payer = new WxPayData(); payer.SetValue("sp_openid", openid); postData.SetValue("payer", payer); var postJson = postData.ToJsonFor(); string result = postJson(url, postJson, privateKey, merchantId, serialNo, "POST"); var result = JsonConvert.DeserializeObject<payModel>(result); return result.prepay_id; }
請求引數按照自己的方法去構建,json格式
通過JSAPI下單介面獲取到發起支付的必要引數prepay_id,然後使用微信支付提供的小程式方法調起小程式支付
public static string GetJsApiParameters(string prepay_id) { string appid = WxPayConfig.XCXAPPID; string privateKey = WxPayConfig.PRIVATEKEY; string timestamp = WxPayApi.GenerateTimeStamp(); string nonceStr = WxPayApi.GenerateNonceStr(); string package = "prepay_id=" + prepay_id; WxPayData jsApiParam = new WxPayData(); jsApiParam.SetValue("appId", appid); jsApiParam.SetValue("timeStamp", timestamp); jsApiParam.SetValue("nonceStr", nonceStr); jsApiParam.SetValue("package", package); jsApiParam.SetValue("signType", "RSA"); string message = string.Format("{0}\n{1}\n{2}\n{3}\n", appid, timestamp, nonceStr, package); string signTxt = Sign(message, privateKey); jsApiParam.SetValue("paySign", signTxt); string parameters = jsApiParam.ToJson(); return parameters; }
返回給小程式呼叫wx.requestPayment(OBJECT)發起微信支付
注意回撥URL:該連結是通過基礎下單介面中的請求引數“notify_url”來設定的,要求必須為https地址。請確保回撥URL是外部可正常訪問的,且不能攜帶字尾引數,否則可能導致商戶無法接收到微信的回撥通知資訊。回撥URL示例: “https://pay.weixin.qq.com/wxpay/pay.action”
public class V3Notify { public callbackViewModel GetNotifyData() { //接收從微信後臺POST過來的資料 System.IO.Stream s = System.Web.HttpContext.Current.Request.InputStream; int count = 0; byte[] buffer = new byte[1024]; StringBuilder builder = new StringBuilder(); while ((count = s.Read(buffer, 0, 1024)) > 0) { builder.Append(Encoding.UTF8.GetString(buffer, 0, count)); } s.Flush(); s.Close(); s.Dispose(); var ReadStr = builder.ToString(); notifyModel wxPayNotify = Newtonsoft.Json.JsonConvert.DeserializeObject<notifyModel>(ReadStr); //開始解密 string WxPayResourceDecryptModel = AesGcmHelper.AesGcmDecrypt(wxPayNotify.resource.associated_data, wxPayNotify.resource.nonce, wxPayNotify.resource.ciphertext); var decryptModel= Newtonsoft.Json.JsonConvert.DeserializeObject<WxPayResourceDecryptModel>(WxPayResourceDecryptModel); var viewModel = new callbackViewModel(); if (decryptModel != null) { //查詢 var model = queryOrder(decryptModel.out_trade_no, decryptModel.sp_mchid, decryptModel.sub_mchid);//訂單查詢介面 viewModel.code = model.trade_state; viewModel.message = model.trade_state_desc; } else { viewModel.code = "FAIL"; viewModel.message = "資料解密失敗"; } return viewModel; } }
整個下單沒有牽涉到業務方面的程式碼,你可以把你的業務程式碼寫在合適的位置處理
七、分賬
在請求分賬之前先在微信服務商後臺邀約商戶授權分賬並指定分賬比例(最大30%),並且發起支付申請時請求引數指定profit_sharing為true
1、首先新增分賬接收方
接收方必須先呼叫介面新增才能在下一步請求分賬時使用,注意不允許重複的openid
public static receiverModel AddReceivers(string sub_mchid,string type,string account,string relation_type) { string url = "https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add"; string appid = WxPayConfig.XCXAPPID; string merchantId = WxPayConfig.MCHID; //商戶號 string serialNo = WxPayConfig.SERIAL_NO; //證書編號 string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私鑰不包括私鑰檔案起始的-----BEGIN PRIVATE KEY----- 亦不包括結尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData(); postData.SetValue("sub_mchid", sub_mchid); postData.SetValue("appid", appid); postData.SetValue("type", type); postData.SetValue("account", account); postData.SetValue("relation_type", relation_type); string postJson = postData.ToJson(); string transactionsResponse = postJson(url, postJson, privateKey, merchantId, serialNo, "POST"); var result = JsonConvert.DeserializeObject<receiverModel>(transactionsResponse); return result; }
2、請求分賬
public static receiverModel PayReceivers(string sub_mchid, string transaction_id, string out_order_no,List<WxPayData> receivers) { string url = "https://api.mch.weixin.qq.com/v3/profitsharing/orders"; string appid = WxPayConfig.XCXAPPID; string merchantId = WxPayConfig.MCHID; //商戶號 string serialNo = WxPayConfig.SERIAL_NO; //證書編號 string privateKey = WxPayConfig.PRIVATEKEY; // NOTE: 私鑰不包括私鑰檔案起始的-----BEGIN PRIVATE KEY----- 亦不包括結尾的-----END PRIVATE KEY----- WxPayData postData = new WxPayData(); postData.SetValue("sub_mchid", sub_mchid); postData.SetValue("appid", appid); postData.SetValue("transaction_id", transaction_id); postData.SetValue("out_order_no", out_order_no); postData.SetValue("unfreeze_unsplit", true); postData.SetValue("receivers", receivers); string postJson = postData.ToJsonFor(); LogHelper.WriteLogToFile("PayReceivers-postJson:" + postJson); string transactionsResponse = postJson(url, postJson, privateKey, merchantId, serialNo, "POST"); var result = JsonConvert.DeserializeObject<receiverModel>(transactionsResponse); return result; }
用到的model
public class receiverModel { public string sub_mchid { get; set; } public string type { get; set; } public string account { get; set; } public string name { get; set; } public string relation_type { get; set; } public string custom_relation { get; set; } }
至此完整流程全部走完,總結下來技術難度並不大,就是步驟多,並且要把握好呼叫的時機,再一個就是請求引數多,容易出錯,要耐心。
如有問題請在評論區留言或私信我