zoukankan      html  css  js  c++  java
  • 微信支付 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 支付、微信分等更多的接口展开介绍,欢迎关注,感谢大家的支持!

    转载请注明出处和作者,谢谢!
    作者:JeffreySu / QQ:498977166
    博客:http://szw.cnblogs.com/

    Senparc官方教程《微信开发深度解析:微信公众号、小程序高效开发秘籍》,耗时2年精心打造的微信开发权威教程,点击这里,购买正版
    
微信开发深度解析:微信公众号、小程序高效开发秘籍

    Senparc 官方微信开发视频教程:《微信公众号+小程序快速开发》,点击这里点击观看
    Senparc 官方微信开发视频教程:《微信公众号+小程序快速开发》
  • 相关阅读:
    VSCode打开大文件插件
    https Java SSL Exception protocol_version
    Error creating bean with name 'tomcatEmbeddedServletContainerFactory ' (or a BeanPostProcessor involved) returned null
    在Java8的foreach()中不能break,如果需要continue时,可以使用return
    Spring cloud 超时及重试配置【ribbon及其它http client】
    祸害阿里云宕机 3 小时的 IO HANG 究竟是个什么鬼?!
    Convertion of grey code and binary 格雷码和二进制数之间的转换
    stringstream 使用方法
    [LeetCode] 91. Decode Ways 解码方法
    QString, string, int, char* 之间相互转换
  • 原文地址:https://www.cnblogs.com/szw/p/tenpayv3-01.html
Copyright © 2011-2022 走看看