Browse Source

初步实现微信支付APIv3 Get、Post 请求执行逻辑、响应验签、AES GCM解密、Json反序列化。
同时新增 获取平台证书列表API、Native下单API

Roc 5 years ago
parent
commit
2255af4db6
52 changed files with 1183 additions and 99 deletions
  1. 17 0
      samples/WebApplicationSample/Controllers/NotifyController.cs
  2. 58 1
      samples/WebApplicationSample/Controllers/WeChatPayController.cs
  3. 19 0
      samples/WebApplicationSample/Models/WeChatPayViewModel.cs
  4. 24 0
      samples/WebApplicationSample/Views/WeChatPay/GetCertificates.cshtml
  5. 12 0
      samples/WebApplicationSample/Views/WeChatPay/Index.cshtml
  6. 52 0
      samples/WebApplicationSample/Views/WeChatPay/QrCodePayV3.cshtml
  7. 4 0
      samples/WebApplicationSample/appsettings.Development.json
  8. 4 0
      samples/WebApplicationSample/appsettings.json
  9. 44 0
      src/Essensoft.AspNetCore.Payment.Security/AEAD_AES_256_GCM.cs
  10. 26 0
      src/Essensoft.AspNetCore.Payment.Security/SHA256WithRSA.cs
  11. 26 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Amount.cs
  12. 22 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Certificate.cs
  13. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/CouponInfo.cs
  14. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/CouponRefundInfo.cs
  15. 37 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Detail.cs
  16. 19 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/EncryptCertificate.cs
  17. 50 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/GoodsDetail.cs
  18. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/HBInfo.cs
  19. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/RefundInfo.cs
  20. 33 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/SceneInfo.cs
  21. 42 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/StoreInfo.cs
  22. 99 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/WeChatPayTransactionsNativeModel.cs
  23. 1 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Essensoft.AspNetCore.Payment.WeChatPay.csproj
  24. 16 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayClient.cs
  25. 21 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayV3GetRequest.cs
  26. 10 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayV3PostRequest.cs
  27. 13 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayNotifyParser.cs
  28. 0 9
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayParser.cs
  29. 7 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayResponseParser.cs
  30. 7 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayV3ResponseJsonParser.cs
  31. 6 2
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayNotifyXmlParser.cs
  32. 42 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayResponseXmlParser.cs
  33. 33 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayV3ResponseJsonParser.cs
  34. 16 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Request/WeChatPayCertificatesRequest.cs
  35. 29 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Request/WeChatPayTransactionsNativeRequest.cs
  36. 12 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Response/WeChatPayCertificatesResponse.cs
  37. 10 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/Response/WeChatPayTransactionsNativeResponse.cs
  38. 2 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/ServiceCollectionExtensions.cs
  39. 100 10
      src/Essensoft.AspNetCore.Payment.WeChatPay/Utility/HttpClientExtensions.cs
  40. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/Utility/WeChatPaySignature.cs
  41. 0 16
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayCertificateManager.cs
  42. 143 18
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayClient.cs
  43. 16 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayClientCertificateManager.cs
  44. 5 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayConsts.cs
  45. 5 5
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayHandlerBuilderFilter.cs
  46. 18 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayNotify.cs
  47. 1 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayNotifyClient.cs
  48. 1 19
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayObject.cs
  49. 28 11
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayOptions.cs
  50. 16 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayPlatformCertificateManager.cs
  51. 19 1
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayResponse.cs
  52. 13 0
      src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayV3Response.cs

+ 17 - 0
samples/WebApplicationSample/Controllers/NotifyController.cs

@@ -330,6 +330,23 @@ namespace WebApplicationSample.Controllers
                 return NoContent();
             }
         }
+
+        /// <summary>
+        /// 统一下单V3支付结果通知
+        /// </summary>
+        [Route("v3/transactions")]
+        [HttpPost]
+        public async Task<IActionResult> Transactions()
+        {
+            try
+            {
+                return NoContent();
+            }
+            catch
+            {
+                return NoContent();
+            }
+        }
     }
 
     #endregion

+ 58 - 1
samples/WebApplicationSample/Controllers/WeChatPayController.cs

@@ -1,6 +1,8 @@
-using System.Text.Json;
+using System.Collections.Generic;
+using System.Text.Json;
 using System.Threading.Tasks;
 using Essensoft.AspNetCore.Payment.WeChatPay;
+using Essensoft.AspNetCore.Payment.WeChatPay.Domain;
 using Essensoft.AspNetCore.Payment.WeChatPay.Request;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
@@ -580,5 +582,60 @@ namespace WebApplicationSample.Controllers
 
             return View();
         }
+
+        /// <summary>
+        /// 获取平台证书列表
+        /// </summary>
+        [HttpGet]
+        [HttpPost]
+        public async Task<IActionResult> GetCertificates()
+        {
+            if (Request.Method == "POST")
+            {
+                var request = new WeChatPayCertificatesRequest();
+                var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+                ViewData["response"] = response.Body;
+                return View();
+            }
+
+            return View();
+        }
+
+        /// <summary>
+        /// 扫码支付-Native下单API
+        /// </summary>
+        [HttpGet]
+        public IActionResult QrCodePayV3()
+        {
+            return View();
+        }
+
+        /// <summary>
+        /// 扫码支付-Native下单API
+        /// </summary>
+        /// <param name="viewModel"></param>
+        [HttpPost]
+        public async Task<IActionResult> QrCodePayV3(WeChatPayQrCodePayV3ViewModel viewModel)
+        {
+            var model = new WeChatPayTransactionsNativeModel
+            {
+                AppId = _optionsAccessor.Value.AppId,
+                MchId = _optionsAccessor.Value.MchId,
+                Amount = new Amount { Total = viewModel.Total, Currency = "CNY" },
+                Description = viewModel.Description,
+                NotifyUrl = viewModel.NotifyUrl,
+                OutTradeNo = viewModel.OutTradeNo,
+            };
+
+            var request = new WeChatPayTransactionsNativeRequest();
+            request.SetBizModel(model);
+
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+
+            // response.CodeUrl 给前端生成二维码
+            ViewData["qrcode"] = response.CodeUrl;
+            ViewData["response"] = response.Body;
+            return View();
+        }
     }
 }

+ 19 - 0
samples/WebApplicationSample/Models/WeChatPayViewModel.cs

@@ -332,4 +332,23 @@ namespace WebApplicationSample.Models
         [Display(Name = "partner_trade_no")]
         public string PartnerTradeNo { get; set; }
     }
+
+    public class WeChatPayQrCodePayV3ViewModel
+    {
+        [Required]
+        [Display(Name = "out_trade_no")]
+        public string OutTradeNo { get; set; }
+
+        [Required]
+        [Display(Name = "description")]
+        public string Description { get; set; }
+
+        [Required]
+        [Display(Name = "total")]
+        public int Total { get; set; }
+
+        [Required]
+        [Display(Name = "notify_url")]
+        public string NotifyUrl { get; set; }
+    }
 }

+ 24 - 0
samples/WebApplicationSample/Views/WeChatPay/GetCertificates.cshtml

@@ -0,0 +1,24 @@
+@{
+    ViewData["Title"] = "获取平台证书列表";
+}
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item"><a asp-controller="WeChatPay" asp-action="Index">微信支付</a></li>
+        <li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
+    </ol>
+</nav>
+<br />
+<div class="card">
+    <div class="card-body">
+        <form asp-controller="WeChatPay" asp-action="GetCertificates">
+            <button type="submit" class="btn btn-primary">提交请求</button>
+        </form>
+        <hr />
+        <form class="form-horizontal">
+            <div class="form-group">
+                <label>Response:</label>
+                <textarea class="form-control" rows="10">@ViewData["response"]</textarea>
+            </div>
+        </form>
+    </div>
+</div>

+ 12 - 0
samples/WebApplicationSample/Views/WeChatPay/Index.cshtml

@@ -126,5 +126,17 @@
             <td><a href="https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=24_7&index=4" target="_blank">https://fraud.mch.weixin.qq.com/risk/getpublickey</a></td>
             <td><a asp-controller="WeChatPay" asp-action="GetPublicKey">立即测试</a></td>
         </tr>
+        <tr>
+            <th scope="row">19</th>
+            <td>获取平台证书列表</td>
+            <td><a href="https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu" target="_blank">https://api.mch.weixin.qq.com/v3/certificates</a></td>
+            <td><a asp-controller="WeChatPay" asp-action="GetCertificates">立即测试</a></td>
+        </tr>
+        <tr>
+            <th scope="row">20</th>
+            <td>扫码支付-Native下单</td>
+            <td><a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transactions/chapter3_3.shtml" target="_blank">https://api.mch.weixin.qq.com/v3/pay/transactions/native</a></td>
+            <td><a asp-controller="WeChatPay" asp-action="QrCodePayV3">立即测试</a></td>
+        </tr>
     </tbody>
 </table>

+ 52 - 0
samples/WebApplicationSample/Views/WeChatPay/QrCodePayV3.cshtml

@@ -0,0 +1,52 @@
+@model WeChatPayQrCodePayV3ViewModel
+@{
+    ViewData["Title"] = "扫码支付-Native下单";
+}
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item"><a asp-controller="WeChatPay" asp-action="Index">微信支付</a></li>
+        <li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
+    </ol>
+</nav>
+<br />
+<div class="card">
+    <div class="card-body">
+        <form asp-controller="WeChatPay" asp-action="QrCodePayV3">
+            <div asp-validation-summary="All" class="text-danger"></div>
+            <div class="form-group">
+                <label asp-for="OutTradeNo"></label>
+                <input type="text" class="form-control" asp-for="OutTradeNo" value="@DateTime.Now.ToString("yyyyMMddHHmmssfff")">
+            </div>
+            <div class="form-group">
+                <label asp-for="Description"></label>
+                <input type="text" class="form-control" asp-for="Description" value="微信扫码支付测试">
+            </div>
+            <div class="form-group">
+                <label asp-for="Total"></label>
+                <input type="text" class="form-control" asp-for="Total" value="1">
+            </div>
+            <div class="form-group">
+                <label asp-for="NotifyUrl"></label>
+                <input type="text" class="form-control" asp-for="NotifyUrl" value="http://*/notify/wechatpay/v3/transactions">
+            </div>
+            <button type="submit" class="btn btn-primary">提交请求</button>
+        </form>
+        <hr />
+        <form class="form-horizontal">
+            <div class="form-group">
+                <label>QrCode:</label>
+                @if (!string.IsNullOrEmpty(ViewData["qrcode"] as string))
+                {
+                    <embed src="../Home/QrCode?size=168&data=@ViewData["qrcode"]" width="168" height="168" type="image/svg+xml" />
+                }
+            </div>
+            <div class="form-group">
+                <label>Response:</label>
+                <textarea class="form-control" rows="10">@ViewData["response"]</textarea>
+            </div>
+        </form>
+    </div>
+</div>
+@section Scripts {
+    @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
+}

+ 4 - 0
samples/WebApplicationSample/appsettings.Development.json

@@ -25,6 +25,10 @@
     // 为微信支付商户平台的API密钥,请注意不是APIv3密钥
     "Key": "",
 
+    // APIv3密钥
+    // 为微信支付商户平台的APIv3密钥,请注意不是API密钥
+    "V3Key": "",
+
     // API证书(.p12扩展名)
     // 为微信支付商户平台的API证书
     // 可为证书文件路径 / 证书文件的base64字符串

+ 4 - 0
samples/WebApplicationSample/appsettings.json

@@ -26,6 +26,10 @@
     // 为微信支付商户平台的API密钥,请注意不是APIv3密钥
     "Key": "",
 
+    // APIv3密钥
+    // 为微信支付商户平台的APIv3密钥,请注意不是API密钥
+    "V3Key": "",
+
     // API证书(.p12扩展名)
     // 为微信支付商户平台的API证书
     // 可为证书文件路径 / 证书文件的base64字符串

+ 44 - 0
src/Essensoft.AspNetCore.Payment.Security/AEAD_AES_256_GCM.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Essensoft.AspNetCore.Payment.Security
+{
+    public class AEAD_AES_256_GCM
+    {
+        public static string Decrypt(string nonce, string ciphertext, string associatedData, string key)
+        {
+            if (string.IsNullOrEmpty(nonce))
+            {
+                throw new ArgumentNullException(nameof(nonce));
+            }
+
+            if (string.IsNullOrEmpty(ciphertext))
+            {
+                throw new ArgumentNullException(nameof(ciphertext));
+            }
+
+            if (string.IsNullOrEmpty(associatedData))
+            {
+                throw new ArgumentNullException(nameof(associatedData));
+            }
+
+            if (string.IsNullOrEmpty(key))
+            {
+                throw new ArgumentNullException(nameof(key));
+            }
+
+            using (var aesGcm = new AesGcm(Encoding.UTF8.GetBytes(key)))
+            {
+                var nonceBytes = Encoding.UTF8.GetBytes(nonce);
+                var ciphertextWithTagBytes = Convert.FromBase64String(ciphertext); // ciphertext 实际包含了 tag,即尾部16字节
+                var ciphertextBytes = ciphertextWithTagBytes[0..^16]; // 排除尾部16字节
+                var tagBytes = ciphertextWithTagBytes[^16..]; // 获取尾部16字节
+                var paintextBytes = new byte[ciphertextBytes.Length];
+                var associatedDataBytes = Encoding.UTF8.GetBytes(associatedData);
+                aesGcm.Decrypt(nonceBytes, ciphertextBytes, tagBytes, paintextBytes, associatedDataBytes);
+                return Encoding.UTF8.GetString(paintextBytes);
+            }
+        }
+    }
+}

+ 26 - 0
src/Essensoft.AspNetCore.Payment.Security/SHA256WithRSA.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Security.Cryptography;
+using System.Text;
 
 namespace Essensoft.AspNetCore.Payment.Security
 {
@@ -57,5 +58,30 @@ namespace Essensoft.AspNetCore.Payment.Security
                 return rsa.VerifyData(InternalEncoding.GetEncoding(charset).GetBytes(data), Convert.FromBase64String(sign), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
             }
         }
+
+        public static string Sign(this RSA rsa, string data)
+        {
+            if (string.IsNullOrEmpty(data))
+            {
+                throw new ArgumentNullException(nameof(data));
+            }
+
+            return Convert.ToBase64String(rsa.SignData(Encoding.UTF8.GetBytes(data), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
+        }
+
+        public static bool Verify(this RSA rsa, string data, string sign)
+        {
+            if (string.IsNullOrEmpty(data))
+            {
+                throw new ArgumentNullException(nameof(data));
+            }
+
+            if (string.IsNullOrEmpty(sign))
+            {
+                throw new ArgumentNullException(nameof(sign));
+            }
+
+            return rsa.VerifyData(Encoding.UTF8.GetBytes(data), Convert.FromBase64String(sign), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+        }
     }
 }

+ 26 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Amount.cs

@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// 订单金额
+    /// </summary>    
+    public class Amount : WeChatPayObject
+    {
+        /// <summary>
+        /// 订单金额
+        /// 订单总金额,单位为分。
+        /// 示例值:100
+        /// </summary>
+        [JsonPropertyName("total")]
+        public int Total { get; set; }
+
+        /// <summary>
+        /// 货币类型	
+        /// CNY:人民币,境内商户号仅支持人民币。
+        /// 示例值:CNY
+        /// </summary>
+        [JsonPropertyName("currency")]
+        public string Currency { get; set; }
+    }
+}

+ 22 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Certificate.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    public class Certificate : WeChatPayObject
+    {
+        [JsonPropertyName("serial_no")]
+        public string SerialNo { get; set; }
+
+        [JsonPropertyName("effective_time")]
+        public string EffectiveTime { get; set; }
+
+        [JsonPropertyName("expire_time")]
+        public string ExpireTime { get; set; }
+
+        [JsonPropertyName("encrypt_certificate")]
+        public EncryptCertificate EncryptCertificate { get; set; }
+    }
+}

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/CouponInfo.cs

@@ -5,7 +5,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
     /// <summary>
     /// 代金券或立减优惠信息
     /// </summary>
-    public class CouponInfo
+    public class CouponInfo : WeChatPayObject
     {
         /// <summary>
         /// 代金券类型

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/CouponRefundInfo.cs

@@ -5,7 +5,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
     /// <summary>
     /// 退款代金券信息
     /// </summary>
-    public class CouponRefundInfo
+    public class CouponRefundInfo : WeChatPayObject
     {
         /// <summary>
         /// 代金券类型

+ 37 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/Detail.cs

@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// 优惠功能
+    /// </summary>    
+    public class Detail : WeChatPayObject
+    {
+        /// <summary>
+        /// 订单原价
+        /// 1、商户侧一张小票订单可能被分多次支付,订单原价用于记录整张小票的交易金额。
+        /// 2、当订单原价与支付金额不相等,则不享受优惠。
+        /// 3、该字段主要用于防止同一张小票分多次支付,以享受多次优惠的情况,正常支付订单不必上传此参数。
+        /// 示例值:608800
+        /// </summary>
+        [JsonPropertyName("cost_price")]
+        public int CostPrice { get; set; }
+
+        /// <summary>
+        /// 商品小票ID
+        /// 商家小票ID
+        /// 示例值:微信123
+        /// </summary>
+        [JsonPropertyName("invoice_id")]
+        public string InvoiceId { get; set; }
+
+        /// <summary>
+        /// 单品列表
+        /// 单品列表信息
+        /// 条目个数限制:【1,undefined】
+        /// </summary>
+        [JsonPropertyName("goods_detail")]
+        public List<GoodsDetail> GoodsDetail { get; set; }
+    }
+}

+ 19 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/EncryptCertificate.cs

@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    public class EncryptCertificate : WeChatPayObject
+    {
+        [JsonPropertyName("algorithm")]
+        public string Algorithm { get; set; }
+
+        [JsonPropertyName("nonce")]
+        public string Nonce { get; set; }
+
+        [JsonPropertyName("associated_data")]
+        public string AssociatedData { get; set; }
+
+        [JsonPropertyName("ciphertext")]
+        public string Ciphertext { get; set; }
+    }
+}

+ 50 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/GoodsDetail.cs

@@ -0,0 +1,50 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// 单品列表
+    /// </summary>   
+    public class GoodsDetail : WeChatPayObject
+    {
+        /// <summary>
+        /// 商户侧商品编码
+        /// 由半角的大小写字母、数字、中划线、下划线中的一种或几种组成。
+        /// 示例值:商品编码
+        /// </summary>
+        [JsonPropertyName("merchant_goods_id")]
+        public string MerchantGoodsId { get; set; }
+
+        /// <summary>
+        /// 微信侧商品编码
+        /// 微信支付定义的统一商品编号(没有可不传)
+        /// 示例值:1001
+        /// </summary>
+        [JsonPropertyName("wechatpay_goods_id")]
+        public string WeChatPayGoodsId { get; set; }
+
+        /// <summary>
+        /// 商品名称
+        /// 商品的实际名称
+        /// 示例值:iPhoneX 256G
+        /// </summary>
+        [JsonPropertyName("goods_name")]
+        public string GoodsName { get; set; }
+
+        /// <summary>
+        /// 商品数量
+        /// 用户购买的数量
+        /// 示例值:1
+        /// </summary>
+        [JsonPropertyName("quantity")]
+        public int Quantity { get; set; }
+
+        /// <summary>
+        /// 商品单价
+        /// 商品单价,单位为分
+        /// 示例值:828800
+        /// </summary>
+        [JsonPropertyName("unit_price")]
+        public int UnitPrice { get; set; }
+    }
+}

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/HBInfo.cs

@@ -5,7 +5,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
     /// <summary>
     /// 红包信息
     /// </summary>
-    public class HbInfo
+    public class HbInfo : WeChatPayObject
     {
         /// <summary>
         /// 领取红包的OpenId

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/RefundInfo.cs

@@ -6,7 +6,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
     /// <summary>
     /// 退款信息
     /// </summary>
-    public class RefundInfo
+    public class RefundInfo : WeChatPayObject
     {
         /// <summary>
         /// 商户退款单号

+ 33 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/SceneInfo.cs

@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// 场景信息
+    /// </summary>       
+    public class SceneInfo : WeChatPayObject
+    {
+        /// <summary>
+        /// 用户终端IP
+        /// 调用微信支付API的机器IP,支持IPv4和IPv4两种格式的IP地址。
+        /// 示例值:14.23.150.211
+        /// </summary>
+        [JsonPropertyName("payer_client_ip")]
+        public string PayerClientIp { get; set; }
+
+        /// <summary>
+        /// 商户端设备号
+        /// 商户端设备号(门店号或收银设备ID)。
+        /// 示例值:013467007045764
+        /// </summary>
+        [JsonPropertyName("device_id")]
+        public string DeviceId { get; set; }
+
+        /// <summary>
+        /// 商户门店信息
+        /// 商户门店信息
+        /// </summary>
+        [JsonPropertyName("store_info")]
+        public StoreInfo StoreInfo { get; set; }
+    }
+}

+ 42 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/StoreInfo.cs

@@ -0,0 +1,42 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// 商户门店信息
+    /// </summary>  
+    public class StoreInfo : WeChatPayObject
+    {
+        /// <summary>
+        /// 门店编号
+        /// 商户侧门店编号
+        /// 示例值:0001
+        /// </summary>
+        [JsonPropertyName("id")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 门店名称
+        /// 商户侧门店名称
+        /// 示例值:腾讯大厦分店
+        /// </summary>
+        [JsonPropertyName("name")]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 地区编码
+        /// 地区编码,详细请见省市区编号对照表。
+        /// 示例值:440305
+        /// </summary>
+        [JsonPropertyName("area_code")]
+        public string AreaCode { get; set; }
+
+        /// <summary>
+        /// 详细地址
+        /// 详细的商户门店地址
+        /// 示例值:广东省深圳市南山区科技中一道10000号
+        /// </summary>
+        [JsonPropertyName("address")]
+        public string Address { get; set; }
+    }
+}

+ 99 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Domain/WeChatPayTransactionsNativeModel.cs

@@ -0,0 +1,99 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Domain
+{
+    /// <summary>
+    /// Native下单API-请求参数
+    /// 最新更新时间:2020.05.26
+    /// https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transactions/chapter3_3.shtml
+    /// </summary>
+    public class WeChatPayTransactionsNativeModel : WeChatPayObject
+    {
+        /// <summary>
+        /// 公众号ID
+        /// 直连商户申请的公众号或移动应用appid。
+        /// 示例值:wxd678efh567hg6787
+        /// </summary>
+        [JsonPropertyName("appid")]
+        public string AppId { get; set; }
+
+        /// <summary>
+        /// 直连商户号
+        /// 直连商户的商户号,由微信支付生成并下发。
+        /// 示例值:1230000109
+        /// </summary>
+        [JsonPropertyName("mchid")]
+        public string MchId { get; set; }
+
+        /// <summary>
+        /// 商品描述
+        /// 商品描述
+        /// 示例值:Image形象店-深圳腾大-QQ公仔
+        /// </summary>
+        [JsonPropertyName("description")]
+        public string Description { get; set; }
+
+        /// <summary>
+        /// 商户订单号
+        /// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一,详见【商户订单号】。
+        /// 特殊规则:最小字符长度为6
+        /// 示例值:1217752501201407033233368018
+        /// </summary>
+        [JsonPropertyName("out_trade_no")]
+        public string OutTradeNo { get; set; }
+
+        /// <summary>
+        /// 交易结束时间
+        /// 订单失效时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
+        /// 示例值:2018-06-08T10:34:56+08:00
+        /// </summary>
+        [JsonPropertyName("time_expire")]
+        public string TimeExpire { get; set; }
+
+        /// <summary>
+        /// 附加数据
+        /// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+        /// 示例值:自定义数据
+        /// </summary>
+        [JsonPropertyName("attach")]
+        public string Attach { get; set; }
+
+        /// <summary>
+        /// 通知地址
+        /// 通知URL必须为直接可访问的URL,不允许携带查询串。
+        /// 格式:URL
+        /// 示例值:https://www.weixin.qq.com/wxpay/pay.php
+        /// </summary>
+        [JsonPropertyName("notify_url")]
+        public string NotifyUrl { get; set; }
+
+        /// <summary>
+        /// 订单优惠标记
+        /// 订单优惠标记
+        /// 示例值:WXG
+        /// </summary>
+        [JsonPropertyName("goods_tag")]
+        public string GoodsTag { get; set; }
+
+        /// <summary>
+        /// 订单金额
+        /// 订单金额信息
+        /// </summary>
+        [JsonPropertyName("amount")]
+        public Amount Amount { get; set; }
+
+        /// <summary>
+        /// 优惠功能
+        /// 优惠功能
+        /// </summary>
+        [JsonPropertyName("detail")]
+        public Detail Detail { get; set; }
+
+        /// <summary>
+        /// 场景信息
+        /// 支付场景描述
+        /// </summary>
+        [JsonPropertyName("scene_info")]
+        public SceneInfo SceneInfo { get; set; }
+    }
+}

+ 1 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Essensoft.AspNetCore.Payment.WeChatPay.csproj

@@ -7,6 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
+    <PackageReference Include="System.Text.Json" Version="4.7.2" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.5" />
   </ItemGroup>
 

+ 16 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayClient.cs

@@ -34,5 +34,21 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
         /// <param name="request">具体的WeChatPay Sdk请求</param>
         /// <param name="options">配置选项</param>
         Task<WeChatPayDictionary> ExecuteAsync(IWeChatPaySdkRequest request, WeChatPayOptions options);
+
+        /// <summary>
+        /// 执行 WeChatPay APIv3 Get请求。
+        /// </summary>
+        /// <param name="request">具体的WeChatPay APIv3 Get请求</param>
+        /// <param name="options">配置选项</param>
+        /// <returns>领域对象</returns>
+        Task<T> ExecuteAsync<T>(IWeChatPayV3GetRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response;
+
+        /// <summary>
+        /// 执行 WeChatPay APIv3 Post请求。
+        /// </summary>
+        /// <param name="request">具体的WeChatPay APIv3 Post请求</param>
+        /// <param name="options">配置选项</param>
+        /// <returns>领域对象</returns>
+        Task<T> ExecuteAsync<T>(IWeChatPayV3PostRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response;
     }
 }

+ 21 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayV3GetRequest.cs

@@ -0,0 +1,21 @@
+namespace Essensoft.AspNetCore.Payment.WeChatPay
+{
+    public interface IWeChatPayV3PostRequest<T> where T : WeChatPayV3Response
+    {
+        /// <summary>
+        /// 请求URL
+        /// </summary>
+        string GetRequestUrl();
+
+        /// <summary>
+        /// 获取BizModel
+        /// </summary>
+        WeChatPayObject GetBizModel();
+
+        /// <summary>
+        /// 设置BizModel
+        /// </summary>
+        /// <param name="bizModel"></param>
+        void SetBizModel(WeChatPayObject bizModel);
+    }
+}

+ 10 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/IWeChatPayV3PostRequest.cs

@@ -0,0 +1,10 @@
+namespace Essensoft.AspNetCore.Payment.WeChatPay
+{
+    public interface IWeChatPayV3GetRequest<T> where T : WeChatPayV3Response
+    {
+        /// <summary>
+        /// 请求URL
+        /// </summary>
+        string GetRequestUrl();
+    }
+}

+ 13 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayNotifyParser.cs

@@ -0,0 +1,13 @@
+#if NETCOREAPP3_1
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
+{
+    public interface IWeChatPayNotifyParser<T> where T : WeChatPayNotify
+    {
+        T Parse(string body);
+
+        T Parse(string body, string data);
+    }
+}
+
+#endif

+ 0 - 9
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayParser.cs

@@ -1,9 +0,0 @@
-namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
-{
-    public interface IWeChatPayParser<T> where T : WeChatPayObject
-    {
-        T Parse(string body);
-
-        T Parse(string body, string data);
-    }
-}

+ 7 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayResponseParser.cs

@@ -0,0 +1,7 @@
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
+{
+    public interface IWeChatPayResponseParser<T> where T : WeChatPayResponse
+    {
+        T Parse(string body);
+    }
+}

+ 7 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/IWeChatPayV3ResponseJsonParser.cs

@@ -0,0 +1,7 @@
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
+{
+    public interface IWeChatPayV3ResponseJsonParser<T> where T : WeChatPayV3Response
+    {
+        T Parse(string body);
+    }
+}

+ 6 - 2
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayXmlParser.cs → src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayNotifyXmlParser.cs

@@ -1,4 +1,6 @@
-using System;
+#if NETCOREAPP3_1
+
+using System;
 using System.IO;
 using System.Text;
 using System.Xml.Linq;
@@ -6,7 +8,7 @@ using System.Xml.Serialization;
 
 namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
 {
-    public class WeChatPayXmlParser<T> : IWeChatPayParser<T> where T : WeChatPayObject
+    public class WeChatPayNotifyXmlParser<T> : IWeChatPayNotifyParser<T> where T : WeChatPayNotify
     {
         public T Parse(string body)
         {
@@ -85,3 +87,5 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
         }
     }
 }
+
+#endif

+ 42 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayResponseXmlParser.cs

@@ -0,0 +1,42 @@
+using System;
+using System.IO;
+using System.Xml.Linq;
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
+{
+    public class WeChatPayResponseXmlParser<T> : IWeChatPayResponseParser<T> where T : WeChatPayResponse
+    {
+        public T Parse(string body)
+        {
+            T result = null;
+            var parameters = new WeChatPayDictionary();
+
+            try
+            {
+                var bodyDoc = XDocument.Parse(body).Element("xml");
+                foreach (var element in bodyDoc.Elements())
+                {
+                    parameters.Add(element.Name.LocalName, element.Value);
+                }
+
+                using (var sr = new StringReader(body))
+                {
+                    var xmldes = new XmlSerializer(typeof(T));
+                    result = (T)xmldes.Deserialize(sr);
+                }
+            }
+            catch { }
+
+            if (result == null)
+            {
+                result = Activator.CreateInstance<T>();
+            }
+
+            result.Body = body;
+            result.Parameters = parameters;
+            result.Execute();
+            return result;
+        }
+    }
+}

+ 33 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Parser/WeChatPayV3ResponseJsonParser.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Parser
+{
+    public class WeChatPayV3ResponseJsonParser<T> : IWeChatPayV3ResponseJsonParser<T> where T : WeChatPayV3Response
+    {
+        private static readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { IgnoreNullValues = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
+
+        public T Parse(string body)
+        {
+            T result = null;
+
+            try
+            {
+                if (body.StartsWith("{") && body.EndsWith("}"))
+                {
+                    result = JsonSerializer.Deserialize<T>(body, jsonSerializerOptions);
+                }
+            }
+            catch { }
+
+            if (result == null)
+            {
+                result = Activator.CreateInstance<T>();
+            }
+
+            result.Body = body;
+            return result;
+        }
+    }
+}

+ 16 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Request/WeChatPayCertificatesRequest.cs

@@ -0,0 +1,16 @@
+using Essensoft.AspNetCore.Payment.WeChatPay.Response;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Request
+{
+    /// <summary>
+    /// 获取平台证书列表
+    /// https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu
+    /// </summary>
+    public class WeChatPayCertificatesRequest : IWeChatPayV3GetRequest<WeChatPayCertificatesResponse>
+    {
+        public string GetRequestUrl()
+        {
+            return "https://api.mch.weixin.qq.com/v3/certificates";
+        }
+    }
+}

+ 29 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Request/WeChatPayTransactionsNativeRequest.cs

@@ -0,0 +1,29 @@
+using Essensoft.AspNetCore.Payment.WeChatPay.Response;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Request
+{
+    /// <summary>
+    /// Native下单API
+    /// 最新更新时间:2020.05.26
+    /// https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/transactions/chapter3_3.shtml
+    /// </summary>
+    public class WeChatPayTransactionsNativeRequest : IWeChatPayV3PostRequest<WeChatPayTransactionsNativeResponse>
+    {
+        private WeChatPayObject bizModel;
+
+        public string GetRequestUrl()
+        {
+            return "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
+        }
+
+        public WeChatPayObject GetBizModel()
+        {
+            return bizModel;
+        }
+
+        public void SetBizModel(WeChatPayObject bizModel)
+        {
+            this.bizModel = bizModel;
+        }
+    }
+}

+ 12 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Response/WeChatPayCertificatesResponse.cs

@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Essensoft.AspNetCore.Payment.WeChatPay.Domain;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Response
+{
+    public class WeChatPayCertificatesResponse : WeChatPayV3Response
+    {
+        [JsonPropertyName("data")]
+        public List<Certificate> Certificates { get; set; }
+    }
+}

+ 10 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/Response/WeChatPayTransactionsNativeResponse.cs

@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay.Response
+{
+    public class WeChatPayTransactionsNativeResponse : WeChatPayV3Response
+    {
+        [JsonPropertyName("code_url")]
+        public string CodeUrl { get; set; }
+    }
+}

+ 2 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/ServiceCollectionExtensions.cs

@@ -12,7 +12,8 @@ namespace Microsoft.Extensions.DependencyInjection
 
             services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, WeChatPayHandlerBuilderFilter>());
 
-            services.AddSingleton<WeChatPayCertificateManager>();
+            services.AddSingleton<WeChatPayClientCertificateManager>();
+            services.AddSingleton<WeChatPayPlatformCertificateManager>();
             services.AddSingleton<IWeChatPayClient, WeChatPayClient>();
 
 #if NETCOREAPP3_1

+ 100 - 10
src/Essensoft.AspNetCore.Payment.WeChatPay/Utility/HttpClientExtensions.cs

@@ -1,27 +1,117 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Net.Http;
+using System.Net.Http.Headers;
 using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
 using System.Threading.Tasks;
+using Essensoft.AspNetCore.Payment.Security;
 
 namespace Essensoft.AspNetCore.Payment.WeChatPay.Utility
 {
     public static class HttpClientExtensions
     {
-        /// <summary>
-        /// 执行HTTP POST请求。
-        /// </summary>
-        /// <param name="client">HttpClient</param>
-        /// <param name="url">请求地址</param>
-        /// <param name="textParams">请求参数</param>
-        /// <returns>HTTP响应内容</returns>
-        public static async Task<string> PostAsync(this HttpClient client, string url, IDictionary<string, string> textParams)
+        private static readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { IgnoreNullValues = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
+
+        public static async Task<string> PostAsync<T>(this HttpClient client, IWeChatPayRequest<T> request, IDictionary<string, string> textParams) where T : WeChatPayResponse
+        {
+            var url = request.GetRequestUrl();
+            var content = WeChatPayUtility.BuildContent(textParams);
+            using (var reqContent = new StringContent(content, Encoding.UTF8, "application/xml"))
+            using (var resp = await client.PostAsync(url, reqContent))
+            using (var respContent = resp.Content)
+            {
+                return await respContent.ReadAsStringAsync();
+            }
+        }
+
+        public static async Task<string> PostAsync<T>(this HttpClient client, IWeChatPayCertRequest<T> request, IDictionary<string, string> textParams) where T : WeChatPayResponse
         {
-            using (var reqContent = new StringContent(WeChatPayUtility.BuildContent(textParams), Encoding.UTF8, "application/xml"))
+            var url = request.GetRequestUrl();
+            var content = WeChatPayUtility.BuildContent(textParams);
+            using (var reqContent = new StringContent(content, Encoding.UTF8, "application/xml"))
             using (var resp = await client.PostAsync(url, reqContent))
             using (var respContent = resp.Content)
             {
                 return await respContent.ReadAsStringAsync();
             }
         }
+
+        public static async Task<(string serial, string timestamp, string nonce, string signature, string body)> GetAsync<T>(this HttpClient client, IWeChatPayV3GetRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response
+        {
+            var url = request.GetRequestUrl();
+            var authorization = BuildAuthorizationString(url, "GET", null, options);
+
+            client.DefaultRequestHeaders.Add(WeChatPayConsts.Wechatpay_Serial, options.CertificateSerialNo);
+            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("WECHATPAY2-SHA256-RSA2048", authorization);
+            client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Unknown")));
+            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+            using (var resp = await client.GetAsync(url))
+            using (var respContent = resp.Content)
+            {
+                var serial = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Serial).First();
+                var timestamp = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Timestamp).First();
+                var nonce = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Nonce).First();
+                var signature = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Signature).First();
+                var body = await respContent.ReadAsStringAsync();
+
+                return (serial, timestamp, nonce, signature, body);
+            }
+        }
+
+        public static async Task<(string serial, string timestamp, string nonce, string signature, string body)> PostAsync<T>(this HttpClient client, IWeChatPayV3PostRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response
+        {
+            var url = request.GetRequestUrl();
+            var content = SerializeBizModel(request);
+            var authorization = BuildAuthorizationString(url, "POST", content, options);
+            client.DefaultRequestHeaders.Add(WeChatPayConsts.Wechatpay_Serial, options.CertificateSerialNo);
+            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("WECHATPAY2-SHA256-RSA2048", authorization);
+            client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Unknown")));
+            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+            using (var reqContent = new StringContent(content, Encoding.UTF8, "application/json"))
+            using (var resp = await client.PostAsync(url, reqContent))
+            using (var respContent = resp.Content)
+            {
+                var serial = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Serial).First();
+                var timestamp = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Timestamp).First();
+                var nonce = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Nonce).First();
+                var signature = resp.Headers.GetValues(WeChatPayConsts.Wechatpay_Signature).First();
+                var body = await respContent.ReadAsStringAsync();
+
+                return (serial, timestamp, nonce, signature, body);
+            }
+        }
+
+        private static string SerializeBizModel<T>(IWeChatPayV3PostRequest<T> request) where T : WeChatPayV3Response
+        {
+            var bizModel = request.GetBizModel();
+            if (bizModel != null)
+            {
+                return JsonSerializer.Serialize(bizModel, bizModel.GetType(), jsonSerializerOptions);
+            }
+
+            throw new WeChatPayException("BizModel is null!");
+        }
+
+        private static string BuildAuthorizationString(string url, string method, string content, WeChatPayOptions options)
+        {
+            var body = string.Empty;
+            if (method == "POST" || method == "PUT" || method == "PATCH")
+            {
+                body = content;
+            }
+
+            var uri = new Uri(url).PathAndQuery;
+            var timestamp = WeChatPayUtility.GetTimeStamp();
+            var nonce = WeChatPayUtility.GenerateNonceStr();
+            var message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";
+            var signature = options.CertificateRSAPrivateKey.Sign(message);
+
+            return $"mchid=\"{options.MchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{options.CertificateSerialNo}\",signature=\"{signature}\"";
+        }
     }
 }

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/Utility/WeChatPaySignature.cs

@@ -11,7 +11,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay.Utility
             var sb = new StringBuilder();
             foreach (var iter in dictionary)
             {
-                if (!string.IsNullOrEmpty(iter.Value) && iter.Key != "sign")
+                if (!string.IsNullOrEmpty(iter.Value) && iter.Key != WeChatPayConsts.sign)
                 {
                     sb.Append(iter.Key).Append('=').Append(iter.Value).Append("&");
                 }

+ 0 - 16
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayCertificateManager.cs

@@ -1,16 +0,0 @@
-using System.Collections.Concurrent;
-using System.Security.Cryptography.X509Certificates;
-
-namespace Essensoft.AspNetCore.Payment.WeChatPay
-{
-    public class WeChatPayCertificateManager
-    {
-        private readonly ConcurrentDictionary<string, X509Certificate2> _certificateDictionary = new ConcurrentDictionary<string, X509Certificate2>();
-
-        public bool ContainsKey(string hash) => _certificateDictionary.ContainsKey(hash);
-
-        public bool TryAdd(string hash, X509Certificate2 certificate) => _certificateDictionary.TryAdd(hash, certificate);
-
-        public bool TryGetValue(string hash, out X509Certificate2 certificate) => _certificateDictionary.TryGetValue(hash, out certificate);
-    }
-}

+ 143 - 18
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayClient.cs

@@ -1,9 +1,12 @@
 using System;
-using System.IO;
 using System.Net.Http;
 using System.Security.Cryptography.X509Certificates;
+using System.Text;
 using System.Threading.Tasks;
+using Essensoft.AspNetCore.Payment.Security;
 using Essensoft.AspNetCore.Payment.WeChatPay.Parser;
+using Essensoft.AspNetCore.Payment.WeChatPay.Request;
+using Essensoft.AspNetCore.Payment.WeChatPay.Response;
 using Essensoft.AspNetCore.Payment.WeChatPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.WeChatPay
@@ -13,14 +16,16 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
         public const string Prefix = nameof(WeChatPayClient) + ".";
 
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly WeChatPayCertificateManager _certificateManager;
+        private readonly WeChatPayClientCertificateManager _clientCertificateManager;
+        private readonly WeChatPayPlatformCertificateManager _platformCertificateManager;
 
         #region WeChatPayClient Constructors
 
-        public WeChatPayClient(IHttpClientFactory httpClientFactory, WeChatPayCertificateManager certificateManager)
+        public WeChatPayClient(IHttpClientFactory httpClientFactory, WeChatPayClientCertificateManager clientCertificateManager, WeChatPayPlatformCertificateManager platformCertificateManager)
         {
             _httpClientFactory = httpClientFactory;
-            _certificateManager = certificateManager;
+            _clientCertificateManager = clientCertificateManager;
+            _platformCertificateManager = platformCertificateManager;
         }
 
         #endregion
@@ -55,8 +60,8 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
             request.PrimaryHandler(options, signType, sortedTxtParams);
 
             var client = _httpClientFactory.CreateClient(nameof(WeChatPayClient));
-            var body = await client.PostAsync(request.GetRequestUrl(), sortedTxtParams);
-            var parser = new WeChatPayXmlParser<T>();
+            var body = await client.PostAsync(request, sortedTxtParams);
+            var parser = new WeChatPayResponseXmlParser<T>();
             var response = parser.Parse(body);
 
             if (request.GetNeedCheckSign())
@@ -149,18 +154,14 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
 
             request.PrimaryHandler(options, signType, sortedTxtParams);
 
-            if (!_certificateManager.ContainsKey(options.CertificateHash))
+            if (!_clientCertificateManager.ContainsKey(options.CertificateSerialNo))
             {
-                var certificate = File.Exists(options.Certificate) ?
-                    new X509Certificate2(options.Certificate, options.CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet) :
-                    new X509Certificate2(Convert.FromBase64String(options.Certificate), options.CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet);
-
-                _certificateManager.TryAdd(options.CertificateHash, certificate);
+                _clientCertificateManager.TryAdd(options.CertificateSerialNo, options.X509Certificate2);
             }
 
-            var client = _httpClientFactory.CreateClient(Prefix + options.CertificateHash);
-            var body = await client.PostAsync(request.GetRequestUrl(), sortedTxtParams);
-            var parser = new WeChatPayXmlParser<T>();
+            var client = _httpClientFactory.CreateClient(Prefix + options.CertificateSerialNo);
+            var body = await client.PostAsync(request, sortedTxtParams);
+            var parser = new WeChatPayResponseXmlParser<T>();
             var response = parser.Parse(body);
 
             if (request.GetNeedCheckSign())
@@ -206,7 +207,84 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
 
         #endregion
 
-        #region Common Method
+        #region IWeChatPayClient Members
+
+        public async Task<T> ExecuteAsync<T>(IWeChatPayV3GetRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response
+        {
+            if (options == null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
+
+            if (string.IsNullOrEmpty(options.MchId))
+            {
+                throw new ArgumentNullException(nameof(options.MchId));
+            }
+
+            if (string.IsNullOrEmpty(options.Certificate))
+            {
+                throw new ArgumentNullException(nameof(options.Certificate));
+            }
+
+            var client = _httpClientFactory.CreateClient(nameof(WeChatPayClient));
+            var (serial, timestamp, nonce, signature, body) = await client.GetAsync(request, options);
+            var parser = new WeChatPayV3ResponseJsonParser<T>();
+            var response = parser.Parse(body);
+
+            // 为下载微信支付平台证书证书响应时,
+            if (response is WeChatPayCertificatesResponse resp)
+            {
+                foreach (var certificate in resp.Certificates)
+                {
+                    // 若证书序列号未被缓存,将解密证书并加入缓存
+                    if (!_platformCertificateManager.ContainsKey(certificate.SerialNo))
+                    {
+                        var certStr = AEAD_AES_256_GCM.Decrypt(certificate.EncryptCertificate.Nonce, certificate.EncryptCertificate.Ciphertext, certificate.EncryptCertificate.AssociatedData, options.V3Key);
+                        var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certStr));
+                        _platformCertificateManager.TryAdd(certificate.SerialNo, cert);
+                    }
+                }
+            }
+
+            await CheckV3ResponseSignAsync(options, serial, timestamp, nonce, signature, body);
+
+            return response;
+        }
+
+        #endregion
+
+        #region IWeChatPayClient Members
+
+        public async Task<T> ExecuteAsync<T>(IWeChatPayV3PostRequest<T> request, WeChatPayOptions options) where T : WeChatPayV3Response
+        {
+            if (options == null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
+
+            if (string.IsNullOrEmpty(options.MchId))
+            {
+                throw new ArgumentNullException(nameof(options.MchId));
+            }
+
+            if (string.IsNullOrEmpty(options.Certificate))
+            {
+                throw new ArgumentNullException(nameof(options.Certificate));
+            }
+
+            var client = _httpClientFactory.CreateClient(nameof(WeChatPayClient));
+            var (serial, timestamp, nonce, signature, body) = await client.PostAsync(request, options);
+            var parser = new WeChatPayV3ResponseJsonParser<T>();
+            var response = parser.Parse(body);
+
+            await CheckV3ResponseSignAsync(options, serial, timestamp, nonce, signature, body);
+
+            return response;
+        }
+
+        #endregion
+
+        #region Check Response Method
 
         private void CheckResponseSign(WeChatPayResponse response, WeChatPayOptions options, WeChatPaySignType signType)
         {
@@ -220,9 +298,9 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
                 throw new WeChatPayException("sign check fail: Parameters is Empty!");
             }
 
-            if (response.Parameters["return_code"] == "SUCCESS")
+            if (response.Parameters["return_code"] == WeChatPayCode.Success)
             {
-                if (!response.Parameters.TryGetValue("sign", out var sign))
+                if (!response.Parameters.TryGetValue(WeChatPayConsts.sign, out var sign))
                 {
                     throw new WeChatPayException("sign check fail: sign is Empty!");
                 }
@@ -235,6 +313,53 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
             }
         }
 
+        private async Task CheckV3ResponseSignAsync(WeChatPayOptions options, string serial, string timestamp, string nonce, string signature, string body)
+        {
+            if (string.IsNullOrEmpty(serial))
+            {
+                throw new WeChatPayException($"sign check fail: {nameof(serial)} is empty!");
+            }
+
+            if (string.IsNullOrEmpty(signature))
+            {
+                throw new WeChatPayException($"sign check fail: {nameof(signature)} is empty!");
+            }
+
+            var cert = await LoadPlatformCertificateAsync(serial, options);
+            var signatureSourceDate = BuildSignatureSourceDate(timestamp, nonce, body);
+
+            if (!cert.GetRSAPublicKey().Verify(signatureSourceDate, signature))
+            {
+                throw new WeChatPayException("sign check fail: check Sign and Data Fail!");
+            }
+        }
+
+        private string BuildSignatureSourceDate(string timestamp, string nonce, string body)
+        {
+            return $"{timestamp}\n{nonce}\n{body}\n";
+        }
+
+        private async Task<X509Certificate2> LoadPlatformCertificateAsync(string serial, WeChatPayOptions options)
+        {
+            // 如果证书序列号已缓存,则直接使用缓存的
+            if (_platformCertificateManager.TryGetValue(serial, out var certificate2))
+            {
+                return certificate2;
+            }
+
+            // 否则重新下载新的平台证书
+            var request = new WeChatPayCertificatesRequest();
+            var response = await ExecuteAsync(request, options);
+            if (response.Certificates.Count > 0 && _platformCertificateManager.TryGetValue(serial, out certificate2))
+            {
+                return certificate2;
+            }
+            else
+            {
+                throw new WeChatPayException("Download certificates failed!");
+            }
+        }
+
         #endregion
     }
 }

+ 16 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayClientCertificateManager.cs

@@ -0,0 +1,16 @@
+using System.Collections.Concurrent;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay
+{
+    public class WeChatPayClientCertificateManager
+    {
+        private readonly ConcurrentDictionary<string, X509Certificate2> _certificateDictionary = new ConcurrentDictionary<string, X509Certificate2>();
+
+        public bool ContainsKey(string serialNo) => _certificateDictionary.ContainsKey(serialNo);
+
+        public bool TryAdd(string serialNo, X509Certificate2 certificate) => _certificateDictionary.TryAdd(serialNo, certificate);
+
+        public bool TryGetValue(string serialNo, out X509Certificate2 certificate) => _certificateDictionary.TryGetValue(serialNo, out certificate);
+    }
+}

+ 5 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayConsts.cs

@@ -30,5 +30,10 @@
 
         public const string MD5 = "MD5";
         public const string HMAC_SHA256 = "HMAC-SHA256";
+
+        public const string Wechatpay_Serial = "Wechatpay-Serial";
+        public const string Wechatpay_Timestamp = "Wechatpay-Timestamp";
+        public const string Wechatpay_Nonce = "Wechatpay-Nonce";
+        public const string Wechatpay_Signature = "Wechatpay-Signature";
     }
 }

+ 5 - 5
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayHandlerBuilderFilter.cs

@@ -7,11 +7,11 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
 {
     public class WeChatPayHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
     {
-        private readonly WeChatPayCertificateManager _certificateManager;
+        private readonly WeChatPayClientCertificateManager _clientCertificateManager;
 
-        public WeChatPayHandlerBuilderFilter(WeChatPayCertificateManager certificateManager)
+        public WeChatPayHandlerBuilderFilter(WeChatPayClientCertificateManager clientCertificateManager)
         {
-            _certificateManager = certificateManager;
+            _clientCertificateManager = clientCertificateManager;
         }
 
         public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
@@ -30,9 +30,9 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
                     if (builder.Name.Contains(WeChatPayClient.Prefix))
                     {
                         var hash = builder.Name.RemovePreFix(WeChatPayClient.Prefix);
-                        if (_certificateManager.TryGetValue(hash, out var certificate))
+                        if (_clientCertificateManager.TryGetValue(hash, out var clientCertificate))
                         {
-                            handler.ClientCertificates.Add(certificate);
+                            handler.ClientCertificates.Add(clientCertificate);
                         }
                     }
                 }

+ 18 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayNotify.cs

@@ -1,9 +1,27 @@
 #if NETCOREAPP3_1
 
+using System.Xml.Serialization;
+
 namespace Essensoft.AspNetCore.Payment.WeChatPay
 {
     public abstract class WeChatPayNotify : WeChatPayObject
     {
+        /// <summary>
+        /// 原始内容
+        /// </summary>
+        [XmlIgnore]
+        public string Body { get; set; }
+
+        /// <summary>
+        /// 原始参数
+        /// </summary>
+        [XmlIgnore]
+        public WeChatPayDictionary Parameters { get; internal set; }
+
+        /// <summary>
+        /// 处理 _$n / _$n_$m
+        /// </summary>
+        internal virtual void Execute() { }
     }
 }
 

+ 1 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayNotifyClient.cs

@@ -44,7 +44,7 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
             }
 
             var body = await new StreamReader(request.Body, Encoding.UTF8).ReadToEndAsync();
-            var parser = new WeChatPayXmlParser<T>();
+            var parser = new WeChatPayNotifyXmlParser<T>();
             var notify = parser.Parse(body);
             if (notify is WeChatPayRefundNotify)
             {

+ 1 - 19
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayObject.cs

@@ -1,24 +1,6 @@
-using System.Xml.Serialization;
-
-namespace Essensoft.AspNetCore.Payment.WeChatPay
+namespace Essensoft.AspNetCore.Payment.WeChatPay
 {
     public abstract class WeChatPayObject
     {
-        /// <summary>
-        /// 原始内容
-        /// </summary>
-        [XmlIgnore]
-        public string Body { get; set; }
-
-        /// <summary>
-        /// 原始参数
-        /// </summary>
-        [XmlIgnore]
-        public WeChatPayDictionary Parameters { get; internal set; }
-
-        /// <summary>
-        /// 处理 _$n / _$n_$m
-        /// </summary>
-        internal virtual void Execute() { }
     }
 }

+ 28 - 11
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayOptions.cs

@@ -1,4 +1,7 @@
-using Essensoft.AspNetCore.Payment.Security;
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
 
 namespace Essensoft.AspNetCore.Payment.WeChatPay
 {
@@ -7,7 +10,9 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
     /// </summary>
     public class WeChatPayOptions
     {
-        internal string CertificateHash;
+        internal X509Certificate2 X509Certificate2;
+        internal RSA CertificateRSAPrivateKey;
+        internal string CertificateSerialNo;
 
         private string certificate;
         private string certificatePassword;
@@ -31,23 +36,19 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
 
         /// <summary>
         /// 子商户应用号
-        /// 仅服务商使用
+        /// 仅服务商使用
         /// </summary>
         public string SubAppId { get; set; }
 
         /// <summary>
         /// 子商户号
-        /// 仅服务商使用
+        /// 仅服务商使用
         /// </summary>
         public string SubMchId { get; set; }
 
         /// <summary>
-        /// API密钥
-        /// </summary>
-        public string Key { get; set; }
-
-        /// <summary>
-        /// API证书(文件名/文件的Base64编码)
+        /// API证书
+        /// 证书文件路径/证书文件的base64字符串
         /// </summary>
         public string Certificate
         {
@@ -57,7 +58,13 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
                 if (!string.IsNullOrEmpty(value))
                 {
                     certificate = value;
-                    CertificateHash = MD5.Compute(certificate);
+
+                    X509Certificate2 = File.Exists(certificate) ?
+                        new X509Certificate2(certificate, CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet) :
+                        new X509Certificate2(Convert.FromBase64String(certificate), CertificatePassword, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet);
+
+                    CertificateRSAPrivateKey = X509Certificate2.GetRSAPrivateKey();
+                    CertificateSerialNo = X509Certificate2.GetSerialNumberString();
                 }
             }
         }
@@ -72,6 +79,16 @@ namespace Essensoft.AspNetCore.Payment.WeChatPay
             set => certificatePassword = value;
         }
 
+        /// <summary>
+        /// API密钥
+        /// </summary>
+        public string Key { get; set; }
+
+        /// <summary>
+        /// APIv3密钥
+        /// </summary>
+        public string V3Key { get; set; }
+
         /// <summary>
         /// RSA公钥
         /// 目前仅调用"企业付款到银行卡API"时使用,执行"获取RSA加密公钥API"即可获取。

+ 16 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayPlatformCertificateManager.cs

@@ -0,0 +1,16 @@
+using System.Collections.Concurrent;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay
+{
+    public class WeChatPayPlatformCertificateManager
+    {
+        private readonly ConcurrentDictionary<string, X509Certificate2> _certificateDictionary = new ConcurrentDictionary<string, X509Certificate2>();
+
+        public bool ContainsKey(string serialNo) => _certificateDictionary.ContainsKey(serialNo);
+
+        public bool TryAdd(string serialNo, X509Certificate2 certificate) => _certificateDictionary.TryAdd(serialNo, certificate);
+
+        public bool TryGetValue(string serialNo, out X509Certificate2 certificate) => _certificateDictionary.TryGetValue(serialNo, out certificate);
+    }
+}

+ 19 - 1
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayResponse.cs

@@ -1,6 +1,24 @@
-namespace Essensoft.AspNetCore.Payment.WeChatPay
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay
 {
     public abstract class WeChatPayResponse : WeChatPayObject
     {
+        /// <summary>
+        /// 原始内容
+        /// </summary>
+        [XmlIgnore]
+        public string Body { get; set; }
+
+        /// <summary>
+        /// 原始参数
+        /// </summary>
+        [XmlIgnore]
+        public WeChatPayDictionary Parameters { get; internal set; }
+
+        /// <summary>
+        /// 处理 _$n / _$n_$m
+        /// </summary>
+        internal virtual void Execute() { }
     }
 }

+ 13 - 0
src/Essensoft.AspNetCore.Payment.WeChatPay/WeChatPayV3Response.cs

@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.WeChatPay
+{
+    public abstract class WeChatPayV3Response : WeChatPayObject
+    {
+        /// <summary>
+        /// 原始内容
+        /// </summary>
+        [JsonIgnore]
+        public string Body { get; set; }
+    }
+}