前言
我在 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 支付、微信分等更多的接口展开介绍,欢迎关注,感谢大家的支持!