前言
我在 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:
微信客戶端收到的支付成功信息始終具有被篡改的可能性,因此,千萬不要:
- 因為客戶端的 JS 收到了看似正確的信息,就觸發服務器端完成支付的指令(如一條Ajax請求);
- 即使觸發服務器端的下一步指令,也不要在該條指令中進行訂單“已支付”狀態的修改,訂單狀態修改,必須是在 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 支付、微信分等更多的接口展開介紹,歡迎關注,感謝大家的支持!