微信支付 V3 开发教程(一):初识 Senparc.Weixin.TenPayV3

前言

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

 

posted on 2021-09-09 10:46  SZW  阅读(6260)  评论(11编辑  收藏  举报