前言

  我在 9 年前釋出了 Senparc.Weixin SDK 第一個開源版本,一直維護至今,如今 Stras 已經破 7K,這一路上得到了 .NET 社群的積極響應和支援,也受到了非常多的寶貴建議,甚至程式碼的 PR,目前累計的程式碼貢獻者數量已經超過350人,在此表示衷心的感謝!

  我們也總在第一時間及時更新微信官方的各類介面,其中也包括微信支付。

  如今,針對已經發布了一段時間的“微信支付V3”,我們釋出了一個完全重構後的全新版本:Senparc.Weixin.TenPayV3

  即使您沒有開發過之前版本的微信支付也沒有關係,因為這是一個完全嶄新的開始,下面讓我們開始最新一代的微信支付開發之旅。

關於微信支付 V2 和 V3

  從微信支付 V2 開始,我們第一時間上線了微信支付的功能,並在 2018 年正式分離出獨立的 Senparc.Weixin.TenPay 作為微信支付的專用類庫。

  微信支付自誕生以來進行了多次升級,其中比較容易混淆的是 V2 和 V3 兩個版本號,在繼續介紹之前,必須要做一個說明:

 目前社群中流傳的“微信支付V3”實際上有 2  個版本的說法,一個 V3 是早期微信支付文件和介面進行了一輪升級,當時文件稱其為 V3,後來又出來一個是微信支付官方對 API 的版本號進行了升級,也稱其為 V3。

 後者的 V3 是真正意義上的“微信支付V3”,本次釋出的模組也是針對這個 V3 而言的。

 由於歷史原因,在先前釋出的 Senparc.Weixin.TenPay 中也已經包含了 V2 和 V3 兩個版本的命名,這裡的 V3 就是早期文件的 V3,和“微信支付V3"的用法實際上有很大差別,但在功能上,基本上屬於“微信支付V3”的子集。

快速開發-準備

  這裡,我先從巨集觀演示一下 Senparc.Weixin.TenPayV3 的能力,通過網頁演示和單元測試,完成最簡單的鑑權、支付、退款和訂單拉取功能(這些功能代表了幾乎所有微信支付內部介面的形式),後續的章節將繼續展開細節進行介紹。

  關於具體的介面和流程介紹,大家還是要耐心看官方的文件:https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml,準備好微信支付V3所需的所有配置(V3 比之前的文件已經有了很大的飛躍,照著做基本上可以順利完成)。下面的示例將以【普通商戶+微信公眾號JSAPI】這個組合進行展示,其他組合功能將在後續展開介紹。

  所有微信支付形式的 Sample 已經在開源專案中,預設使用 .NET 6 專案開啟:https://github.com/JeffreySu/WeiXinMPSDK/tree/master/Samples/net6-mvc,為了方便測試,您可以直接下載或者克隆專案,機型測試,對應程式碼可以移植到自己的專案中。

  下載程式碼並開啟上述目錄中的 Senparc.Weixin.Sample.Net6.sln:

  其中,Controller 和 Views 的命名,為了和之前已經誕生的舊版本 V3 區分,我們暫時命名為 RealV3 :

  不需要修改任何程式碼,直接執行 Senparc.Weixin.Sample.NET6 專案,即可開啟 Sample 首頁:

  由於 Sample 集成了微信公眾號、小程式、企業微信、微信支付,以及相關的快取、模擬訊息、文件下載等演示,所以看上去內容比較多,不用著急,Sample 配有詳細的註釋,並且對檔案進行了分類,我們只需聚焦相關的部分。

開發第一步:引用 Nuget 包

  Sample 專案已經引用好了原始碼專案,如果您是全新的專案,可以直接引用 Senparc.Weixin.TenPayV3 包。

  方法一:使用 VS 管理器引用:

  方法二:直接在 .csproj 檔案中引用(注意從 Senparc.Weixin.TenPayV3 網頁檢視最新版本):

    <ItemGroup>
<PackageReference Include="Senparc.Weixin.TenPayV3" Version="0.3.500.2-preview2" />
</ItemGroup>

開發第二步:設定微信支付資訊

  在 Web 專案下面,找到 appsettings.json 檔案,設定微信公眾號和微信支付資訊(其他資訊根據說明,不需要的可以刪除,或者保留原狀),預設情況下只需要修改 SenparcWeixinSetting 節點下的“公眾號”和“微信支付V3(新版)”的對應資訊:

  "SenparcWeixinSetting": {
//注意:所有的字串值都可能被用於字典索引,因此請勿留空字串(但可以根據需要,刪除對應的整條設定)! //微信全域性
"IsDebug": true, //以下不使用的引數可以刪除,key 修改後將會失效 //公眾號
"Token": "微信支付不需要",
"EncodingAESKey": "微信支付不需要",
"WeixinAppId": "MyWeixinAppId",
"WeixinAppSecret": "MyWeixinAppSecret", //微信支付V3(新版)
"TenPayV3_AppId": "MyWeixinAppId(同上)",
"TenPayV3_AppSecret": "MyWeixinAppSecret(同上)",
"TenPayV3_SubAppId": "",
"TenPayV3_SubAppSecret": "",
"TenPayV3_MchId": "xxxxxxxx",
"TenPayV3_SubMchId": "", //子商戶,沒有可留空
"TenPayV3_Key": "79xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"TenPayV3_CertPath": "可留空", //支付證書物理路徑,如:D:\\cert\\apiclient_cert.p12
"TenPayV3_CertSecret": "可留空", //支付證書密碼(原始密碼和 MchId 相同)
"TenPayV3_TenpayNotify": "http://sdk.weixin.senparc.com/TenpayV3/PayNotifyUrl", //http://YourDomainName/TenpayV3/PayNotifyUrl
"TenPayV3_PrivateKey": "MIIExxxxxxxxxxxxxxxxx", //(新)證書私鑰
"TenPayV3_SerialNumber": "5Bxxxxxxxxxxxxxxxxxxxxxx", //證書序列號
"TenPayV3_ApiV3Key": "xxxxxxxxxxxxxxxxxxxxxxxx", //(新)APIv3 金鑰
//如果不設定TenPayV3_WxOpenTenpayNotify,預設在 TenPayV3_TenpayNotify 的值最後加上 "WxOpen"
"TenPayV3_WxOpenTenpayNotify": "http://sdk.weixin.senparc.com/TenpayV3/PayNotifyUrlWxOpen" //http://YourDomainName/TenpayV3/PayNotifyUrlWxOpen
}

說明:TenPayV3_CertPath 和 TenPayV3_CertSecret 是“文件版本V3"時期的遺留產物,在新V3中已經可以忽略

開發第三步:開發商品列表和 JSAPI 支付頁面

  Sample 中提供了一個非常簡約的商品列表和支付(詳情)頁:

功能 Controller檔案 View檔案
商品列表 TenPayRealV3Controller.cs / ProductList() /Views/TenPayRealV3/ProductList.cshtml

JSAPI支付頁面

(商品詳情)

TenPayRealV3Controller.cs / JsApi() /Views/TenPayRealV3/JsApi.cshtml

  具體業務的實現這裡不再展開,相關 OAuth 授權的內容屬於公眾號開發的範疇,詳細介紹可以參考《Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十二):OAuth2.0說明》。

  這裡著重講一下 JSAPI 支付頁面,為了方便演示,Sample 中把 JSAPI 和詳情頁放到了一起,實際專案中,詳情頁可以單獨安排,此處 JSAPI 頁面相當於是訂單支付頁面。

  Controller:

  先看 TenPayRealV3Controller.cs 下的 JsApi() 方法中的關鍵程式碼:

sp_billno = string.Format("{0}{1}{2}", TenPayV3Info.MchId/*10位*/, SystemTime.Now.ToString("yyyyMMddHHmmss"),
TenPayV3Util.BuildRandomStr(6));

  上述程式碼用於生成訂單號(在文件中也叫 out_trade_no),訂單號建議加上日期,方便排序,然後加上流水號或者隨機數,根據具體專案情況而定。這裡一定要確保唯一性。

var notifyUrl = TenPayV3Info.TenPayV3Notify.Replace("/TenpayV3/", "/TenpayRealV3/").Replace("http://", "https://");

  上述程式碼用於定義支付回撥的地址,這裡使用 Replace 是因為 Sample 中相容了 2 套支付示範,實際開發過程中直接設定好 appsettings.json 中的引數即可。

TransactionsRequestData jsApiRequestData = new(TenPayV3Info.AppId, TenPayV3Info.MchId, name + " - 微信支付 V3", sp_billno, new TenpayDateTime(DateTime.Now.AddHours(1), false), null, notifyUrl, null, new() { currency = "CNY", total = price }, new(openId), null, null, null);

  上述程式碼用於組裝訪問預支付介面的引數。

var result = await _basePayApis.JsApiAsync(jsApiRequestData);

  上述程式碼用於呼叫預支付介面,獲取 prepay_id,其中已經在建構函式中定義好的私有變數 _basePayApis(BasePayApis 型別),是執行相關一系列支付介面的例項化類:

        public TenPayRealV3Controller()
{
_tenpayV3Setting = Senparc.Weixin.Config.SenparcWeixinSetting.TenpayV3Setting;
_basePayApis = new BasePayApis(_tenpayV3Setting);
}
                if (result.VerifySignSuccess != true)
{
throw new WeixinException("獲取 prepay_id 結果校驗出錯!");
}

  獲取到 result 後,一定要進行簽名驗證(包括其他介面)!實際的簽名和驗證過程比較複雜,SDK 已經完全封裝好,您只需要確保 VerifySignSuccess 引數為 true 即可。

                var jsApiUiPackage = TenPaySignHelper.GetJsApiUiPackage(TenPayV3Info.AppId, result.prepay_id);
ViewData["jsApiUiPackage"] = jsApiUiPackage;

  上述程式碼用於生成前端 UI JsSdk 所需的所有資訊,包括時間戳、隨機字串、簽名字串等,開發者不需要自行編寫加密演算法,開箱即用。

  jsApiUiPackage 資訊存放在 ViewData["jsApiUiPackage"] 中,在 View 中可以直接被呼叫。實際開發環境下,可以用各類方式傳遞此資訊,包括 Ajax + Json。

  View:

  對應 View 頁面(JsApi.cshtml)關鍵程式碼介紹如下:

        document.addEventListener('WeixinJSBridgeReady', function onBridgeReady() {
//...
}

  上述程式碼是監聽 JSAPI 就緒的方法。

 1               WeixinJSBridge.invoke('getBrandWCPayRequest', {
2 "appId": "@jsApiUiPackage.AppId", //公眾號名稱,由商戶傳入
3 "timeStamp": "@jsApiUiPackage.Timestamp", //時間戳
4 "nonceStr": "@jsApiUiPackage.NonceStr", //隨機串
5 "package": "@Html.Raw(jsApiUiPackage.PrepayIdPackage)",//擴充套件包
6 "signType": "RSA", //微信V3簽名方式:RSA
7 "paySign": "@Html.Raw(jsApiUiPackage.Signature)" //微信簽名
8 }, function (res) {
9
10 //alert(JSON.stringify(res));
11
12 if (res.err_msg == "get_brand_wcpay_request:ok") {
13 if (confirm('支付成功!點選“確定”進入退款流程測試。')) {
14 location.href = '@Url.Action("Refund", "TenPayRealV3")';
15 }
16 //console.log(JSON.stringify(res));
17 }else{
18 alert(JSON.stringify(res));
19 }
20 // 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在使用者支付成功後返回ok,但並不保證它絕對可靠。
21 //因此微信團隊建議,當收到ok返回時,向商戶後臺詢問是否收到交易成功的通知,若收到通知,前端展示交易成功的介面;若此時未收到通知,商戶後臺主動呼叫查詢訂單介面,查詢訂單的當前狀態,並反饋給前端展示相應的介面。
22 });

  上述程式碼在使用者點選支付按鈕的時候觸發,將自動進行一系列驗證,並喚起客戶端的微信支付介面(如輸入密碼或指紋)。

  其中:

  • 第 2-7 行:注入之前在 Controller 中配置的各類引數。注意:paySign 引數一定要加 Html.Raw(),否則可能因為加密字串被轉義而失敗!
  • 第 12 行:判斷是否支付成功,並進行下一步操作。注意:此處的成功不一定是微信支付真的成功了,因為此資訊有被篡改的可能性,因此正式環境一定要以 PayNotifyUrl 中的驗證結果為準!

  回撥驗證 PayNotifyUrl:

  微信客戶端收到的支付成功資訊始終具有被篡改的可能性,因此,千萬不要:

  1. 因為客戶端的 JS 收到了看似正確的資訊,就觸發伺服器端完成支付的指令(如一條Ajax請求);
  2. 即使觸發伺服器端的下一步指令,也不要在該條指令中進行訂單“已支付”狀態的修改,訂單狀態修改,必須是在 PayNotifyUrl 中!

  根據之前 appsettings.json 以及 JsApi() 方法中的設定,最終的回撥地址為:https://sdk.weixin.senparc.com/TenpayRealV3/PayNotifyUrl,程式碼在 TenPayRealV3Controller 中的 PayNotifyUrl() 方法,此方法中演示了正確的驗證支付狀態的最佳實踐:

 1         /// <summary>
2 /// JS-SDK支付回撥地址(在下單介面中設定的 notify_url)
3 /// </summary>
4 /// <returns></returns>
5 public async Task<IActionResult> PayNotifyUrl()
6 {
7 try
8 {
9 //獲取微信伺服器非同步傳送的支付通知資訊
10 var resHandler = new TenPayNotifyHandler(HttpContext);
11 var orderReturnJson = await resHandler.AesGcmDecryptGetObjectAsync<OrderReturnJson>();
12
13 //記錄日誌
14 Senparc.Weixin.WeixinTrace.SendCustomLog("PayNotifyUrl 接收到訊息", orderReturnJson.ToJson(true));
15
16 //演示記錄 transaction_id,實際開發中需要記錄到資料庫,以便退款和後續跟蹤
17 TradeNumberToTransactionId[orderReturnJson.out_trade_no] = orderReturnJson.transaction_id;
18
19 //獲取支付狀態
20 string trade_state = orderReturnJson.trade_state;
21
22 //驗證請求是否從微信發過來(安全)
23 NotifyReturnData returnData = new();
24
25 //驗證可靠的支付狀態
26 if (orderReturnJson.VerifySignSuccess == true && trade_state == "SUCCESS")
27 {
28 returnData.code = "SUCCESS";//正確的訂單處理
29 /* 提示:
30 * 1、直到這裡,才能認為交易真正成功了,可以進行資料庫操作,但是別忘了返回規定格式的訊息!
31 * 2、上述判斷已經具有比較高的安全性以外,還可以對訪問 IP 進行判斷進一步加強安全性。
32 * 3、下面演示的是傳送支付成功的模板訊息提示,非必須。
33 */
34
35 #region 傳送支付成功模板訊息提醒
36 //略...
37 #endregion
38 }
39 else
40 {
41 returnData.code = "FAILD";//錯誤的訂單處理
42 returnData.message = "驗證失敗";
43
44 //此處可以給使用者傳送支付失敗提示等
45 }
46
47 #region 記錄日誌(也可以記錄到資料庫審計日誌中)
48 //略...
49 #endregion
50
51 return Json(returnData);
52 }
53 catch (Exception ex)
54 {
55 WeixinTrace.WeixinExceptionLog(new WeixinException(ex.Message, ex));
56 throw;
57 }
58 }

  註釋已經比較詳細,這裡不再贅述,所有簽名校驗等安全驗證資訊已經全部封裝在介面中,開箱即用。官方要求的完整流程可參考文件:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml

開發第四步:Startup.cs 中配置啟動程式碼

  Senparc.Weixin.TenPayV3 基於 Senparc.Weixin SDK 整體基座,同時由 CO2NET、NeuChar 等基礎庫提供強大的底層能力支撐,同時我們需要使用一些程式碼,完成 appsettings.json 等資訊的自動注入,因此,需要在 Web 專案的 startup.cs 中新增一些程式碼,以下是關鍵程式碼的介紹(Sample 中為了演示所有的模組所以程式碼比較多,可以根據需要選用下方的程式碼):

  ConfigureServices() 方法:

 1         public void ConfigureServices(IServiceCollection services)
2 {
3 services.AddSession();//使用Session(實踐證明需要在配置 Mvc 之前)
4
5 var builder = services.AddControllersWithViews()
6 .AddNewtonsoftJson();// 支援 NewtonsoftJson
7
8 services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
9
10 services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
11 services.AddMemoryCache();//使用本地快取必須新增
12
13 services.AddSenparcWeixinServices(Configuration);//Senparc.Weixin 註冊(必須)
14 }

  上述程式碼完成了 Web 專案的一系列註冊,其中:

  • 第 3 行:為了讓 Demo 不依賴資料庫,我們使用了 Session 進行個人臨時資料的儲存,實際開發專案中不一定需要,可根據需要新增。
  • 第 5-6 行:註冊 MVC 和 JSON 相關能力,根據需要新增。
  • 第 8 行:提供 Cookie 支援,根據需要新增。
  • 第 10 行:為自動注入 HttpContext 添加註冊,根據需要新增。
  • 第 11 行:註冊本地快取,這一行為必須,因為 SDK 執行過程總需要使用到本地快取。
  • 第 13 行:對 Senparc.Weixin SDK 進行註冊,必須。

  可以看到,最小化支援 Senpar.Weixin.TenPayV3,此處實際上只需要最少新增 2 行程式碼。

  Configure() 方法:

 1       public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
2 IOptions<SenparcSetting> senparcSetting, IOptions<SenparcWeixinSetting> senparcWeixinSetting)
3 {
4 app.UseHttpsRedirection();
5 app.UseStaticFiles();
6 app.UseRouting();
7
8 var registerService = app
9 //使用 Senparc.CO2NET 引擎
10 .UseSenparcGlobal(env, senparcSetting.Value, g => { })
11 //使用 Senparc.Weixin SDK
12 .UseSenparcWeixin(senparcWeixinSetting.Value, weixinRegister =>
13 {
14 //註冊最新的 TenPay V3
15 weixinRegister.RegisterTenpayRealV3(senparcWeixinSetting.Value, "【盛派網路小助手】公眾號-RealV3");
16 });
17 }

  上述程式碼中:

  • 第 4-6 行:常規方法。
  • 第 10 行:啟動 Senparc.CO2NET 引擎,提供一系列基礎能力(如快取、日誌、佇列等)。
  • 第 12 行:啟動 Senparc.Weixin SDK,其中可以進行微信公眾號、小程式、企業微信、微信支付等不同模組的註冊。
  • 第 15 行:註冊微信支付V3的資訊,資料來源頭為 appsettings.json。注意:這一行註冊過程可以在使用微信支付功能前的任意地方執行,但建議在啟動時就完成註冊。除使用 appsetting.json 方式自動注入,也可以手動構造實體類,賦值並傳入。

上線演示

  上述 Sample 可以直接釋出,最新的程式碼我們已經發布到了到官方線上示例站點:https://sdk.weixin.senparc.com/,有兩種途徑可以進入上述 JsApi 頁面進行支付測試。

  方式一:關注公眾號:盛派網路小助手,點選選單

  進入選單【更多測試】>【微信支付V3】:

  選擇任意一個商品,如【產品1】,點選進入:

  點選【點選提交可體驗微信支付】按鈕,進入客戶端支付狀態:

  在客戶端完成支付(輸入密碼或指紋),即可出現支付完成的官方介面:

  點選【完成】按鈕,可以繼續體驗退款流程(開發相關功能介紹請看下一篇系列文章:《微信支付 V3 開發教程(二):退款》。

  返回公眾號內,可以看到已經通過 PayNotifyUrl 傳送過來的模板訊息(同時已經經過安全驗證):

  

  並可以在微信支付訊息中,看到官方的訊息推送:

  方式二:通過 https://sdk.weixin.senparc.com/ 頂部選單【工具箱】>【微信支付 V3 測試(PC端)】進入:

  進入後同樣是 ProductList 頁面:

  選擇一個商品進入,可以看到 PC 端提供了多種支付方式的演示,包括:H5 支付、Native 支付,以及掃一掃支付:

  提示:由於產品Id隨每次系統啟動變化,所以上述二維碼在您看到的時候已經失效,您可以重新從入口進入,獲得最新的二維碼。

  • 關於 H5 支付請關注後續文章:《微信支付 V3 開發教程(三):H5 支付》
  • 關於 Native 支付請關注後續文章:《微信支付 V3 開發教程(四):Native 支付》

  當前演示的 JsApi 支付,可在“掃一掃”支付方式中,使用微信掃碼進入,即可在微信端開啟上述“方法一”中介紹的產品列表,並體驗支付流程。

  

更多內容

  本文是《微信支付 V3 開發教程》的開篇,後續還將對包括退款、對賬訂單、H5 支付、Native 支付、微信分等更多的介面展開介紹,歡迎關注,感謝大家的支援!