Browse Source

QPay 实现(#26)

Roc 6 years ago
parent
commit
5b4fec9703
61 changed files with 1166 additions and 459 deletions
  1. 6 4
      samples/WebApplicationSample/Controllers/NotifyController.cs
  2. 35 32
      samples/WebApplicationSample/Controllers/QPayController.cs
  3. 4 4
      samples/WebApplicationSample/Models/QPayViewModel.cs
  4. 2 2
      samples/WebApplicationSample/Views/QPay/AppPay.cshtml
  5. 2 2
      samples/WebApplicationSample/Views/QPay/B2CPay.cshtml
  6. 2 2
      samples/WebApplicationSample/Views/QPay/MicroPay.cshtml
  7. 2 2
      samples/WebApplicationSample/Views/QPay/PubPay.cshtml
  8. 2 2
      samples/WebApplicationSample/Views/QPay/QrCodePay.cshtml
  9. 1 1
      samples/WebApplicationSample/Views/QPay/Refund.cshtml
  10. 34 0
      src/Essensoft.AspNetCore.Payment.QPay/IQPayCertRequest.cs
  11. 0 31
      src/Essensoft.AspNetCore.Payment.QPay/IQPayCertificateRequest.cs
  12. 6 29
      src/Essensoft.AspNetCore.Payment.QPay/IQPayClient.cs
  13. 3 14
      src/Essensoft.AspNetCore.Payment.QPay/IQPayNotifyClient.cs
  14. 12 9
      src/Essensoft.AspNetCore.Payment.QPay/IQPayRequest.cs
  15. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayEPayB2CNotify.cs
  16. 83 0
      src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayHbMchSendNotify.cs
  17. 16 4
      src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayMicroPayNotify.cs
  18. 3 3
      src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayUnifiedOrderNotify.cs
  19. 0 3
      src/Essensoft.AspNetCore.Payment.QPay/Parser/IQPayParser.cs
  20. 99 30
      src/Essensoft.AspNetCore.Payment.QPay/Parser/QPayListPropertyParser.cs
  21. 13 22
      src/Essensoft.AspNetCore.Payment.QPay/Parser/QPayXmlParser.cs
  22. 25 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayCertificateManager.cs
  23. 67 96
      src/Essensoft.AspNetCore.Payment.QPay/QPayClient.cs
  24. 15 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayCode.cs
  25. 14 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayConsts.cs
  26. 10 5
      src/Essensoft.AspNetCore.Payment.QPay/QPayDictionary.cs
  27. 0 3
      src/Essensoft.AspNetCore.Payment.QPay/QPayException.cs
  28. 40 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayHandlerBuilderFilter.cs
  29. 0 3
      src/Essensoft.AspNetCore.Payment.QPay/QPayNotify.cs
  30. 15 24
      src/Essensoft.AspNetCore.Payment.QPay/QPayNotifyClient.cs
  31. 5 6
      src/Essensoft.AspNetCore.Payment.QPay/QPayObject.cs
  32. 27 8
      src/Essensoft.AspNetCore.Payment.QPay/QPayOptions.cs
  33. 0 3
      src/Essensoft.AspNetCore.Payment.QPay/QPayResponse.cs
  34. 10 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayTradeStates.cs
  35. 20 0
      src/Essensoft.AspNetCore.Payment.QPay/QPayTradeType.cs
  36. 13 9
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayCloseOrderRequest.cs
  37. 16 12
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayB2CRequest.cs
  38. 11 2
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayQueryRequest.cs
  39. 11 2
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayStatementDownRequest.cs
  40. 47 0
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchDownListFileRequest.cs
  41. 66 0
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchListQueryRequest.cs
  42. 127 0
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchSendRequest.cs
  43. 14 10
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayMicroPayRequest.cs
  44. 12 8
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayOrderQueryRequest.cs
  45. 12 8
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayRefundQueryRequest.cs
  46. 13 9
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayRefundRequest.cs
  47. 13 9
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayReverseRequest.cs
  48. 13 3
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPaySpDownloadStatementDownRequest.cs
  49. 21 11
      src/Essensoft.AspNetCore.Payment.QPay/Request/QPayUnifiedOrderRequest.cs
  50. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayEPayQueryResponse.cs
  51. 9 0
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchDownListFileResponse.cs
  52. 72 0
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchListQueryResponse.cs
  53. 38 0
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchSendResponse.cs
  54. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayOrderQueryResponse.cs
  55. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayRefundQueryResponse.cs
  56. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPayRefundResponse.cs
  57. 1 1
      src/Essensoft.AspNetCore.Payment.QPay/Response/QPaySpDownloadStatementDownResponse.cs
  58. 10 2
      src/Essensoft.AspNetCore.Payment.QPay/ServiceCollectionExtensions.cs
  59. 9 11
      src/Essensoft.AspNetCore.Payment.QPay/Utility/HttpClientExtensions.cs
  60. 3 7
      src/Essensoft.AspNetCore.Payment.QPay/Utility/QPaySignature.cs
  61. 57 6
      src/Essensoft.AspNetCore.Payment.QPay/Utility/QPayUtility.cs

+ 6 - 4
samples/WebApplicationSample/Controllers/NotifyController.cs

@@ -235,10 +235,12 @@ namespace WebApplicationSample.Controllers
     public class QPayNotifyController : Controller
     {
         private readonly IQPayNotifyClient _client;
+        private readonly IOptions<QPayOptions> _optionsAccessor;
 
-        public QPayNotifyController(IQPayNotifyClient client)
+        public QPayNotifyController(IQPayNotifyClient client, IOptions<QPayOptions> optionsAccessor)
         {
             _client = client;
+            _optionsAccessor = optionsAccessor;
         }
 
         /// <summary>
@@ -251,7 +253,7 @@ namespace WebApplicationSample.Controllers
         {
             try
             {
-                var notify = await _client.ExecuteAsync<QPayUnifiedOrderNotify>(Request);
+                var notify = await _client.ExecuteAsync<QPayUnifiedOrderNotify>(Request, _optionsAccessor.Value);
                 if ("SUCCESS" == notify.TradeState)
                 {
                     Console.WriteLine("OutTradeNo: " + notify.OutTradeNo);
@@ -275,7 +277,7 @@ namespace WebApplicationSample.Controllers
         {
             try
             {
-                var notify = await _client.ExecuteAsync<QPayMicroPayNotify>(Request);
+                var notify = await _client.ExecuteAsync<QPayMicroPayNotify>(Request, _optionsAccessor.Value);
                 if ("SUCCESS" == notify.TradeState)
                 {
                     Console.WriteLine("OutTradeNo: " + notify.OutTradeNo);
@@ -299,7 +301,7 @@ namespace WebApplicationSample.Controllers
         {
             try
             {
-                var notify = await _client.ExecuteAsync<QPayEPayB2CNotify>(Request);
+                var notify = await _client.ExecuteAsync<QPayEPayB2CNotify>(Request, _optionsAccessor.Value);
                 Console.WriteLine("OutTradeNo: " + notify.OutTradeNo);
                 return QPayNotifyResult.Success;
             }

+ 35 - 32
samples/WebApplicationSample/Controllers/QPayController.cs

@@ -3,6 +3,7 @@ using Essensoft.AspNetCore.Payment.QPay;
 using Essensoft.AspNetCore.Payment.QPay.Request;
 using Essensoft.AspNetCore.Payment.Security;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
 using WebApplicationSample.Models;
 
 namespace WebApplicationSample.Controllers
@@ -10,10 +11,12 @@ namespace WebApplicationSample.Controllers
     public class QPayController : Controller
     {
         private readonly IQPayClient _client;
+        private readonly IOptions<QPayOptions> _optionsAccessor;
 
-        public QPayController(IQPayClient client)
+        public QPayController(IQPayClient client, IOptions<QPayOptions> optionsAccessor)
         {
             _client = client;
+            _optionsAccessor = optionsAccessor;
         }
 
         /// <summary>
@@ -49,14 +52,14 @@ namespace WebApplicationSample.Controllers
                 Body = viewModel.Body,
                 FeeType = viewModel.FeeType,
                 TotalFee = viewModel.TotalFee,
-                SpbillCreateIp = viewModel.SpbillCreateIp,
+                SpBillCreateIp = viewModel.SpBillCreateIp,
                 DeviceInfo = viewModel.DeviceInfo,
                 AuthCode = viewModel.AuthCode,
                 TradeType = viewModel.TradeType,
                 NotifyUrl = viewModel.NotifyUrl
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -84,13 +87,13 @@ namespace WebApplicationSample.Controllers
                 Body = viewModel.Body,
                 FeeType = viewModel.FeeType,
                 TotalFee = viewModel.TotalFee,
-                SpbillCreateIp = viewModel.SpbillCreateIp,
+                SpBillCreateIp = viewModel.SpBillCreateIp,
                 TradeType = viewModel.TradeType,
                 NotifyUrl = viewModel.NotifyUrl
             };
-            var response = await _client.ExecuteAsync(request);
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
             ViewData["qrcode"] = response.CodeUrl;
-            ViewData["response"] = response.Body;
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -118,12 +121,12 @@ namespace WebApplicationSample.Controllers
                 Body = viewModel.Body,
                 FeeType = viewModel.FeeType,
                 TotalFee = viewModel.TotalFee,
-                SpbillCreateIp = viewModel.SpbillCreateIp,
+                SpBillCreateIp = viewModel.SpBillCreateIp,
                 TradeType = viewModel.TradeType,
                 NotifyUrl = viewModel.NotifyUrl
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -151,12 +154,12 @@ namespace WebApplicationSample.Controllers
                 Body = viewModel.Body,
                 FeeType = viewModel.FeeType,
                 TotalFee = viewModel.TotalFee,
-                SpbillCreateIp = viewModel.SpbillCreateIp,
+                SpBillCreateIp = viewModel.SpBillCreateIp,
                 TradeType = viewModel.TradeType,
                 NotifyUrl = viewModel.NotifyUrl
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -183,8 +186,8 @@ namespace WebApplicationSample.Controllers
                 TransactionId = viewModel.TransactionId,
                 OutTradeNo = viewModel.OutTradeNo
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -210,8 +213,8 @@ namespace WebApplicationSample.Controllers
             {
                 OutTradeNo = viewModel.OutTradeNo
             };
-            var response = await _client.ExecuteAsync(request, "qpayCertificateName");
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -237,13 +240,13 @@ namespace WebApplicationSample.Controllers
             {
                 OutTradeNo = viewModel.OutTradeNo
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
         /// <summary>
-        /// 关闭订单
+        /// 申请退款
         /// </summary>
         /// <returns></returns>
         [HttpGet]
@@ -253,7 +256,7 @@ namespace WebApplicationSample.Controllers
         }
 
         /// <summary>
-        /// 关闭订单
+        /// 申请退款
         /// </summary>
         /// <param name="viewModel"></param>
         /// <returns></returns>
@@ -267,10 +270,10 @@ namespace WebApplicationSample.Controllers
                 OutTradeNo = viewModel.OutTradeNo,
                 RefundFee = viewModel.RefundFee,
                 OpUserId = viewModel.OpUserId,
-                OpUserPasswd = viewModel.OpUserPasswd
+                OpUserPasswd = MD5.Compute(viewModel.OpUserPasswd).ToUpper(),
             };
-            var response = await _client.ExecuteAsync(request, "qpayCertificateName");
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -299,8 +302,8 @@ namespace WebApplicationSample.Controllers
                 TransactionId = viewModel.TransactionId,
                 OutTradeNo = viewModel.OutTradeNo
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -322,14 +325,14 @@ namespace WebApplicationSample.Controllers
         [HttpPost]
         public async Task<IActionResult> StatementDown(QPayStatementDownViewModel viewModel)
         {
-            var request = new QPayStatementDownRequest
+            var request = new QPaySpDownloadStatementDownRequest
             {
                 BillDate = viewModel.BillDate,
                 BillType = viewModel.BillType,
                 TarType = viewModel.TarType
             };
-            var response = await _client.ExecuteAsync(request);
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
 
@@ -361,11 +364,11 @@ namespace WebApplicationSample.Controllers
                 CheckRealName = viewModel.CheckRealName,
                 OpUserId = viewModel.OpUserId,
                 OpUserPasswd = MD5.Compute(viewModel.OpUserPasswd).ToUpper(),
-                SpbillCreateIp = viewModel.SpbillCreateIp,
+                SpBillCreateIp = viewModel.SpBillCreateIp,
                 NotifyUrl = viewModel.NotifyUrl,
             };
-            var response = await _client.ExecuteAsync(request, "qpayCertificateName");
-            ViewData["response"] = response.Body;
+            var response = await _client.ExecuteAsync(request, _optionsAccessor.Value);
+            ViewData["response"] = response.ResponseBody;
             return View();
         }
     }

+ 4 - 4
samples/WebApplicationSample/Models/QPayViewModel.cs

@@ -22,7 +22,7 @@ namespace WebApplicationSample.Models
 
         [Required]
         [Display(Name = "spbill_create_ip")]
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         [Required]
         [Display(Name = "device_info")]
@@ -61,7 +61,7 @@ namespace WebApplicationSample.Models
 
         [Required]
         [Display(Name = "spbill_create_ip")]
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         [Required]
         [Display(Name = "trade_type")]
@@ -163,7 +163,7 @@ namespace WebApplicationSample.Models
 
         [Display(Name = "total_fee")]
         [Required]
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         [Display(Name = "memo")]
         public string Memo { get; set; }
@@ -181,7 +181,7 @@ namespace WebApplicationSample.Models
 
         [Required]
         [Display(Name = "spbill_create_ip")]
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         [Display(Name = "notify_url")]
         public string NotifyUrl { get; set; }

+ 2 - 2
samples/WebApplicationSample/Views/QPay/AppPay.cshtml

@@ -30,8 +30,8 @@
                 <input type="text" class="form-control" asp-for="TotalFee" value="1" />
             </div>
             <div class="form-group">
-                <label asp-for="SpbillCreateIp"></label>
-                <input type="text" class="form-control" asp-for="SpbillCreateIp" value="127.0.0.1" />
+                <label asp-for="SpBillCreateIp"></label>
+                <input type="text" class="form-control" asp-for="SpBillCreateIp" value="127.0.0.1" />
             </div>
             <div class="form-group">
                 <label asp-for="TradeType"></label>

+ 2 - 2
samples/WebApplicationSample/Views/QPay/B2CPay.cshtml

@@ -46,8 +46,8 @@
                 <input type="text" class="form-control" asp-for="OpUserPasswd" />
             </div>
             <div class="form-group">
-                <label asp-for="SpbillCreateIp"></label>
-                <input type="text" class="form-control" asp-for="SpbillCreateIp" value="127.0.0.1" />
+                <label asp-for="SpBillCreateIp"></label>
+                <input type="text" class="form-control" asp-for="SpBillCreateIp" value="127.0.0.1" />
             </div>
             <div class="form-group">
                 <label asp-for="NotifyUrl"></label>

+ 2 - 2
samples/WebApplicationSample/Views/QPay/MicroPay.cshtml

@@ -30,8 +30,8 @@
                 <input type="text" class="form-control" asp-for="TotalFee" value="1" />
             </div>
             <div class="form-group">
-                <label asp-for="SpbillCreateIp"></label>
-                <input type="text" class="form-control" asp-for="SpbillCreateIp" value="127.0.0.1" />
+                <label asp-for="SpBillCreateIp"></label>
+                <input type="text" class="form-control" asp-for="SpBillCreateIp" value="127.0.0.1" />
             </div>
             <div class="form-group">
                 <label asp-for="DeviceInfo"></label>

+ 2 - 2
samples/WebApplicationSample/Views/QPay/PubPay.cshtml

@@ -30,8 +30,8 @@
                 <input type="text" class="form-control" asp-for="TotalFee" value="1" />
             </div>
             <div class="form-group">
-                <label asp-for="SpbillCreateIp"></label>
-                <input type="text" class="form-control" asp-for="SpbillCreateIp" value="127.0.0.1" />
+                <label asp-for="SpBillCreateIp"></label>
+                <input type="text" class="form-control" asp-for="SpBillCreateIp" value="127.0.0.1" />
             </div>
             <div class="form-group">
                 <label asp-for="TradeType"></label>

+ 2 - 2
samples/WebApplicationSample/Views/QPay/QrCodePay.cshtml

@@ -30,8 +30,8 @@
                 <input type="text" class="form-control" asp-for="TotalFee" value="1" />
             </div>
             <div class="form-group">
-                <label asp-for="SpbillCreateIp"></label>
-                <input type="text" class="form-control" asp-for="SpbillCreateIp" value="127.0.0.1" />
+                <label asp-for="SpBillCreateIp"></label>
+                <input type="text" class="form-control" asp-for="SpBillCreateIp" value="127.0.0.1" />
             </div>
             <div class="form-group">
                 <label asp-for="TradeType"></label>

+ 1 - 1
samples/WebApplicationSample/Views/QPay/Refund.cshtml

@@ -1,6 +1,6 @@
 @model QPayRefundViewModel
 @{
-    ViewData["Title"] = "关闭订单";
+    ViewData["Title"] = "申请退款";
 }
 <nav aria-label="breadcrumb">
     <ol class="breadcrumb">

+ 34 - 0
src/Essensoft.AspNetCore.Payment.QPay/IQPayCertRequest.cs

@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public interface IQPayCertRequest<T> where T : QPayResponse
+    {
+        /// <summary>
+        /// 获取API接口链接
+        /// </summary>
+        /// <returns>API接口链接</returns>
+        string GetRequestUrl();
+
+        /// <summary>
+        /// 获取所有的Key-Value形式的文本请求参数字典。其中:
+        /// Key: 请求参数名
+        /// Value: 请求参数文本值
+        /// </summary>
+        /// <returns>文本请求参数字典</returns>
+        IDictionary<string, string> GetParameters();
+
+        /// <summary>
+        /// 基本参数处理器
+        /// </summary>
+        /// <param name="options">配置选项</param>
+        /// <param name="sortedTxtParams">排序文本参数</param>
+        void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams);
+
+        /// <summary>
+        /// 是否需要检查响应内容签名
+        /// </summary>
+        /// <returns>是否需要检查</returns>
+        bool GetNeedCheckSign();
+    }
+}

+ 0 - 31
src/Essensoft.AspNetCore.Payment.QPay/IQPayCertificateRequest.cs

@@ -1,31 +0,0 @@
-using System.Collections.Generic;
-
-namespace Essensoft.AspNetCore.Payment.QPay
-{
-    /// <summary>
-    /// QPay 证书请求接口。
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    public interface IQPayCertificateRequest<T> where T : QPayResponse
-    {
-        /// <summary>
-        /// API接口地址
-        /// </summary>
-        /// <returns></returns>
-        string GetRequestUrl();
-
-        /// <summary>
-        /// 获取所有的Key-Value形式的文本请求参数字典。其中:
-        /// Key: 请求参数名
-        /// Value: 请求参数文本值
-        /// </summary>
-        /// <returns>文本请求参数字典</returns>
-        IDictionary<string, string> GetParameters();
-
-        /// <summary>
-        /// 是否验证应答内容签名
-        /// </summary>
-        /// <returns>是否验证</returns>
-        bool IsCheckResponseSign();
-    }
-}

+ 6 - 29
src/Essensoft.AspNetCore.Payment.QPay/IQPayClient.cs

@@ -2,45 +2,22 @@
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 客户端。
-    /// </summary>
     public interface IQPayClient
     {
         /// <summary>
-        /// 执行QPay API请求。
+        /// 执行 QPay API请求。
         /// </summary>
-        /// <typeparam name="T">领域对象</typeparam>
         /// <param name="request">具体的QPay API请求</param>
+        /// <param name="options">配置选项</param>
         /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(IQPayRequest<T> request) where T : QPayResponse;
+        Task<T> ExecuteAsync<T>(IQPayRequest<T> request, QPayOptions options) where T : QPayResponse;
 
         /// <summary>
-        /// 执行QPay API请求。
+        /// 执行 QPay API证书请求。
         /// </summary>
-        /// <typeparam name="T">领域对象</typeparam>
-        /// <param name="request">具体的QPay API请求</param>
-        /// <param name="optionsName">配置选项名称</param>
-        /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(IQPayRequest<T> request, string optionsName) where T : QPayResponse;
-
-        /// <summary>
-        /// 执行QPay API证书请求。
-        /// </summary>
-        /// <typeparam name="T">领域对象</typeparam>
-        /// <param name="request">具体的QPay API证书请求</param>
-        /// <param name="certificateName">证书名称</param>
-        /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(IQPayCertificateRequest<T> request, string certificateName) where T : QPayResponse;
-
-        /// <summary>
-        /// 执行QPay API证书请求。
-        /// </summary>
-        /// <typeparam name="T">领域对象</typeparam>
         /// <param name="request">具体的QPay API证书请求</param>
-        /// <param name="optionsName">配置选项名称</param>
-        /// <param name="certificateName">证书名称</param>
+        /// <param name="options">配置选项</param>
         /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(IQPayCertificateRequest<T> request, string optionsName, string certificateName) where T : QPayResponse;
+        Task<T> ExecuteAsync<T>(IQPayCertRequest<T> request, QPayOptions options) where T : QPayResponse;
     }
 }

+ 3 - 14
src/Essensoft.AspNetCore.Payment.QPay/IQPayNotifyClient.cs

@@ -3,26 +3,15 @@ using Microsoft.AspNetCore.Http;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 通知解析客户端。
-    /// </summary>
     public interface IQPayNotifyClient
     {
         /// <summary>
-        /// 执行QPay通知请求解析。
+        /// 执行 QPay 通知请求解析。
         /// </summary>
         /// <typeparam name="T">领域对象</typeparam>
         /// <param name="request">控制器的请求</param>
+        /// <param name="options">配置选项</param>
         /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(HttpRequest request) where T : QPayNotify;
-
-        /// <summary>
-        /// 执行QPay通知请求解析。
-        /// </summary>
-        /// <typeparam name="T">领域对象</typeparam>
-        /// <param name="request">控制器的请求</param>
-        /// <param name="optionsName">配置选项名称</param>
-        /// <returns>领域对象</returns>
-        Task<T> ExecuteAsync<T>(HttpRequest request, string optionsName) where T : QPayNotify;
+        Task<T> ExecuteAsync<T>(HttpRequest request, QPayOptions options) where T : QPayNotify;
     }
 }

+ 12 - 9
src/Essensoft.AspNetCore.Payment.QPay/IQPayRequest.cs

@@ -2,16 +2,12 @@
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 请求接口。
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
     public interface IQPayRequest<T> where T : QPayResponse
     {
         /// <summary>
-        /// API接口地址
+        /// 获取API接口链接
         /// </summary>
-        /// <returns></returns>
+        /// <returns>API接口链接</returns>
         string GetRequestUrl();
 
         /// <summary>
@@ -23,9 +19,16 @@ namespace Essensoft.AspNetCore.Payment.QPay
         IDictionary<string, string> GetParameters();
 
         /// <summary>
-        /// 是否验证应答内容签名
+        /// 基本参数处理器
         /// </summary>
-        /// <returns>是否验证</returns>
-        bool IsCheckResponseSign();
+        /// <param name="options">配置选项</param>
+        /// <param name="sortedTxtParams">排序文本参数</param>
+        void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams);
+
+        /// <summary>
+        /// 是否需要检查响应内容签名
+        /// </summary>
+        /// <returns>是否需要检查</returns>
+        bool GetNeedCheckSign();
     }
 }

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayEPayB2CNotify.cs

@@ -48,7 +48,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Notify
         /// 金额
         /// </summary>
         [XmlElement("total_fee")]
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         /// <summary>
         /// 时间(红包领取或退款成功时间)

+ 83 - 0
src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayHbMchSendNotify.cs

@@ -0,0 +1,83 @@
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Notify
+{
+    /// <summary>
+    /// 现金红包 - 领取结果通知
+    /// </summary>
+    [XmlRoot("xml")]
+    public class QPayHbMchSendNotify : QPayNotify
+    {
+        /// <summary>
+        /// 应用ID
+        /// </summary>
+        [XmlElement("appid")]
+        public string AppId { get; set; }
+
+        /// <summary>
+        /// 商户号
+        /// </summary>
+        [XmlElement("mch_id")]
+        public string MchId { get; set; }
+
+        /// <summary>
+        /// 商户订单号
+        /// </summary>
+        [XmlElement("out_trade_no")]
+        public string OutTradeNo { get; set; }
+
+        /// <summary>
+        /// QQ钱包业务单号
+        /// </summary>
+        [XmlElement("transaction_id")]
+        public string TransactionId { get; set; }
+
+        /// <summary>
+        /// 收款用户openid
+        /// </summary>
+        [XmlElement("openid")]
+        public string OpenId { get; set; }
+
+        /// <summary>
+        /// 金额
+        /// </summary>
+        [XmlElement("total_fee")]
+        public long TotalFee { get; set; }
+
+        /// <summary>
+        /// 时间(红包领取或退款成功时间)
+        /// </summary>
+        [XmlElement("time_end")]
+        public string TimeEnd { get; set; }
+
+        /// <summary>
+        /// 状态
+        /// </summary>
+        [XmlElement("state")]
+        public int State { get; set; }
+
+        /// <summary>
+        /// 退款原因
+        /// </summary>
+        [XmlElement("refund_reason")]
+        public string RefundReason { get; set; }
+
+        /// <summary>
+        /// 商户附加数据
+        /// </summary>
+        [XmlElement("attach")]
+        public string Attach { get; set; }
+
+        /// <summary>
+        /// 签名
+        /// </summary>
+        [XmlElement("sign")]
+        public string Sign { get; set; }
+
+        /// <summary>
+        /// 签名类型
+        /// </summary>
+        [XmlElement("sign_type")]
+        public string SignType { get; set; }
+    }
+}

+ 16 - 4
src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayMicroPayNotify.cs

@@ -15,11 +15,23 @@ namespace Essensoft.AspNetCore.Payment.QPay.Notify
         public string AppId { get; set; }
 
         /// <summary>
-        /// 商户号ID
+        /// 子商户应用ID
+        /// </summary>
+        [XmlElement("sub_appid")]
+        public string SubAppId { get; set; }
+
+        /// <summary>
+        /// 商户号
         /// </summary>
         [XmlElement("mch_id")]
         public string MchId { get; set; }
 
+        /// <summary>
+        /// 子商户号
+        /// </summary>
+        [XmlElement("sub_mch_id")]
+        public string SubMchId { get; set; }
+
         /// <summary>
         /// 随机字符串
         /// </summary>
@@ -66,19 +78,19 @@ namespace Essensoft.AspNetCore.Payment.QPay.Notify
         /// 订单金额
         /// </summary>
         [XmlElement("total_fee")]
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         /// <summary>
         /// 用户支付金额
         /// </summary>
         [XmlElement("cash_fee")]
-        public string CashFee { get; set; }
+        public int CashFee { get; set; }
 
         /// <summary>
         /// QQ钱包优惠金额
         /// </summary>
         [XmlElement("coupon_fee")]
-        public string CouponFee { get; set; }
+        public int CouponFee { get; set; }
 
         /// <summary>
         /// QQ钱包订单号

+ 3 - 3
src/Essensoft.AspNetCore.Payment.QPay/Notify/QPayUnifiedOrderNotify.cs

@@ -66,19 +66,19 @@ namespace Essensoft.AspNetCore.Payment.QPay.Notify
         /// 订单金额
         /// </summary>
         [XmlElement("total_fee")]
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         /// <summary>
         /// 用户支付金额
         /// </summary>
         [XmlElement("cash_fee")]
-        public string CashFee { get; set; }
+        public int CashFee { get; set; }
 
         /// <summary>
         /// QQ钱包优惠金额
         /// </summary>
         [XmlElement("coupon_fee")]
-        public string CouponFee { get; set; }
+        public int CouponFee { get; set; }
 
         /// <summary>
         /// QQ钱包订单号

+ 0 - 3
src/Essensoft.AspNetCore.Payment.QPay/Parser/IQPayParser.cs

@@ -1,8 +1,5 @@
 namespace Essensoft.AspNetCore.Payment.QPay.Parser
 {
-    /// <summary>
-    /// QPay 解释器。
-    /// </summary>
     public interface IQPayParser<T> where T : QPayObject
     {
         T Parse(string body);

+ 99 - 30
src/Essensoft.AspNetCore.Payment.QPay/Parser/QPayListPropertyParser.cs

@@ -1,61 +1,117 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
 using System.Xml.Serialization;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Parser
 {
-    /// <summary>
-    /// QPay ListProperty 解释器。
-    /// </summary>
     public class QPayListPropertyParser
     {
-        public List<T> Parse<T, TChildren>(QPayDictionary dictionary, int index = -1)
+        public List<T> Parse<T>(QPayDictionary dictionary) where T : new()
+        {
+            var list = new List<T>();
+            var properties = typeof(T).GetProperties();
+            var keyfirst = properties[0];
+            var count = dictionary.Keys.Where(p => Regex.IsMatch(p, $@"{GetKeyName(keyfirst)}_\d")).Count();
+
+            for (var i = 0; i < count; i++)
+            {
+                var item = new T();
+                foreach (var field in properties)
+                {
+                    var name = $"{GetKeyName(field)}_{i}";
+                    field.SetValue(item, Convert.ChangeType(dictionary.GetValue(name), field.PropertyType));
+                }
+                list.Add(item);
+            }
+            return list;
+        }
+
+        public List<T> Parse<T, TChildren>(QPayDictionary dictionary) where T : new() where TChildren : new()
+        {
+            var list = new List<T>();
+            var properties = typeof(T).GetProperties();
+            var keyfirst = properties[0];
+            var count = dictionary.Keys.Where(p => Regex.IsMatch(p, $@"{GetKeyName(keyfirst)}_\d")).Count();
+
+            for (var i = 0; i < count; i++)
+            {
+                var item = new T();
+                foreach (var field in properties)
+                {
+                    if (field.PropertyType == typeof(List<TChildren>))
+                    {
+                        var sublist = new List<TChildren>();
+                        var subProperties = typeof(TChildren).GetProperties();
+                        var subFirstkey = subProperties[0];
+                        var SubFirstName = GetKeyName(subFirstkey);
+                        var subCount = dictionary.Keys.Where(p => Regex.IsMatch(p, $@"{SubFirstName}_{i}_\d")).Count();
+                        for (var j = 0; j < subCount; j++)
+                        {
+                            var subItem = new TChildren();
+                            foreach (var subfield in subProperties)
+                            {
+                                var name = $"{GetKeyName(subfield)}_{i}_{j}";
+                                subfield.SetValue(item, Convert.ChangeType(dictionary.GetValue(name), subfield.PropertyType));
+                            }
+                            sublist.Add(subItem);
+                        }
+                        field.SetValue(item, sublist);
+                    }
+                    else
+                    {
+                        var name = $"{GetKeyName(field)}_{i}";
+                        field.SetValue(item, Convert.ChangeType(dictionary.GetValue(name), field.PropertyType));
+                    }
+                }
+                list.Add(item);
+            }
+            return list;
+        }
+
+        public List<T> Parse<T, TChildren>(QPayDictionary dictionary, int index) where T : new() where TChildren : new()
         {
             var flag = true;
             var list = new List<T>();
             var i = 0;
-
             while (flag)
             {
                 var type = typeof(T);
-                var obj = Activator.CreateInstance(type);
+                var obj = new T();
                 var properties = type.GetProperties();
                 var isFirstProperty = true;
 
-                foreach (var propertie in properties)
+                foreach (var item in properties)
                 {
-                    if (propertie.PropertyType == typeof(List<TChildren>))
+                    if (item.PropertyType == typeof(List<TChildren>))
                     {
                         var chidrenList = Parse<TChildren, object>(dictionary, i);
-                        propertie.SetValue(obj, chidrenList);
+                        item.SetValue(obj, chidrenList);
                         continue;
                     }
 
-                    var renameAttribute = propertie.GetCustomAttributes(typeof(XmlElementAttribute), true);
-                    if (renameAttribute.Length > 0)
+                    var key = GetKeyName(item);
+                    if (index > -1)
                     {
-                        var key = ((XmlElementAttribute)renameAttribute[0]).ElementName;
+                        key += $"_{index}";
+                    }
+                    key += $"_{i}";
 
-                        if (index > -1)
+                    var value = dictionary.GetValue(key);
+                    if (value == null)
+                    {
+                        if (isFirstProperty)
                         {
-                            key += $"_{index}";
+                            flag = false;
+                            break;
                         }
-                        key += $"_{i}";
-
-                        var value = dictionary.GetValue(key);
-                        if (value == null)
-                        {
-                            if (isFirstProperty)
-                            {
-                                flag = false;
-                                break;
-                            }
-                            continue;
-                        }
-
-                        isFirstProperty = false;
-                        propertie.SetValue(obj, Convert.ChangeType(value, propertie.PropertyType));
+                        continue;
                     }
+
+                    isFirstProperty = false;
+                    item.SetValue(obj, Convert.ChangeType(value, item.PropertyType));
                 }
 
                 if (!flag)
@@ -63,11 +119,24 @@ namespace Essensoft.AspNetCore.Payment.QPay.Parser
                     return list;
                 }
 
-                list.Add((T)obj);
+                list.Add(obj);
                 i++;
             }
 
             return list;
         }
+
+        private string GetKeyName(PropertyInfo item)
+        {
+            var key = item.GetCustomAttributes(typeof(XmlElementAttribute), true);
+            if (key.Length > 0)
+            {
+                return ((XmlElementAttribute)key[0]).ElementName;
+            }
+            else
+            {
+                throw new QPayException($"{item.Name} undefined key name.");
+            }
+        }
     }
 }

+ 13 - 22
src/Essensoft.AspNetCore.Payment.QPay/Parser/QPayXmlParser.cs

@@ -5,48 +5,39 @@ using System.Xml.Serialization;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Parser
 {
-    /// <summary>
-    /// QPay XML 解释器。
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
     public class QPayXmlParser<T> : IQPayParser<T> where T : QPayObject
     {
         public T Parse(string body)
         {
-            T rsp = null;
+            T result = null;
             var parameters = new QPayDictionary();
 
             try
             {
-                using (var sr = new StringReader(body))
-                {
-                    var xmldes = new XmlSerializer(typeof(T));
-                    rsp = (T)xmldes.Deserialize(sr);
-                }
-
                 var doc = XDocument.Parse(body).Root;
                 foreach (var element in doc.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 (rsp == null)
+            if (result == null)
             {
-                rsp = Activator.CreateInstance<T>();
+                result = Activator.CreateInstance<T>();
             }
 
-            if (rsp != null)
-            {
-                rsp.Body = body;
-
-                rsp.Parameters = parameters;
-
-                rsp.Execute();
-            }
+            result.ResponseBody = body;
+            result.ResponseParameters = parameters;
+            result.Execute();
 
-            return rsp;
+            return result;
         }
     }
 }

+ 25 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayCertificateManager.cs

@@ -0,0 +1,25 @@
+using System.Collections.Concurrent;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public class QPayCertificateManager
+    {
+        private readonly ConcurrentDictionary<string, X509Certificate2> _certDic = new ConcurrentDictionary<string, X509Certificate2>();
+
+        public bool Contains(string hash)
+        {
+            return _certDic.ContainsKey(hash);
+        }
+
+        public bool TryAdd(string hash, X509Certificate2 certificate)
+        {
+            return _certDic.TryAdd(hash, certificate);
+        }
+
+        public bool TryGet(string hash, out X509Certificate2 certificate)
+        {
+            return _certDic.TryGetValue(hash, out certificate);
+        }
+    }
+}

+ 67 - 96
src/Essensoft.AspNetCore.Payment.QPay/QPayClient.cs

@@ -1,146 +1,117 @@
 using System;
+using System.IO;
 using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
 using System.Threading.Tasks;
 using Essensoft.AspNetCore.Payment.QPay.Parser;
-using Essensoft.AspNetCore.Payment.QPay.Request;
 using Essensoft.AspNetCore.Payment.QPay.Utility;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 客户端。
-    /// </summary>
     public class QPayClient : IQPayClient
     {
-        private const string APPID = "appid";
-        private const string MCHID = "mch_id";
-        private const string NONCE_STR = "nonce_str";
-        private const string SIGN = "sign";
-        private const string UIN = "uin";
-        
-        private readonly ILogger _logger;
-        private readonly IHttpClientFactory _clientFactory;
-        private readonly IOptionsSnapshot<QPayOptions> _optionsSnapshotAccessor;
+        public const string Prefix = nameof(QPayClient) + ".";
+
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly QPayCertificateManager _certificateManager;
 
         #region QPayClient Constructors
 
-        public QPayClient(
-            ILogger<QPayClient> logger,
-            IHttpClientFactory clientFactory,
-            IOptionsSnapshot<QPayOptions> optionsAccessor)
+        public QPayClient(IHttpClientFactory httpClientFactory, QPayCertificateManager certificateManager)
         {
-            _logger = logger;
-            _clientFactory = clientFactory;
-            _optionsSnapshotAccessor = optionsAccessor;
+            _httpClientFactory = httpClientFactory;
+            _certificateManager = certificateManager;
         }
 
         #endregion
 
         #region IQPayClient Members
 
-        public async Task<T> ExecuteAsync<T>(IQPayRequest<T> request) where T : QPayResponse
+        public async Task<T> ExecuteAsync<T>(IQPayRequest<T> request, QPayOptions options) where T : QPayResponse
         {
-            return await ExecuteAsync(request, null);
-        }
-
-        public async Task<T> ExecuteAsync<T>(IQPayRequest<T> request, string optionsName) where T : QPayResponse
-        {
-            var options = _optionsSnapshotAccessor.Get(optionsName);
-            var sortedTxtParams = new QPayDictionary(request.GetParameters())
+            if (options == null)
             {
-                { MCHID, options.MchId },
-                { NONCE_STR, Guid.NewGuid().ToString("N") }
-            };
+                throw new ArgumentNullException(nameof(options));
+            }
 
-            if (request is QPayEPayQueryRequest)
+            if (string.IsNullOrEmpty(options.MchId))
             {
+                throw new ArgumentNullException(nameof(options.MchId));
             }
-            else if (request is QPayEPayStatementDownRequest)
+
+            if (string.IsNullOrEmpty(options.Key))
             {
+                throw new ArgumentNullException(nameof(options.Key));
             }
-            else
-            {
-                if (string.IsNullOrEmpty(sortedTxtParams.GetValue(APPID)))
-                {
-                    sortedTxtParams.Add(APPID, options.AppId);
-                }
-            }
-
-            sortedTxtParams.Add(SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
 
-            var content = QPayUtility.BuildContent(sortedTxtParams);
-            _logger.Log(options.LogLevel, "Request:{content}", content);
-
-            using (var client = _clientFactory.CreateClient())
-            {
-                var body = await client.DoPostAsync(request.GetRequestUrl(), content);
-                _logger.Log(options.LogLevel, "Response:{body}", body);
+            var sortedTxtParams = new QPayDictionary(request.GetParameters());
 
-                var parser = new QPayXmlParser<T>();
-                var rsp = parser.Parse(body);
+            request.PrimaryHandler(options, sortedTxtParams);
 
-                if (request.IsCheckResponseSign())
-                {
-                    CheckResponseSign(rsp, options);
-                }
+            var client = _httpClientFactory.CreateClient(nameof(QPayClient));
+            var body = await client.PostAsync(request.GetRequestUrl(), sortedTxtParams);
+            var parser = new QPayXmlParser<T>();
+            var rsp = parser.Parse(body);
 
-                return rsp;
+            if (request.GetNeedCheckSign())
+            {
+                CheckResponseSign(rsp, options);
             }
+
+            return rsp;
         }
 
         #endregion
 
         #region IQPayClient Members
 
-        public async Task<T> ExecuteAsync<T>(IQPayCertificateRequest<T> request, string certificateName) where T : QPayResponse
+        public async Task<T> ExecuteAsync<T>(IQPayCertRequest<T> request, QPayOptions options) where T : QPayResponse
         {
-            return await ExecuteAsync(request, null, certificateName);
-        }
+            if (options == null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
 
-        public async Task<T> ExecuteAsync<T>(IQPayCertificateRequest<T> request, string optionsName, string certificateName) where T : QPayResponse
-        {
-            var options = _optionsSnapshotAccessor.Get(optionsName);
-            var sortedTxtParams = new QPayDictionary(request.GetParameters())
+            if (string.IsNullOrEmpty(options.MchId))
             {
-                { MCHID, options.MchId },
-                { NONCE_STR, Guid.NewGuid().ToString("N") }
-            };
+                throw new ArgumentNullException(nameof(options.MchId));
+            }
 
-            if (request is QPayEPayB2CRequest)
+            if (string.IsNullOrEmpty(options.Key))
             {
-                if (string.IsNullOrEmpty(sortedTxtParams.GetValue(UIN)) && string.IsNullOrEmpty(sortedTxtParams.GetValue(APPID)))
-                {
-                    sortedTxtParams.Add(APPID, options.AppId);
-                }
+                throw new ArgumentNullException(nameof(options.Key));
             }
-            else
+
+            if (string.IsNullOrEmpty(options.Certificate))
             {
-                if (string.IsNullOrEmpty(sortedTxtParams.GetValue(APPID)))
-                {
-                    sortedTxtParams.Add(APPID, options.AppId);
-                }
+                throw new ArgumentNullException(nameof(options.Certificate));
             }
 
-            sortedTxtParams.Add(SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
-            var content = QPayUtility.BuildContent(sortedTxtParams);
-            _logger.Log(options.LogLevel, "Request:{content}", content);
-            using (var client = string.IsNullOrEmpty(certificateName) ? _clientFactory.CreateClient() : _clientFactory.CreateClient(certificateName))
+            var sortedTxtParams = new QPayDictionary(request.GetParameters());
+
+            request.PrimaryHandler(options, sortedTxtParams);
+
+            var hash = options.GetCertificateHash();
+            if (!_certificateManager.Contains(hash))
             {
-                var body = await client.DoPostAsync(request.GetRequestUrl(), content);
-                _logger.Log(options.LogLevel, "Response:{body}", body);
+                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);
 
-                var parser = new QPayXmlParser<T>();
-                var rsp = parser.Parse(body);
+                _certificateManager.TryAdd(hash, certificate);
+            }
 
-                if (request.IsCheckResponseSign())
-                {
-                    CheckResponseSign(rsp, options);
-                }
+            var client = _httpClientFactory.CreateClient(Prefix + hash);
+            var body = await client.PostAsync(request.GetRequestUrl(), sortedTxtParams);
+            var parser = new QPayXmlParser<T>();
+            var rsp = parser.Parse(body);
 
-                return rsp;
+            if (request.GetNeedCheckSign())
+            {
+                CheckResponseSign(rsp, options);
             }
+
+            return rsp;
         }
 
         #endregion
@@ -149,24 +120,24 @@ namespace Essensoft.AspNetCore.Payment.QPay
 
         private void CheckResponseSign(QPayResponse response, QPayOptions options)
         {
-            if (string.IsNullOrEmpty(response.Body))
+            if (string.IsNullOrEmpty(response.ResponseBody))
             {
                 throw new QPayException("sign check fail: Body is Empty!");
             }
 
-            if (response.Parameters.Count == 0)
+            if (response.ResponseParameters.Count == 0)
             {
                 throw new QPayException("sign check fail: Parameters is Empty!");
             }
 
-            if (response.Parameters["return_code"] == "SUCCESS")
+            if (response.ResponseParameters["return_code"] == "SUCCESS")
             {
-                if (!response.Parameters.TryGetValue("sign", out var sign))
+                if (!response.ResponseParameters.TryGetValue("sign", out var sign))
                 {
                     throw new QPayException("sign check fail: sign is Empty!");
                 }
 
-                var cal_sign = QPaySignature.SignWithKey(response.Parameters, options.Key);
+                var cal_sign = QPaySignature.SignWithKey(response.ResponseParameters, options.Key);
                 if (cal_sign != sign)
                 {
                     throw new QPayException("sign check fail: check Sign and Data Fail!");

+ 15 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayCode.cs

@@ -0,0 +1,15 @@
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public class QPayCode
+    {
+        /// <summary>
+        /// 成功
+        /// </summary>
+        public const string Success = "SUCCESS";
+
+        /// <summary>
+        /// 失败
+        /// </summary>
+        public const string Failure = "FAIL";
+    }
+}

+ 14 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayConsts.cs

@@ -0,0 +1,14 @@
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    internal class QPayConsts
+    {
+        public const string APPID = "appid";
+        public const string MCH_ID = "mch_id";
+        public const string NONCE_STR = "nonce_str";
+        public const string SIGN = "sign";
+
+        public const string UIN = "uin";
+
+        public const string QQAPPID = "qqappid";
+    }
+}

+ 10 - 5
src/Essensoft.AspNetCore.Payment.QPay/QPayDictionary.cs

@@ -1,12 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 字典。
-    /// </summary>
     public class QPayDictionary : SortedDictionary<string, string>
     {
+        private const string DATE_TIME_FORMAT = "yyyyMMddHHmmss";
+
         public QPayDictionary() { }
 
         public QPayDictionary(IDictionary<string, string> dictionary)
@@ -25,6 +25,11 @@ namespace Essensoft.AspNetCore.Payment.QPay
             {
                 strValue = (string)value;
             }
+            else if (value is DateTime?)
+            {
+                var dateTime = value as DateTime?;
+                strValue = dateTime.Value.ToString(DATE_TIME_FORMAT);
+            }
             else if (value is int?)
             {
                 strValue = (value as int?).Value.ToString();
@@ -39,7 +44,7 @@ namespace Essensoft.AspNetCore.Payment.QPay
             }
             else if (value is bool?)
             {
-                strValue = (value as bool?).Value.ToString().ToLower();
+                strValue = (value as bool?).Value.ToString().ToLowerInvariant();
             }
             else
             {

+ 0 - 3
src/Essensoft.AspNetCore.Payment.QPay/QPayException.cs

@@ -2,9 +2,6 @@
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 异常。
-    /// </summary>
     public class QPayException : Exception
     {
         public QPayException(string messages) : base(messages)

+ 40 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayHandlerBuilderFilter.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Net.Http;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
+using Microsoft.Extensions.Http;
+
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public class QPayHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
+    {
+        private readonly QPayCertificateManager _certificateManager;
+
+        public QPayHandlerBuilderFilter(QPayCertificateManager certificateManager)
+        {
+            _certificateManager = certificateManager;
+        }
+
+        public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
+        {
+            if (next == null)
+            {
+                throw new ArgumentNullException(nameof(next));
+            }
+
+            return builder =>
+            {
+                if (builder.PrimaryHandler is HttpClientHandler handler)
+                {
+                    if (builder.Name.Contains(QPayClient.Prefix))
+                    {
+                        var hash = builder.Name.RemovePreFix(QPayClient.Prefix);
+                        if (_certificateManager.TryGet(hash, out var certificate))
+                        {
+                            handler.ClientCertificates.Add(certificate);
+                        }
+                    }
+                }
+            };
+        }
+    }
+}

+ 0 - 3
src/Essensoft.AspNetCore.Payment.QPay/QPayNotify.cs

@@ -1,8 +1,5 @@
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 通知。
-    /// </summary>
     public abstract class QPayNotify : QPayObject
     {
     }

+ 15 - 24
src/Essensoft.AspNetCore.Payment.QPay/QPayNotifyClient.cs

@@ -5,43 +5,34 @@ using System.Threading.Tasks;
 using Essensoft.AspNetCore.Payment.QPay.Parser;
 using Essensoft.AspNetCore.Payment.QPay.Utility;
 using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 通知解析客户端。
-    /// </summary>
     public class QPayNotifyClient : IQPayNotifyClient
     {
-        private readonly ILogger _logger;
-        private readonly IOptionsSnapshot<QPayOptions> _optionsSnapshotAccessor;
-
         #region QPayNotifyClient Constructors
 
-        public QPayNotifyClient(
-            ILogger<QPayNotifyClient> logger,
-            IOptionsSnapshot<QPayOptions> optionsAccessor)
+        public QPayNotifyClient()
         {
-            _logger = logger;
-            _optionsSnapshotAccessor = optionsAccessor;
         }
 
         #endregion
 
         #region IQPayNotifyClient Members
 
-        public async Task<T> ExecuteAsync<T>(HttpRequest request) where T : QPayNotify
+        public async Task<T> ExecuteAsync<T>(HttpRequest request, QPayOptions options) where T : QPayNotify
         {
-            return await ExecuteAsync<T>(request, null);
-        }
+            if (request == null)
+            {
+                throw new ArgumentNullException(nameof(request));
+            }
+
+            if (string.IsNullOrEmpty(options.Key))
+            {
+                throw new ArgumentNullException(nameof(options.Key));
+            }
 
-        public async Task<T> ExecuteAsync<T>(HttpRequest request, string optionsName) where T : QPayNotify
-        {
-            var options = _optionsSnapshotAccessor.Get(optionsName);
             var body = await new StreamReader(request.Body, Encoding.UTF8).ReadToEndAsync();
-            _logger.Log(options.LogLevel, "Request:{body}", body);
 
             var parser = new QPayXmlParser<T>();
             var rsp = parser.Parse(body);
@@ -55,22 +46,22 @@ namespace Essensoft.AspNetCore.Payment.QPay
 
         private void CheckNotifySign(QPayNotify response, QPayOptions options)
         {
-            if (string.IsNullOrEmpty(response.Body))
+            if (string.IsNullOrEmpty(response.ResponseBody))
             {
                 throw new QPayException("sign check fail: Body is Empty!");
             }
 
-            if (response.Parameters.Count == 0)
+            if (response.ResponseParameters.Count == 0)
             {
                 throw new QPayException("sign check fail: Parameters is Empty!");
             }
 
-            if (!response.Parameters.TryGetValue("sign", out var sign))
+            if (!response.ResponseParameters.TryGetValue("sign", out var sign))
             {
                 throw new QPayException("sign check fail: sign is Empty!");
             }
 
-            var cal_sign = QPaySignature.SignWithKey(response.Parameters, options.Key);
+            var cal_sign = QPaySignature.SignWithKey(response.ResponseParameters, options.Key);
             if (cal_sign != sign)
             {
                 throw new QPayException("sign check fail: check Sign and Data Fail!");

+ 5 - 6
src/Essensoft.AspNetCore.Payment.QPay/QPayObject.cs

@@ -1,23 +1,22 @@
-using System.Xml.Serialization;
+using System;
+using System.Xml.Serialization;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 基础对象。
-    /// </summary>
+    [Serializable]
     public abstract class QPayObject
     {
         /// <summary>
         /// 原始内容
         /// </summary>
         [XmlIgnore]
-        public string Body { get; set; }
+        public string ResponseBody { get; set; }
 
         /// <summary>
         /// 原始参数
         /// </summary>
         [XmlIgnore]
-        public QPayDictionary Parameters { get; internal set; }
+        public QPayDictionary ResponseParameters { get; internal set; }
 
         /// <summary>
         /// 处理 _$n

+ 27 - 8
src/Essensoft.AspNetCore.Payment.QPay/QPayOptions.cs

@@ -1,30 +1,49 @@
-using Microsoft.Extensions.Logging;
+using Essensoft.AspNetCore.Payment.Security;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 选项。
-    /// </summary>
     public class QPayOptions
     {
+        private string certificatePassword;
+
         /// <summary>
         /// 应用ID
         /// </summary>
         public string AppId { get; set; }
 
         /// <summary>
-        /// 商户号
+        /// QQ钱包 商户号
         /// </summary>
         public string MchId { get; set; }
 
         /// <summary>
-        /// API秘钥
+        /// QQ钱包 API秘钥
         /// </summary>
         public string Key { get; set; }
 
         /// <summary>
-        /// 日志等级
+        /// QQ钱包 API证书(文件名/文件的Base64编码)
         /// </summary>
-        public LogLevel LogLevel { get; set; } = LogLevel.Information;
+        public string Certificate { get; set; }
+
+        /// <summary>
+        /// QQ钱包 API证书密码(默认为商户号)
+        /// </summary>
+        public string CertificatePassword
+        {
+            get
+            {
+                return string.IsNullOrEmpty(certificatePassword) ? MchId : certificatePassword;
+            }
+            set
+            {
+                certificatePassword = value;
+            }
+        }
+
+        public string GetCertificateHash()
+        {
+            return MD5.Compute(Certificate);
+        }
     }
 }

+ 0 - 3
src/Essensoft.AspNetCore.Payment.QPay/QPayResponse.cs

@@ -1,8 +1,5 @@
 namespace Essensoft.AspNetCore.Payment.QPay
 {
-    /// <summary>
-    /// QPay 响应。
-    /// </summary>
     public abstract class QPayResponse : QPayObject
     {
     }

+ 10 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayTradeStates.cs

@@ -0,0 +1,10 @@
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public class QPayTradeStates
+    {
+        /// <summary>
+        /// 成功
+        /// </summary>
+        public const string Success = "SUCCESS";
+    }
+}

+ 20 - 0
src/Essensoft.AspNetCore.Payment.QPay/QPayTradeType.cs

@@ -0,0 +1,20 @@
+namespace Essensoft.AspNetCore.Payment.QPay
+{
+    public class QPayTradeType
+    {
+        /// <summary>
+        /// 公众号支付
+        /// </summary>
+        public const string JsApi = "JSAPI";
+
+        /// <summary>
+        /// 扫码支付
+        /// </summary>
+        public const string Native = "NATIVE";
+
+        /// <summary>
+        /// App支付
+        /// </summary>
+        public const string App = "APP";
+    }
+}

+ 13 - 9
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayCloseOrderRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -8,11 +9,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
     /// </summary>
     public class QPayCloseOrderRequest : IQPayRequest<QPayCloseOrderResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -31,7 +27,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// <summary>
         /// 订单金额
         /// </summary>
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         #region IQPayRequest Members
 
@@ -44,7 +40,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "out_trade_no", OutTradeNo },
@@ -53,7 +48,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 16 - 12
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayB2CRequest.cs

@@ -1,23 +1,19 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
     /// <summary>
     /// 企业付款 - 企业付款到余额
     /// </summary>
-    public class QPayEPayB2CRequest : IQPayCertificateRequest<QPayEPayB2CResponse>
+    public class QPayEPayB2CRequest : IQPayCertRequest<QPayEPayB2CResponse>
     {
         /// <summary>
         /// 字符集
         /// </summary>
         public string InputCharset { get; set; } = "UTF-8";
 
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 用户opeind
         /// </summary>
@@ -41,7 +37,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// <summary>
         /// 付款金额
         /// </summary>
-        public string TotalFee { get; set; }
+        public int TotalFee { get; set; }
 
         /// <summary>
         /// 付款备注
@@ -76,7 +72,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// <summary>
         /// IP地址
         /// </summary>
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         /// <summary>
         /// 用户到账结果通知
@@ -95,7 +91,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             var parameters = new QPayDictionary
             {
                 { "input_charset", InputCharset },
-                { "appid", AppId },
                 { "openid", OpenId },
                 { "uin", Uin },
                 { "out_trade_no", OutTradeNo },
@@ -107,13 +102,22 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
                 { "check_real_name", CheckRealName },
                 { "op_user_id", OpUserId },
                 { "op_user_passwd", OpUserPasswd },
-                { "spbill_create_ip", SpbillCreateIp },
+                { "spbill_create_ip", SpBillCreateIp },
                 { "notify_url", NotifyUrl },
             };
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return false;
         }

+ 11 - 2
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayQueryRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -35,7 +36,15 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return false;
         }

+ 11 - 2
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayEPayStatementDownRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -29,7 +30,15 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return false;
         }

+ 47 - 0
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchDownListFileRequest.cs

@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Request
+{
+    /// <summary>
+    /// 现金红包 - 对账单下载
+    /// </summary>
+    public class QPayHbMchDownListFileRequest : IQPayRequest<QPayHbMchDownListFileResponse>
+    {
+        /// <summary>
+        /// 对账单时间
+        /// </summary>
+        public string Date { get; set; }
+
+        #region IQPayRequest Members
+
+        public string GetRequestUrl()
+        {
+            return "https://api.qpay.qq.com/cgi-bin/hongbao/qpay_hb_mch_down_list_file.cgi";
+        }
+
+        public IDictionary<string, string> GetParameters()
+        {
+            var parameters = new QPayDictionary
+            {
+                { "date", Date },
+            };
+            return parameters;
+        }
+
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
+        {
+            return false;
+        }
+
+        #endregion
+    }
+}

+ 66 - 0
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchListQueryRequest.cs

@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Request
+{
+    /// <summary>
+    /// 现金红包 - 红包详情查询
+    /// </summary>
+    public class QPayHbMchListQueryRequest : IQPayRequest<QPayHbMchListQueryResponse>
+    {
+        /// <summary>
+        /// 发起方式
+        /// </summary>
+        public long SendType { get; set; }
+
+        /// <summary>
+        /// 商户订单号
+        /// </summary>
+        public string MchBillNo { get; set; }
+
+        /// <summary>
+        /// 红包单号
+        /// </summary>
+        public string ListId { get; set; }
+
+        /// <summary>
+        /// 子商户id
+        /// </summary>
+        public string SubMchId { get; set; }
+
+        #region IQPayRequest Members
+
+        public string GetRequestUrl()
+        {
+            return "https://qpay.qq.com/cgi-bin/mch_query/qpay_hb_mch_list_query.cgi";
+        }
+
+        public IDictionary<string, string> GetParameters()
+        {
+            var parameters = new QPayDictionary
+            {
+                { "send_type", SendType },
+                { "mch_billno", MchBillNo },
+                { "listid", ListId },
+                { "sub_mch_id", SubMchId },
+            };
+            return parameters;
+        }
+
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
+        {
+            return false;
+        }
+
+        #endregion
+    }
+}

+ 127 - 0
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayHbMchSendRequest.cs

@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Request
+{
+    /// <summary>
+    /// 现金红包 - 创建现金红包
+    /// </summary>
+    public class QPayHbMchSendRequest : IQPayCertRequest<QPayHbMchSendResponse>
+    {
+        /// <summary>
+        /// 字符集
+        /// </summary>
+        public string Charset { get; set; } = "UTF-8";
+
+        /// <summary>
+        /// 商户订单号
+        /// </summary>
+        public string MchBillNo { get; set; }
+
+        /// <summary>
+        /// 商户名称
+        /// </summary>
+        public string MchName { get; set; }
+
+        /// <summary>
+        /// 接收者openid
+        /// </summary>
+        public string ReOpenId { get; set; }
+
+        /// <summary>
+        /// 发放总金额
+        /// </summary>
+        public long TotalAmount { get; set; }
+
+        /// <summary>
+        /// 红包发放总人数
+        /// </summary>
+        public int TotalNum { get; set; }
+
+        /// <summary>
+        /// 红包祝福语
+        /// </summary>
+        public string Wishing { get; set; }
+
+        /// <summary>
+        /// 活动名称
+        /// </summary>
+        public string ActName { get; set; }
+
+        /// <summary>
+        /// 商户logo图片ID
+        /// </summary>
+        public int IconId { get; set; }
+
+        /// <summary>
+        /// 商户banner图片ID
+        /// </summary>
+        public int BannerId { get; set; }
+
+        /// <summary>
+        /// 红包领取结果通知
+        /// </summary>
+        public string NotifyUrl { get; set; }
+
+        /// <summary>
+        /// 是否发送公众号消息链接
+        /// </summary>
+        public int NotSendMsg { get; set; }
+
+        /// <summary>
+        /// 最小红包金额
+        /// </summary>
+        public long MinValue { get; set; }
+
+        /// <summary>
+        /// 最大红包金额
+        /// </summary>
+        public long MaxValue { get; set; }
+
+        #region IQPayRequest Members
+
+        public string GetRequestUrl()
+        {
+            return "https://api.qpay.qq.com/cgi-bin/hongbao/qpay_hb_mch_send.cgi";
+        }
+
+        public IDictionary<string, string> GetParameters()
+        {
+            var parameters = new QPayDictionary
+            {
+                { "charset", Charset },
+                { "mch_billno", MchBillNo },
+                { "mch_name", MchName },
+                { "re_openid", ReOpenId },
+                { "total_amount", TotalAmount },
+                { "total_num", TotalNum },
+                { "wishing", Wishing },
+                { "act_name", ActName },
+                { "icon_id", IconId },
+                { "banner_id", BannerId },
+                { "notify_url", NotifyUrl },
+                { "not_send_msg", NotSendMsg },
+                { "min_value", MinValue },
+                { "max_value", MaxValue },
+            };
+            return parameters;
+        }
+
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+            sortedTxtParams.Add(QPayConsts.QQAPPID, options.AppId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
+        {
+            return false;
+        }
+
+        #endregion
+    }
+}

+ 14 - 10
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayMicroPayRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -8,11 +9,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
     /// </summary>
     public class QPayMicroPayRequest : IQPayRequest<QPayMicroPayResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -51,7 +47,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// <summary>
         /// 终端IP
         /// </summary>
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         /// <summary>
         /// 支付方式限制
@@ -94,7 +90,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "body", Body },
@@ -102,7 +97,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
                 { "out_trade_no", OutTradeNo },
                 { "fee_type", FeeType },
                 { "total_fee", TotalFee },
-                { "spbill_create_ip", SpbillCreateIp },
+                { "spbill_create_ip", SpBillCreateIp },
                 { "limit_pay", LimitPay },
                 { "promotion_tag", PromotionTag },
                 { "notify_url", NotifyUrl },
@@ -113,7 +108,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 12 - 8
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayOrderQueryRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -8,11 +9,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
     /// </summary>
     public class QPayOrderQueryRequest : IQPayRequest<QPayOrderQueryResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -44,7 +40,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "transaction_id", TransactionId },
@@ -53,7 +48,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 12 - 8
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayRefundQueryRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -8,11 +9,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
     /// </summary>
     public class QPayRefundQueryRequest : IQPayRequest<QPayRefundQueryResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -54,7 +50,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "refund_id", RefundId },
@@ -65,7 +60,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 13 - 9
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayRefundRequest.cs

@@ -1,18 +1,14 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
     /// <summary>
     /// 申请退款
     /// </summary>
-    public class QPayRefundRequest : IQPayCertificateRequest<QPayRefundResponse>
+    public class QPayRefundRequest : IQPayCertRequest<QPayRefundResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -69,7 +65,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "transaction_id", TransactionId },
@@ -83,7 +78,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 13 - 9
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayReverseRequest.cs

@@ -1,18 +1,14 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
     /// <summary>
     /// 撤销订单
     /// </summary>
-    public class QPayReverseRequest : IQPayCertificateRequest<QPayReverseResponse>
+    public class QPayReverseRequest : IQPayCertRequest<QPayReverseResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -49,7 +45,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "out_trade_no", OutTradeNo },
@@ -59,7 +54,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 13 - 3
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayStatementDownRequest.cs → src/Essensoft.AspNetCore.Payment.QPay/Request/QPaySpDownloadStatementDownRequest.cs

@@ -1,12 +1,13 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
     /// <summary>
     /// 对账单下载
     /// </summary>
-    public class QPayStatementDownRequest : IQPayRequest<QPayStatementDownResponse>
+    public class QPaySpDownloadStatementDownRequest : IQPayRequest<QPaySpDownloadStatementDownResponse>
     {
         /// <summary>
         /// 对账单时间
@@ -41,7 +42,16 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return false;
         }

+ 21 - 11
src/Essensoft.AspNetCore.Payment.QPay/Request/QPayUnifiedOrderRequest.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Essensoft.AspNetCore.Payment.QPay.Response;
+using Essensoft.AspNetCore.Payment.QPay.Utility;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Request
 {
@@ -8,11 +9,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
     /// </summary>
     public class QPayUnifiedOrderRequest : IQPayRequest<QPayUnifiedOrderResponse>
     {
-        /// <summary>
-        /// 应用ID
-        /// </summary>
-        public string AppId { get; set; }
-
         /// <summary>
         /// 子商户应用ID
         /// </summary>
@@ -51,7 +47,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// <summary>
         /// 终端IP
         /// </summary>
-        public string SpbillCreateIp { get; set; }
+        public string SpBillCreateIp { get; set; }
 
         /// <summary>
         /// 交易起始时间
@@ -93,6 +89,11 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         /// </summary>
         public string DeviceInfo { get; set; }
 
+        /// <summary>
+        /// 小程序跳转地址和参数
+        /// </summary>
+        public string MiniAppParam { get; set; }
+
         #region IQPayRequest Members
 
         public string GetRequestUrl()
@@ -104,7 +105,6 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
         {
             var parameters = new QPayDictionary
             {
-                { "appid", AppId },
                 { "sub_appid", SubAppId },
                 { "sub_mch_id", SubMchId },
                 { "body", Body },
@@ -112,7 +112,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
                 { "out_trade_no", OutTradeNo },
                 { "fee_type", FeeType },
                 { "total_fee", TotalFee },
-                { "spbill_create_ip", SpbillCreateIp },
+                { "spbill_create_ip", SpBillCreateIp },
                 { "time_start", TimeStart },
                 { "time_expire", TimeExpire },
                 { "limit_pay", LimitPay },
@@ -120,12 +120,22 @@ namespace Essensoft.AspNetCore.Payment.QPay.Request
                 { "promotion_tag", PromotionTag },
                 { "trade_type", TradeType },
                 { "notify_url", NotifyUrl },
-                { "device_info", DeviceInfo }
+                { "device_info", DeviceInfo },
+                { "mini_app_param",  MiniAppParam },
             };
             return parameters;
         }
 
-        public bool IsCheckResponseSign()
+        public void PrimaryHandler(QPayOptions options, QPayDictionary sortedTxtParams)
+        {
+            sortedTxtParams.Add(QPayConsts.NONCE_STR, QPayUtility.GenerateNonceStr());
+            sortedTxtParams.Add(QPayConsts.APPID, options.AppId);
+            sortedTxtParams.Add(QPayConsts.MCH_ID, options.MchId);
+
+            sortedTxtParams.Add(QPayConsts.SIGN, QPaySignature.SignWithKey(sortedTxtParams, options.Key));
+        }
+
+        public bool GetNeedCheckSign()
         {
             return true;
         }

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayEPayQueryResponse.cs

@@ -100,7 +100,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Response
         /// </summary>
         [XmlElement("appid")]
         public string AppId { get; set; }
-       
+
         /// <summary>
         /// 用户标识	
         /// </summary>

+ 9 - 0
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchDownListFileResponse.cs

@@ -0,0 +1,9 @@
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Response
+{
+    [XmlRoot("xml")]
+    public class QPayHbMchDownListFileResponse : QPayResponse
+    {
+    }
+}

+ 72 - 0
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchListQueryResponse.cs

@@ -0,0 +1,72 @@
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Response
+{
+    [XmlRoot("xml")]
+    public class QPayHbMchListQueryResponse : QPayResponse
+    {
+        /// <summary>
+        /// 返回错误码,0表示成功。
+        /// </summary>
+        [XmlElement("result")]
+        public long Result { get; set; }
+
+        /// <summary>
+        /// 对应result的说明,Ok表示成功
+        /// </summary>
+        [XmlElement("res_info")]
+        public string ResInfo { get; set; }
+
+        /// <summary>
+        /// 红包单号
+        /// </summary>
+        [XmlElement("listid")]
+        public string ListId { get; set; }
+
+        /// <summary>
+        /// 状态
+        /// 1: 已支付
+        /// 2: 已抢完
+        /// 3: 已过期
+        /// 4: 已退款(含部分退款)
+        /// </summary>
+        [XmlElement("state")]
+        public int State { get; set; }
+
+        /// <summary>
+        /// 红包总个数
+        /// </summary>
+        [XmlElement("total_num")]
+        public long TotalNum { get; set; }
+
+        /// <summary>
+        /// 领取个数
+        /// </summary>
+        [XmlElement("recv_num")]
+        public long RecvNum { get; set; }
+
+        /// <summary>
+        /// 红包总金额	
+        /// </summary>
+        [XmlElement("total_amount")]
+        public long TotalAmount { get; set; }
+
+        /// <summary>
+        /// 领取金额
+        /// </summary>
+        [XmlElement("recv_amount")]
+        public long RecvAmount { get; set; }
+
+        /// <summary>
+        /// 领取详情数组
+        /// </summary>
+        [XmlElement("recv_details")]
+        public string RecvDetails { get; set; }
+
+        /// <summary>
+        /// 领取人QQ
+        /// </summary>
+        [XmlElement("uin")]
+        public string Uin { get; set; }
+    }
+}

+ 38 - 0
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayHbMchSendResponse.cs

@@ -0,0 +1,38 @@
+using System.Xml.Serialization;
+
+namespace Essensoft.AspNetCore.Payment.QPay.Response
+{
+    [XmlRoot("xml")]
+    public class QPayHbMchSendResponse : QPayResponse
+    {
+        /// <summary>
+        /// 返回状态码
+        /// </summary>
+        [XmlElement("return_code")]
+        public string ReturnCode { get; set; }
+
+        /// <summary>
+        /// 返回信息
+        /// </summary>
+        [XmlElement("return_msg")]
+        public string ReturnMsg { get; set; }
+
+        /// <summary>
+        /// 原始错误码
+        /// </summary>
+        [XmlElement("retcode")]
+        public string RetCode { get; set; }
+
+        /// <summary>
+        /// 原始错误信息
+        /// </summary>
+        [XmlElement("retmsg")]
+        public string RetMsg { get; set; }
+
+        /// <summary>
+        /// 红包单号
+        /// </summary>
+        [XmlElement("listid")]
+        public string ListId { get; set; }
+    }
+}

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayOrderQueryResponse.cs

@@ -42,7 +42,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Response
         public string SubAppId { get; set; }
 
         /// <summary>
-        /// 商户号ID
+        /// 商户号
         /// </summary>
         [XmlElement("mch_id")]
         public string MchId { get; set; }

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayRefundQueryResponse.cs

@@ -125,7 +125,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Response
         internal override void Execute()
         {
             var parser = new QPayListPropertyParser();
-            RefundInfos = parser.Parse<RefundInfo, object>(Parameters);
+            RefundInfos = parser.Parse<RefundInfo>(ResponseParameters);
         }
     }
 }

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayRefundResponse.cs

@@ -105,7 +105,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Response
         /// 商户退款单号
         /// </summary>
         [XmlElement("out_refund_no")]
-        public int OutRefundNo { get; set; }
+        public string OutRefundNo { get; set; }
 
         /// <summary>
         /// QQ钱包退款单号

+ 1 - 1
src/Essensoft.AspNetCore.Payment.QPay/Response/QPayStatementDownResponse.cs → src/Essensoft.AspNetCore.Payment.QPay/Response/QPaySpDownloadStatementDownResponse.cs

@@ -3,7 +3,7 @@
 namespace Essensoft.AspNetCore.Payment.QPay.Response
 {
     [XmlRoot("xml")]
-    public class QPayStatementDownResponse : QPayResponse
+    public class QPaySpDownloadStatementDownResponse : QPayResponse
     {
         /// <summary>
         /// 返回状态码

+ 10 - 2
src/Essensoft.AspNetCore.Payment.QPay/ServiceCollectionExtensions.cs

@@ -1,5 +1,7 @@
 using System;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Http;
 
 namespace Essensoft.AspNetCore.Payment.QPay
 {
@@ -15,8 +17,14 @@ namespace Essensoft.AspNetCore.Payment.QPay
             this IServiceCollection services,
             Action<QPayOptions> setupAction)
         {
-            services.AddScoped<IQPayClient, QPayClient>();
-            services.AddScoped<IQPayNotifyClient, QPayNotifyClient>();
+            services.AddHttpClient(nameof(QPayClient));
+
+            services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, QPayHandlerBuilderFilter>());
+
+            services.AddSingleton<QPayCertificateManager>();
+            services.AddSingleton<IQPayClient, QPayClient>();
+            services.AddSingleton<IQPayNotifyClient, QPayNotifyClient>();
+
             if (setupAction != null)
             {
                 services.Configure(setupAction);

+ 9 - 11
src/Essensoft.AspNetCore.Payment.QPay/Utility/HttpClientExtensions.cs

@@ -1,12 +1,10 @@
-using System.Net.Http;
+using System.Collections.Generic;
+using System.Net.Http;
 using System.Text;
 using System.Threading.Tasks;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Utility
 {
-    /// <summary>
-    /// HTTP客户端扩展。
-    /// </summary>
     public static class HttpClientExtensions
     {
         /// <summary>
@@ -14,15 +12,15 @@ namespace Essensoft.AspNetCore.Payment.QPay.Utility
         /// </summary>
         /// <param name="client">HttpClient</param>
         /// <param name="url">请求地址</param>
-        /// <param name="content">请求内容</param>
-        /// <returns>HTTP响应内容</returns>
-        public static async Task<string> DoPostAsync(this HttpClient client, string url, string content)
+        /// <param name="textParams">请求参数</param>
+        /// <returns>响应内容</returns>
+        public static async Task<string> PostAsync(this HttpClient client, string url, IDictionary<string, string> textParams)
         {
-            using (var requestContent = new StringContent(content, Encoding.UTF8, "application/xml"))
-            using (var response = await client.PostAsync(url, requestContent))
-            using (var responseContent = response.Content)
+            using (var reqContent = new StringContent(QPayUtility.BuildContent(textParams), Encoding.UTF8, "application/xml"))
+            using (var resp = await client.PostAsync(url, reqContent))
+            using (var respContent = resp.Content)
             {
-                return await responseContent.ReadAsStringAsync();
+                return await respContent.ReadAsStringAsync();
             }
         }
     }

+ 3 - 7
src/Essensoft.AspNetCore.Payment.QPay/Utility/QPaySignature.cs

@@ -1,15 +1,11 @@
-using System.Collections.Generic;
-using System.Text;
+using System.Text;
 using Essensoft.AspNetCore.Payment.Security;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Utility
 {
-    /// <summary>
-    /// QPay 签名类。
-    /// </summary>
     public class QPaySignature
     {
-        public static string SignWithKey(SortedDictionary<string, string> dictionary, string key)
+        public static string SignWithKey(QPayDictionary dictionary, string key)
         {
             var content = new StringBuilder();
             foreach (var iter in dictionary)
@@ -20,7 +16,7 @@ namespace Essensoft.AspNetCore.Payment.QPay.Utility
                 }
             }
             var signContent = content.Append("key=").Append(key).ToString();
-            return MD5.Compute(signContent).ToUpper();
+            return MD5.Compute(signContent).ToUpperInvariant();
         }
     }
 }

+ 57 - 6
src/Essensoft.AspNetCore.Payment.QPay/Utility/QPayUtility.cs

@@ -1,18 +1,16 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Text;
 
 namespace Essensoft.AspNetCore.Payment.QPay.Utility
 {
-    /// <summary>
-    /// QPay 工具类。
-    /// </summary>
     public static class QPayUtility
     {
         /// <summary>
-        /// 组装普通文本请求参数。
+        /// 组装XML格式请求参数。
         /// </summary>
         /// <param name="dictionary">Key-Value形式请求参数字典</param>
-        /// <returns>URL编码后的请求数据</returns>
+        /// <returns>XML格式的请求数据</returns>
         public static string BuildContent(IDictionary<string, string> dictionary)
         {
             var content = new StringBuilder("<xml>");
@@ -25,5 +23,58 @@ namespace Essensoft.AspNetCore.Payment.QPay.Utility
             }
             return content.Append("</xml>").ToString();
         }
+
+        public static string GenerateNonceStr()
+        {
+            return Guid.NewGuid().ToString("N");
+        }
+
+        public static string RemovePreFix(this string str, params string[] preFixes)
+        {
+            if (str == null)
+            {
+                return null;
+            }
+
+            if (str == string.Empty)
+            {
+                return string.Empty;
+            }
+
+            if (preFixes.IsNullOrEmpty())
+            {
+                return str;
+            }
+
+            foreach (var preFix in preFixes)
+            {
+                if (str.StartsWith(preFix))
+                {
+                    return str.Right(str.Length - preFix.Length);
+                }
+            }
+
+            return str;
+        }
+
+        public static bool IsNullOrEmpty<T>(this ICollection<T> source)
+        {
+            return source == null || source.Count <= 0;
+        }
+
+        public static string Right(this string str, int len)
+        {
+            if (str == null)
+            {
+                throw new ArgumentNullException("str");
+            }
+
+            if (str.Length < len)
+            {
+                throw new ArgumentException("len argument can not be bigger than given string's length!");
+            }
+
+            return str.Substring(str.Length - len, len);
+        }
     }
 }