feat(tenpaybusiness): 导入项目

This commit is contained in:
RHQYZ 2022-05-09 19:28:47 +08:00 committed by GitHub
parent 26b9c17a7d
commit a0a90f4851
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 4606 additions and 95 deletions

View File

@ -16,6 +16,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3", "src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj", "{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayBusiness", "src\SKIT.FlurlHttpClient.Wechat.TenpayBusiness\SKIT.FlurlHttpClient.Wechat.TenpayBusiness.csproj", "{A0951B55-4C90-450C-A165-FBDD2096D668}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Ads", "src\SKIT.FlurlHttpClient.Wechat.Ads\SKIT.FlurlHttpClient.Wechat.Ads.csproj", "{7F155EFB-152F-4798-9984-99102B21D2F8}"
@ -30,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj", "{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests.csproj", "{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Ads.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj", "{561E0BFB-7817-41FE-BAF2-D78817679AC1}"
@ -62,6 +66,10 @@ Global
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.Build.0 = Release|Any CPU
{A0951B55-4C90-450C-A165-FBDD2096D668}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0951B55-4C90-450C-A165-FBDD2096D668}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0951B55-4C90-450C-A165-FBDD2096D668}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0951B55-4C90-450C-A165-FBDD2096D668}.Release|Any CPU.Build.0 = Release|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -86,6 +94,10 @@ Global
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.Build.0 = Release|Any CPU
{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A}.Release|Any CPU.Build.0 = Release|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -118,12 +130,14 @@ Global
{082C1F69-7932-473F-A700-49584371BE8C} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{18DEF654-1EDF-46C7-8430-685D6236E9C5} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{A0951B55-4C90-450C-A165-FBDD2096D668} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{AAE2E9BC-4D0B-4495-8825-DF8C405DB4A0} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{574A567A-6D2C-49F6-9A98-0133CA9B007D} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{3ABC7209-CA5E-42C1-8EA2-1724C16EFC0A} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{561E0BFB-7817-41FE-BAF2-D78817679AC1} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{9689135B-44BA-4D55-8663-7C669BAFE066} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}

View File

@ -0,0 +1,12 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class WechatTenpayBusinessSensitiveAttribute : Attribute
{
public WechatTenpayBusinessSensitiveAttribute()
{
}
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class WechatTenpayBusinessSensitivePropertyAttribute : Attribute
{
public WechatTenpayBusinessSensitivePropertyAttribute()
{
}
}
}

View File

@ -0,0 +1,7 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Constants
{
public static class EncryptionAlgorithms
{
public const string RSA_OAEP_WITH_SM4_128_CBC = "RSA_OAEP_with_SM4_128_CBC";
}
}

View File

@ -0,0 +1,7 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Constants
{
internal static class FormDataFields
{
public const string FORMDATA_META = "meta";
}
}

View File

@ -0,0 +1,10 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Constants
{
public static class SignAlgorithms
{
/// <summary>
/// SHA256withRSA。
/// </summary>
public const string SHA245_WITH_RSA = "SHA256withRSA";
}
}

View File

@ -0,0 +1,78 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Events
{
/// <summary>
/// <para>表示 pay.created 通知的数据。</para>
/// <para>表示 pay.bank_accept 通知的数据。</para>
/// <para>表示 pay.succeeded 通知的数据。</para>
/// <para>表示 pay.failed 通知的数据。</para>
/// <para>表示 pay.revoked 通知的数据。</para>
/// </summary>
public class MSEPayPaymentEvent : WechatTenpayBusinessEvent<MSEPayPaymentEvent.Types.EventContent>
{
public static class Types
{
public class EventContent
{
/// <summary>
/// 获取或设置平台支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("out_payment_id")]
public string OutPaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("payment_id")]
public string PaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置支付金额(单位:分)。
/// </summary>
[Newtonsoft.Json.JsonProperty("amount")]
[System.Text.Json.Serialization.JsonPropertyName("amount")]
public int Amount { get; set; }
/// <summary>
/// 获取或设置付款方 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("purchaser_id")]
[System.Text.Json.Serialization.JsonPropertyName("purchaser_id")]
public string PurchaserId { get; set; } = default!;
/// <summary>
/// 获取或设置收款方企业 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("bargainor_ent_id")]
[System.Text.Json.Serialization.JsonPropertyName("bargainor_ent_id")]
public string BargainorEnterpriseId { get; set; } = default!;
/// <summary>
/// 获取或设置订单状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置失败原因。
/// </summary>
[Newtonsoft.Json.JsonProperty("failed_reason")]
[System.Text.Json.Serialization.JsonPropertyName("failed_reason")]
public string? FailedReason { get; set; }
/// <summary>
/// 获取或设置支付成功时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("pay_succ_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339NullableDateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("pay_succ_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339NullableDateTimeOffsetConverter))]
public DateTimeOffset? PaySuccessTime { get; set; }
}
}
}
}

View File

@ -0,0 +1,89 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Events
{
/// <summary>
/// <para>表示 product.application.finish 通知的数据。</para>
/// </summary>
public class ProductApplicationEvent : WechatTenpayBusinessEvent<ProductApplicationEvent.Types.EventContent>
{
public static class Types
{
public class EventContent
{
public static class Types
{
public class Product
{
public static class Types
{
public class Account
{
/// <summary>
/// 获取或设置企业账户 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_acct_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_acct_id")]
public string EnterpriseAccountId { get; set; } = default!;
}
}
/// <summary>
/// 获取或设置产品名称。
/// </summary>
[Newtonsoft.Json.JsonProperty("product_name")]
[System.Text.Json.Serialization.JsonPropertyName("product_name")]
public string ProductName { get; set; } = default!;
/// <summary>
/// 获取或设置开通状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置账户列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("accounts")]
[System.Text.Json.Serialization.JsonPropertyName("accounts")]
public Types.Account[]? AccountList { get; set; }
}
}
/// <summary>
/// 获取或设置业务申请编号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_request_no")]
[System.Text.Json.Serialization.JsonPropertyName("out_request_no")]
public string OutRequestNumber { get; set; } = default!;
/// <summary>
/// 获取或设置微企付开户申请单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("request_no")]
[System.Text.Json.Serialization.JsonPropertyName("request_no")]
public string RequestNumber { get; set; } = default!;
/// <summary>
/// 获取或设置企业 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_id")]
public string EnterpriseId { get; set; } = default!;
/// <summary>
/// 获取或设置开通状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置开通产品列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("product_details")]
[System.Text.Json.Serialization.JsonPropertyName("product_details")]
public Types.Product[] ProductList { get; set; } = default!;
}
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Exceptions
{
public class WechatTenpayBusinessEventVerificationException : WechatTenpayBusinessException
{
/// <inheritdoc/>
internal WechatTenpayBusinessEventVerificationException()
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessEventVerificationException(string message)
: base(message)
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessEventVerificationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Exceptions
{
public class WechatTenpayBusinessRequestEncryptionException : WechatTenpayBusinessException
{
/// <inheritdoc/>
internal WechatTenpayBusinessRequestEncryptionException()
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessRequestEncryptionException(string message)
: base(message)
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessRequestEncryptionException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Exceptions
{
public class WechatTenpayBusinessRequestSignatureException : WechatTenpayBusinessException
{
/// <inheritdoc/>
internal WechatTenpayBusinessRequestSignatureException()
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessRequestSignatureException(string message)
: base(message)
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessRequestSignatureException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Exceptions
{
public class WechatTenpayBusinessResponseDecryptionException : WechatTenpayBusinessException
{
/// <inheritdoc/>
internal WechatTenpayBusinessResponseDecryptionException()
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessResponseDecryptionException(string message)
: base(message)
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessResponseDecryptionException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Exceptions
{
public class WechatTenpayBusinessResponseVerificationException : WechatTenpayBusinessException
{
/// <inheritdoc/>
internal WechatTenpayBusinessResponseVerificationException()
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessResponseVerificationException(string message)
: base(message)
{
}
/// <inheritdoc/>
internal WechatTenpayBusinessResponseVerificationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
internal static class WechatTenpayBusinessClientSignExtensions
{
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strAuthorization, string strBody)
{
return VerifySignature(client, strAuthorization, strBody, out _);
}
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strAuthorization, string strBody, out Exception? error)
{
if (!string.IsNullOrEmpty(strAuthorization))
{
try
{
IDictionary<string, string?> dictTBEPAuthorization = strAuthorization
.Split(',')
.Select(s => s.Trim().Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries))
.ToDictionary(
k => k[0],
v => v.Length > 1 ? v[1].TrimStart('\"').TrimEnd('\"') : null
);
string strTimestamp = dictTBEPAuthorization["timestamp"]!;
string strNonce = dictTBEPAuthorization["nonce"]!;
string strSignature = dictTBEPAuthorization["signature"]!;
string strSerialNumber = dictTBEPAuthorization["tbep_serial_number"]!;
string strSignAlgorithm = dictTBEPAuthorization["signature_algorithm"]!;
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, strSignAlgorithm, out error);
}
catch (Exception ex)
{
error = ex;
return false;
}
}
error = new Exception("Could not read value of `TBEP-Authorization`.");
return false;
}
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out _);
}
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, string strSignAlgorithm)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, strSignAlgorithm, out _);
}
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, out Exception? error)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out error);
}
public static bool VerifySignature(this WechatTenpayBusinessClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, string strSignAlgorithm, out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
switch (strSignAlgorithm)
{
case Constants.SignAlgorithms.SHA245_WITH_RSA:
{
if (client.Credentials.TBEPCertificateSerialNumber != null &&
client.Credentials.TBEPCertificatePublicKey != null)
{
try
{
if (!string.Equals(client.Credentials.TBEPCertificateSerialNumber, strSerialNumber))
{
error = new Exception("There is no TBEP public key matched the serial number.");
return false;
}
error = null;
return Utilities.RSAUtility.VerifyWithSHA256(
publicKey: client.Credentials.TBEPCertificatePublicKey,
plainText: GetPlainTextForSignature(timestamp: strTimestamp, nonce: strNonce, body: strBody),
signature: strSignature
);
}
catch (Exception ex)
{
error = ex;
return false;
}
}
error = new Exception("There is no TBEP public key or serial number.");
return false;
}
default:
{
error = new Exception("Unsupported sign algorithm.");
return false;
}
}
}
private static string GetPlainTextForSignature(string timestamp, string nonce, string body)
{
return $"{timestamp}\n{nonce}\n{body}\n";
}
}
}

View File

@ -0,0 +1,34 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientEventDeserializationExtensions
{
/// <summary>
/// <para>反序列化得到 <see cref="WechatTenpayBusinessEvent"/> 对象。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="callbackJson"></param>
/// <returns></returns>
public static WechatTenpayBusinessEvent DeserializeEvent(this WechatTenpayBusinessClient client, string callbackJson)
{
return DeserializeEvent<WechatTenpayBusinessEvent>(client, callbackJson);
}
/// <summary>
/// <para>反序列化得到 <see cref="WechatTenpayBusinessEvent"/> 对象。</para>
/// </summary>
/// <typeparam name="TEvent"></typeparam>
/// <param name="client"></param>
/// <param name="callbackJson"></param>
/// <returns></returns>
public static TEvent DeserializeEvent<TEvent>(this WechatTenpayBusinessClient client, string callbackJson)
where TEvent : WechatTenpayBusinessEvent
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (string.IsNullOrEmpty(callbackJson)) throw new ArgumentNullException(callbackJson);
return client.JsonSerializer.Deserialize<TEvent>(callbackJson);
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Linq;
using System.Text;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientEventVerificationExtensions
{
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="callbackAuthorization"></param>
/// <param name="callbackBody"></param>
/// <returns></returns>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackAuthorization, string callbackBody)
{
return VerifyEventSignature(client, callbackAuthorization, callbackBody, out _);
}
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="callbackAuthorization"></param>
/// <param name="callbackBody"></param>
/// <param name="error"></param>
/// <returns></returns>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackAuthorization, string callbackBody, out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (callbackAuthorization == null) throw new ArgumentNullException(nameof(callbackAuthorization));
if (callbackBody == null) throw new ArgumentNullException(nameof(callbackBody));
bool ret = WechatTenpayBusinessClientSignExtensions.VerifySignature(client, callbackAuthorization, callbackBody, out error);
if (error != null)
error = new Exceptions.WechatTenpayBusinessEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", error);
return ret;
}
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="callbackTimestamp"></param>
/// <param name="callbackNonce">。</param>
/// <param name="callbackBody"></param>
/// <param name="callbackSignature"></param>
/// <param name="callbackSerialNumber"></param>
/// <returns></returns>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackTimestamp, string callbackNonce, string callbackBody, string callbackSignature, string callbackSerialNumber)
{
return VerifyEventSignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out _);
}
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="callbackTimestamp"></param>
/// <param name="callbackNonce">。</param>
/// <param name="callbackBody"></param>
/// <param name="callbackSignature"></param>
/// <param name="callbackSerialNumber"></param>
/// <param name="callbackSignAlgorithm"></param>
/// <returns></returns>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackTimestamp, string callbackNonce, string callbackBody, string callbackSignature, string callbackSerialNumber, string callbackSignAlgorithm)
{
return VerifyEventSignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, callbackSignAlgorithm, out _);
}
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="callbackTimestamp"></param>
/// <param name="callbackNonce">。</param>
/// <param name="callbackBody"></param>
/// <param name="callbackSignature"></param>
/// <param name="callbackSerialNumber"></param>
/// <param name="error"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackTimestamp, string callbackNonce, string callbackBody, string callbackSignature, string callbackSerialNumber, out Exception? error)
{
return VerifyEventSignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out error);
}
/// <summary>
/// <para>验证回调通知事件签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="callbackTimestamp"></param>
/// <param name="callbackNonce">。</param>
/// <param name="callbackBody"></param>
/// <param name="callbackSignature"></param>
/// <param name="callbackSerialNumber"></param>
/// <param name="callbackSignAlgorithm"></param>
/// <param name="error"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool VerifyEventSignature(this WechatTenpayBusinessClient client, string callbackTimestamp, string callbackNonce, string callbackBody, string callbackSignature, string callbackSerialNumber, string callbackSignAlgorithm, out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
bool ret = WechatTenpayBusinessClientSignExtensions.VerifySignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, callbackSignAlgorithm, out error);
if (error != null)
error = new Exceptions.WechatTenpayBusinessEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", error);
return ret;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientExecuteFileUploadsExtensions
{
/// <summary>
/// <para>异步调用 [POST] /file-uploads 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.UploadFileResponse> ExecuteUploadFileAsync(this WechatTenpayBusinessClient client, Models.UploadFileRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
if (request.FileName == null)
request.FileName = Guid.NewGuid().ToString("N").ToLower() + ".png";
if (request.FileHash == null)
request.FileHash = BitConverter.ToString(Utilities.SM3Utility.Hash(request.FileBytes)).Replace("-", "").ToLower();
if (request.FileContentType == null)
request.FileContentType = Utilities.FileNameToContentTypeMapper.GetContentTypeForImage(request.FileName!) ?? "image/png";
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "file-uploads");
using var httpContent = Utilities.FileHttpContentBuilder.Build(fileName: request.FileName, fileBytes: request.FileBytes, fileContentType: request.FileContentType, fileMetaJson: client.JsonSerializer.Serialize(request));
return await client.SendRequestAsync<Models.UploadFileResponse>(flurlReq, httpContent: httpContent, cancellationToken: cancellationToken);
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientExecuteMSEPayAccountsExtensions
{
/// <summary>
/// <para>异步调用 [POST] /mse-pay/accounts/mse-pay/{platform_id}/bill 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.GetMSEPayAccountBillResponse> ExecuteGetMSEPayAccountBillAsync(this WechatTenpayBusinessClient client, Models.GetMSEPayAccountBillRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "accounts", "mse-pay", client.Credentials.PlatformId, "bill");
return await client.SendRequestWithJsonAsync<Models.GetMSEPayAccountBillResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [GET] /{download_url} 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.DownloadMSEPayAccountBillResponse> ExecuteDownloadMSEPayAccountBillAsync(this WechatTenpayBusinessClient client, Models.DownloadMSEPayAccountBillRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Get, request.DownloadUrl)
.WithUrl(request.DownloadUrl);
return await client.SendRequestWithJsonAsync<Models.DownloadMSEPayAccountBillResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientExecuteMSEPayPaymentsExtensions
{
/// <summary>
/// <para>异步调用 [POST] /mse-pay/payments/h5-pay 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.CreateMSEPayPaymentH5PayResponse> ExecuteCreateMSEPayPaymentH5PayAsync(this WechatTenpayBusinessClient client, Models.CreateMSEPayPaymentH5PayRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "payments", "h5-pay");
return await client.SendRequestWithJsonAsync<Models.CreateMSEPayPaymentH5PayResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [GET] /mse-pay/payments/out-payment-id/{out_payment_id} 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.GetMSEPayPaymentByOutPaymentIdResponse> ExecuteGetMSEPayPaymentByOutPaymentIdAsync(this WechatTenpayBusinessClient client, Models.GetMSEPayPaymentByOutPaymentIdRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Get, "mse-pay", "payments", "out-payment-id", request.OutPaymentId);
return await client.SendRequestWithJsonAsync<Models.GetMSEPayPaymentByOutPaymentIdResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [GET] /mse-pay/payments/{payment_id} 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.GetMSEPayPaymentByPaymentIdResponse> ExecuteGetMSEPayPaymentByPaymentIdAsync(this WechatTenpayBusinessClient client, Models.GetMSEPayPaymentByPaymentIdRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Get, "mse-pay", "payments", request.PaymentId);
return await client.SendRequestWithJsonAsync<Models.GetMSEPayPaymentByPaymentIdResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [POST] /mse-pay/payments/{payment_id}/close 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.CloseMSEPayPaymentResponse> ExecuteCloseMSEPayPaymentAsync(this WechatTenpayBusinessClient client, Models.CloseMSEPayPaymentRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "payments", request.PaymentId, "close");
return await client.SendRequestWithJsonAsync<Models.CloseMSEPayPaymentResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientExecuteMSEPayProductApplicationsExtensions
{
/// <summary>
/// <para>异步调用 [POST] /mse-pay/product-applications 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.CreateMSEPayProductApplicationResponse> ExecuteCreateMSEPayProductApplicationAsync(this WechatTenpayBusinessClient client, Models.CreateMSEPayProductApplicationRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "product-applications");
return await client.SendRequestWithJsonAsync<Models.CreateMSEPayProductApplicationResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [GET] /mse-pay/product-applications/out-request-no/{out_request_no} 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.GetMSEPayProductApplicationByOutRequestNumberResponse> ExecuteGetMSEPayProductApplicationByOutRequestNumberAsync(this WechatTenpayBusinessClient client, Models.GetMSEPayProductApplicationByOutRequestNumberRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Get, "mse-pay", "product-applications", "out-request-no", request.OutRequestNumber);
return await client.SendRequestWithJsonAsync<Models.GetMSEPayProductApplicationByOutRequestNumberResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [GET] /mse-pay/product-applications/{request_no} 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.GetMSEPayProductApplicationByRequestNumberResponse> ExecuteGetMSEPayProductApplicationByRequestNumberAsync(this WechatTenpayBusinessClient client, Models.GetMSEPayProductApplicationByRequestNumberRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Get, "mse-pay", "product-applications", request.RequestNumber);
return await client.SendRequestWithJsonAsync<Models.GetMSEPayProductApplicationByRequestNumberResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
/// <summary>
/// <para>异步调用 [POST] /mse-pay/product-applications/{request_no}/links 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.CreateMSEPayProductApplicationLinkResponse> ExecuteCreateMSEPayProductApplicationLinkAsync(this WechatTenpayBusinessClient client, Models.CreateMSEPayProductApplicationLinkRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "product-applications", request.RequestNumber, "links");
return await client.SendRequestWithJsonAsync<Models.CreateMSEPayProductApplicationLinkResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientExecuteMSEPayRedirectsExtensions
{
/// <summary>
/// <para>异步调用 [POST] /mse-pay/redirects 接口。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Models.CreateMSEPayRedirectLinkResponse> ExecuteCreateMSEPayRedirectLinkAsync(this WechatTenpayBusinessClient client, Models.CreateMSEPayRedirectLinkRequest request, CancellationToken cancellationToken = default)
{
if (client is null) throw new ArgumentNullException(nameof(client));
if (request is null) throw new ArgumentNullException(nameof(request));
IFlurlRequest flurlReq = client
.CreateRequest(request, HttpMethod.Post, "mse-pay", "redirects");
return await client.SendRequestWithJsonAsync<Models.CreateMSEPayRedirectLinkResponse>(flurlReq, data: request, cancellationToken: cancellationToken);
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Reflection;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientRequestEncryptionExtensions
{
/// <summary>
/// <para>加密请求中传入的敏感数据。该方法会改变传入的请求模型对象。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="request"></param>
/// <returns></returns>
public static TRequest EncryptRequestSensitiveProperty<TRequest>(this WechatTenpayBusinessClient client, TRequest request)
where TRequest : WechatTenpayBusinessRequest
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (request == null) throw new ArgumentNullException(nameof(request));
try
{
bool requireEncrypt = request.GetType().GetCustomAttributes<WechatTenpayBusinessSensitiveAttribute>(inherit: true).Any();
if (requireEncrypt)
{
if (request.TBEPEncryption is null)
request.TBEPEncryption = new WechatTenpayBusinessRequestTBEPEncryption() { Algorithm = client.Credentials.SensitivePropertyEncryptionAlgorithm };
if (Constants.EncryptionAlgorithms.RSA_OAEP_WITH_SM4_128_CBC.Equals(request.TBEPEncryption.Algorithm))
{
Utilities.ReflectionUtility.ReplacePropertyStringValue(ref request, (target, currentProp, oldValue) =>
{
var attr = currentProp.GetCustomAttribute<WechatTenpayBusinessSensitivePropertyAttribute>();
if (attr == null)
return (false, oldValue);
string sm4IV = client.Credentials.SensitivePropertyEncryptionSM4IV!;
string sm4Key = client.Credentials.SensitivePropertyEncryptionSM4Key!;
string sm4EncryptedKey = Utilities.RSAUtility.EncryptWithECB(publicKey: client.Credentials.TBEPCertificatePublicKey, plainText: sm4Key);
request.TBEPEncryption.CertificateSerialNumber = client.Credentials.TBEPCertificateSerialNumber;
request.TBEPEncryption.EncryptedKey = sm4EncryptedKey;
request.TBEPEncryption.IV = sm4IV;
string newValue = Utilities.SM4Utility.EncryptWithCBC(key: sm4Key, iv: sm4IV, plainText: oldValue);
return (true, newValue);
});
}
else
{
throw new NotSupportedException("Unsupported encryption algorithm.");
}
}
}
catch (Exception ex) when (!(ex is Exceptions.WechatTenpayBusinessRequestEncryptionException))
{
throw new Exceptions.WechatTenpayBusinessRequestEncryptionException("Encrypt request failed. Please see the `InnerException` for more details.", ex);
}
return request;
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Reflection;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientResponseDecryptionExtensions
{
/// <summary>
/// <para>解密响应中返回的敏感数据。该方法会改变传入的响应模型对象。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static TResponse DecryptResponseSensitiveProperty<TResponse>(this WechatTenpayBusinessClient client, TResponse response)
where TResponse : WechatTenpayBusinessResponse
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
if (!response.IsSuccessful())
throw new Exceptions.WechatTenpayBusinessResponseDecryptionException("Decrypt response failed, because the response is not successful.");
try
{
bool requireDecrypt = response.GetType().GetCustomAttributes<WechatTenpayBusinessSensitiveAttribute>(inherit: true).Any();
if (requireDecrypt)
{
if (response.TBEPEncryption is null)
throw new InvalidOperationException("Could not read value of `TBEP-Encrypt`.");
if (response.TBEPEncryption.CertificateSerialNumber != client.Credentials.PlatformCertificateSerialNumber)
throw new Exceptions.WechatTenpayBusinessResponseDecryptionException("Decrypt response failed, because there is no platform certificate matched the serial number.");
if (Constants.EncryptionAlgorithms.RSA_OAEP_WITH_SM4_128_CBC.Equals(response.TBEPEncryption.Algorithm))
{
Utilities.ReflectionUtility.ReplacePropertyStringValue(ref response, (target, currentProp, oldValue) =>
{
var attr = currentProp.GetCustomAttribute<WechatTenpayBusinessSensitivePropertyAttribute>();
if (attr == null)
return (false, oldValue);
string sm4EncryptedKey = response.TBEPEncryption.EncryptedKey!;
string sm4Key = Utilities.RSAUtility.DecryptWithECB(privateKey: client.Credentials.PlatformCertificatePrivateKey, cipherText: sm4EncryptedKey);
string sm4IV = response.TBEPEncryption.IV!;
string newValue = Utilities.SM4Utility.DecryptWithCBC(key: sm4Key, iv: sm4IV, cipherText: oldValue);
return (true, newValue);
});
}
else
{
throw new NotSupportedException("Unsupported decryption algorithm.");
}
}
}
catch (Exception ex) when (!(ex is Exceptions.WechatTenpayBusinessResponseDecryptionException))
{
throw new Exceptions.WechatTenpayBusinessResponseDecryptionException("Decrypt response failed. Please see the `InnerException` for more details.", ex);
}
return response;
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
public static class WechatTenpayBusinessClientResponseVerificationExtensions
{
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="response"></param>
/// <returns></returns>
public static bool VerifyResponseSignature<TResponse>(this WechatTenpayBusinessClient client, TResponse response)
where TResponse : WechatTenpayBusinessResponse
{
return VerifyResponseSignature(client, response, out _);
}
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="response"></param>
/// <param name="error"></param>
/// <returns></returns>
public static bool VerifyResponseSignature<TResponse>(this WechatTenpayBusinessClient client, TResponse response, out Exception? error)
where TResponse : WechatTenpayBusinessResponse
{
if (client == null) throw new ArgumentNullException(nameof(client));
string? responseAuthHeader = response.RawHeaders.FirstOrDefault(e => string.Equals(e.Key, "TBEP-Authorization", StringComparison.OrdinalIgnoreCase)).Value;
string responseBody = Encoding.UTF8.GetString(response.RawBytes);
bool ret = WechatTenpayBusinessClientSignExtensions.VerifySignature(client, responseAuthHeader, responseBody, out error);
if (error != null)
error = new Exceptions.WechatTenpayBusinessResponseVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", error);
return ret;
}
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="responseTimestamp"></param>
/// <param name="responseNonce">。</param>
/// <param name="responseBody"></param>
/// <param name="responseSignature"></param>
/// <param name="responseSerialNumber"></param>
/// <returns></returns>
public static bool VerifyResponseSignature(this WechatTenpayBusinessClient client, string responseTimestamp, string responseNonce, string responseBody, string responseSignature, string responseSerialNumber)
{
return VerifyResponseSignature(client, responseTimestamp, responseNonce, responseBody, responseSignature, responseSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out _);
}
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <param name="client"></param>
/// <param name="responseTimestamp"></param>
/// <param name="responseNonce">。</param>
/// <param name="responseBody"></param>
/// <param name="responseSignature"></param>
/// <param name="responseSerialNumber"></param>
/// <param name="responseSignAlgorithm"></param>
/// <returns></returns>
public static bool VerifyResponseSignature(this WechatTenpayBusinessClient client, string responseTimestamp, string responseNonce, string responseBody, string responseSignature, string responseSerialNumber, string responseSignAlgorithm)
{
return VerifyResponseSignature(client, responseTimestamp, responseNonce, responseBody, responseSignature, responseSerialNumber, responseSignAlgorithm, out _);
}
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="responseTimestamp"></param>
/// <param name="responseNonce">。</param>
/// <param name="responseBody"></param>
/// <param name="responseSignature"></param>
/// <param name="responseSerialNumber"></param>
/// <param name="error"></param>
/// <returns></returns>
public static bool VerifyResponseSignature(this WechatTenpayBusinessClient client, string responseTimestamp, string responseNonce, string responseBody, string responseSignature, string responseSerialNumber, out Exception? error)
{
return VerifyResponseSignature(client, responseTimestamp, responseNonce, responseBody, responseSignature, responseSerialNumber, Constants.SignAlgorithms.SHA245_WITH_RSA, out error);
}
/// <summary>
/// <para>验证响应签名。</para>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
/// <param name="client"></param>
/// <param name="responseTimestamp"></param>
/// <param name="responseNonce">。</param>
/// <param name="responseBody"></param>
/// <param name="responseSignature"></param>
/// <param name="responseSerialNumber"></param>
/// <param name="responseSignAlgorithm"></param>
/// <param name="error"></param>
/// <returns></returns>
public static bool VerifyResponseSignature(this WechatTenpayBusinessClient client, string responseTimestamp, string responseNonce, string responseBody, string responseSignature, string responseSerialNumber, string responseSignAlgorithm, out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
bool ret = WechatTenpayBusinessClientSignExtensions.VerifySignature(client, responseTimestamp, responseNonce, responseBody, responseSignature, responseSerialNumber, responseSignAlgorithm, out error);
if (error != null)
error = new Exceptions.WechatTenpayBusinessResponseVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", error);
return ret;
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Interceptors
{
internal class WechatTenpayBusinessRequestSignatureInterceptor : FlurlHttpCallInterceptor
{
private readonly string _signAlg;
private readonly string _platformId;
private readonly string _platformCertSn;
private readonly string _platformCertPk;
public WechatTenpayBusinessRequestSignatureInterceptor(string signAlg, string platformId, string platformCertSn, string platformCertPk)
{
_signAlg = signAlg;
_platformId = platformId;
_platformCertSn = platformCertSn;
_platformCertPk = platformCertPk;
}
public override async Task BeforeCallAsync(FlurlCall flurlCall)
{
if (flurlCall == null) throw new ArgumentNullException(nameof(flurlCall));
if (flurlCall.Completed) throw new Exceptions.WechatTenpayBusinessRequestSignatureException("This interceptor must be called before request completed.");
string method = flurlCall.HttpRequestMessage.Method.ToString().ToUpper();
string url = flurlCall.HttpRequestMessage.RequestUri?.PathAndQuery ?? string.Empty;
string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
string nonce = Guid.NewGuid().ToString("N");
string body = string.Empty;
if (flurlCall.HttpRequestMessage.Content is MultipartFormDataContent formdataContent)
{
// NOTICE: multipart/form-data 文件上传请求的待签名参数需特殊处理
var httpContent = formdataContent.SingleOrDefault(e => Constants.FormDataFields.FORMDATA_META.Equals(e.Headers.ContentDisposition?.Name?.Trim('\"')));
if (httpContent != null)
{
body = await httpContent.ReadAsStringAsync();
}
}
else
{
body = flurlCall.RequestBody ?? string.Empty;
}
string plainText = $"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n";
string signText;
switch (_signAlg)
{
case Constants.SignAlgorithms.SHA245_WITH_RSA:
{
try
{
signText = Utilities.RSAUtility.SignWithSHA256(_platformCertPk, plainText);
}
catch (Exception ex)
{
throw new Exceptions.WechatTenpayBusinessRequestSignatureException("Generate signature of request failed. Please see the `InnerException` for more details.", ex);
}
}
break;
default:
throw new Exceptions.WechatTenpayBusinessRequestSignatureException("Unsupported authorization sign algorithm.");
}
string authString = $"platform_id=\"{_platformId}\",platform_serial_number=\"{_platformCertSn}\",nonce=\"{nonce}\",timestamp=\"{timestamp}\",signature=\"{signText}\",signature_algorithm=\"{_signAlg}\"";
flurlCall.Request.Headers.Remove(FlurlHttpClient.Constants.HttpHeaders.Authorization);
flurlCall.Request.WithHeader(FlurlHttpClient.Constants.HttpHeaders.Authorization, authString);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /file-uploads 接口的请求。</para>
/// </summary>
public class UploadFileRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置图片文件字节数组。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public byte[] FileBytes { get; set; } = Array.Empty<byte>();
/// <summary>
/// 获取或设置图片文件名(必须以 jpg、bmp、png 为后缀)。如果不指定将由系统自动生成。
/// </summary>
[Newtonsoft.Json.JsonProperty("filename")]
[System.Text.Json.Serialization.JsonPropertyName("filename")]
public string? FileName { get; set; }
/// <summary>
/// 获取或设置图片文件摘要。如果不指定将由系统自动生成。
/// </summary>
[Newtonsoft.Json.JsonProperty("sm3")]
[System.Text.Json.Serialization.JsonPropertyName("sm3")]
public string? FileHash { get; set; }
/// <summary>
/// 获取或设置图片文件 Conent-Type。如果不指定将由系统自动生成。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? FileContentType { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /file-uploads 接口的响应。</para>
/// </summary>
public class UploadFileResponse : WechatTenpayBusinessResponse
{
/// <summary>
/// 获取或设置媒体文件标识 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("file_id")]
[System.Text.Json.Serialization.JsonPropertyName("file_id")]
public string FileId { get; set; } = default!;
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /{download_url} 接口的请求。</para>
/// </summary>
public class DownloadMSEPayAccountBillRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置文件下载链接。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string DownloadUrl { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,13 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /{download_url} 接口的响应。</para>
/// </summary>
public class DownloadMSEPayAccountBillResponse : WechatTenpayBusinessResponse
{
public override bool IsSuccessful()
{
return base.IsSuccessful() && RawBytes?.Length > 0;
}
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/accounts/mse-pay/{platform_id}/bill 接口的请求。</para>
/// </summary>
public class GetMSEPayAccountBillRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置查询日期字符串格式yyyy-MM-dd
/// </summary>
[Newtonsoft.Json.JsonProperty("query_date")]
[System.Text.Json.Serialization.JsonPropertyName("query_date")]
public string DateString { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,33 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/accounts/mse-pay/{platform_id}/bill 接口的响应。</para>
/// </summary>
public class GetMSEPayAccountBillResponse : WechatTenpayBusinessResponse
{
/// <summary>
/// 获取或设置文件下载链接。
/// </summary>
[Newtonsoft.Json.JsonProperty("download_url")]
[System.Text.Json.Serialization.JsonPropertyName("download_url")]
public string DownloadUrl { get; set; } = default!;
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
/// <summary>
/// 获取或设置账单状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("bill_status")]
[System.Text.Json.Serialization.JsonPropertyName("bill_status")]
public string BillStatus { get; set; } = default!;
}
}

View File

@ -0,0 +1,22 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/payments/{payment_id}/close 接口的请求。</para>
/// </summary>
public class CloseMSEPayPaymentRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string PaymentId { get; set; } = string.Empty;
/// <summary>
/// 获取或设置关闭原因。
/// </summary>
[Newtonsoft.Json.JsonProperty("close_reason")]
[System.Text.Json.Serialization.JsonPropertyName("close_reason")]
public string CloseReason { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,40 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/payments/{payment_id}/close 接口的响应。</para>
/// </summary>
public class CloseMSEPayPaymentResponse : WechatTenpayBusinessResponse
{
/// <summary>
/// 获取或设置平台支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("out_payment_id")]
public string OutPaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("payment_id")]
public string PaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置关单状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置关单时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("close_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("close_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset CloseTime { get; set; }
}
}

View File

@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/payments/h5-pay 接口的请求。</para>
/// </summary>
[WechatTenpayBusinessSensitive]
public class CreateMSEPayPaymentH5PayRequest : WechatTenpayBusinessRequest
{
public static class Types
{
public class Payee
{
/// <summary>
/// 获取或设置企业 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_id")]
public string EnterpriseId { get; set; } = string.Empty;
/// <summary>
/// 获取或设置企业名称(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_name")]
[System.Text.Json.Serialization.JsonPropertyName("ent_name")]
[WechatTenpayBusinessSensitiveProperty]
public string EnterpriseName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置企业账户 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_acct_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_acct_id")]
public string EnterpriseAccountId { get; set; } = string.Empty;
/// <summary>
/// 获取或设置银行卡号后 4 位。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_account_number_last4")]
[System.Text.Json.Serialization.JsonPropertyName("bank_account_number_last4")]
public string? BankAccountNumberLast4String { get; set; }
}
public class Goods
{
/// <summary>
/// 获取或设置商品名称。
/// </summary>
[Newtonsoft.Json.JsonProperty("good_name")]
[System.Text.Json.Serialization.JsonPropertyName("good_name")]
public string GoodsName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置商品数量。
/// </summary>
[Newtonsoft.Json.JsonProperty("good_number")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.TextualIntegerConverter))]
[System.Text.Json.Serialization.JsonPropertyName("good_number")]
[System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString)]
public int Count { get; set; }
/// <summary>
/// 获取或设置商品金额(单位:分)。
/// </summary>
[Newtonsoft.Json.JsonProperty("good_amount")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.TextualIntegerConverter))]
[System.Text.Json.Serialization.JsonPropertyName("good_amount")]
[System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString)]
public int Amount { get; set; }
}
public class NotifyConfig
{
/// <summary>
/// 获取或设置支付结果通知 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("server_notify_url")]
[System.Text.Json.Serialization.JsonPropertyName("server_notify_url")]
public string ServerNotifyUrl { get; set; } = string.Empty;
}
public class RiskControl
{
/// <summary>
/// 获取或设置设备号。
/// </summary>
[Newtonsoft.Json.JsonProperty("device_id")]
[System.Text.Json.Serialization.JsonPropertyName("device_id")]
public string? DeviceId { get; set; }
/// <summary>
/// 获取或设置用户终端 IP。
/// </summary>
[Newtonsoft.Json.JsonProperty("payer_client_ip")]
[System.Text.Json.Serialization.JsonPropertyName("payer_client_ip")]
public string? ClientIp { get; set; }
/// <summary>
/// 获取或设置用户浏览器 User-Agent。
/// </summary>
[Newtonsoft.Json.JsonProperty("payer_ua")]
[System.Text.Json.Serialization.JsonPropertyName("payer_ua")]
public string? ClientUserAgent { get; set; }
/// <summary>
/// 获取或设置下单时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("create_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339NullableDateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("create_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339NullableDateTimeOffsetConverter))]
public DateTimeOffset? CreateTime { get; set; }
/// <summary>
/// 获取或设置提货方式。
/// </summary>
[Newtonsoft.Json.JsonProperty("pick_type")]
[System.Text.Json.Serialization.JsonPropertyName("pick_type")]
public string? PickType { get; set; }
/// <summary>
/// 获取或设置提货描述。
/// </summary>
[Newtonsoft.Json.JsonProperty("pick_description")]
[System.Text.Json.Serialization.JsonPropertyName("pick_description")]
public string? PickDescription { get; set; }
}
public class Promotion
{
}
}
/// <summary>
/// 获取或设置付款类型。
/// </summary>
[Newtonsoft.Json.JsonProperty("purchaser_type")]
[System.Text.Json.Serialization.JsonPropertyName("purchaser_type")]
public string PurchaserType { get; set; } = string.Empty;
/// <summary>
/// 获取或设置平台支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("out_payment_id")]
public string OutPaymentId { get; set; } = string.Empty;
/// <summary>
/// 获取或设置支付金额(单位:分)。
/// </summary>
[Newtonsoft.Json.JsonProperty("amount")]
[System.Text.Json.Serialization.JsonPropertyName("amount")]
public int Amount { get; set; }
/// <summary>
/// 获取或设置币种。
/// <para>默认值CNY</para>
/// </summary>
[Newtonsoft.Json.JsonProperty("currency")]
[System.Text.Json.Serialization.JsonPropertyName("currency")]
public string Currency { get; set; } = "CNY";
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
/// <summary>
/// 获取或设置收款方信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("payee")]
[System.Text.Json.Serialization.JsonPropertyName("payee")]
public Types.Payee Payee { get; set; } = new Types.Payee();
/// <summary>
/// 获取或设置附言。
/// </summary>
[Newtonsoft.Json.JsonProperty("memo")]
[System.Text.Json.Serialization.JsonPropertyName("memo")]
public string? Memo { get; set; }
/// <summary>
/// 获取或设置商品信息列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("goods")]
[System.Text.Json.Serialization.JsonPropertyName("goods")]
public IList<Types.Goods>? GoodsList { get; set; }
/// <summary>
/// 获取或设置附加信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("attachment")]
[System.Text.Json.Serialization.JsonPropertyName("attachment")]
public string? Attachment { get; set; }
/// <summary>
/// 获取或设置风控信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("risk_control")]
[System.Text.Json.Serialization.JsonPropertyName("risk_control")]
public Types.RiskControl? RiskControl { get; set; }
/// <summary>
/// 获取或设置回调配置信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("notify_url")]
[System.Text.Json.Serialization.JsonPropertyName("notify_url")]
public Types.NotifyConfig NotifyConfig { get; set; } = new Types.NotifyConfig();
/// <summary>
/// 获取或设置优惠信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("promotion_detail")]
[System.Text.Json.Serialization.JsonPropertyName("promotion_detail")]
public Types.Promotion? Promotion { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/payments/h5-pay 接口的响应。</para>
/// </summary>
public class CreateMSEPayPaymentH5PayResponse : GetMSEPayPaymentByPaymentIdResponse
{
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/payments/out-payment-id/{out_payment_id} 接口的请求。</para>
/// </summary>
public class GetMSEPayPaymentByOutPaymentIdRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置平台支付单号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string OutPaymentId { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,9 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/payments/out-payment-id/{out_payment_id} 接口的响应。</para>
/// </summary>
public class GetMSEPayPaymentByOutPaymentIdResponse : GetMSEPayPaymentByPaymentIdResponse
{
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/payments/{payment_id} 接口的请求。</para>
/// </summary>
public class GetMSEPayPaymentByPaymentIdRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string PaymentId { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,131 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/payments/{payment_id} 接口的响应。</para>
/// </summary>
[WechatTenpayBusinessSensitive]
public class GetMSEPayPaymentByPaymentIdResponse : WechatTenpayBusinessResponse
{
public static class Types
{
public class Payee
{
/// <summary>
/// 获取或设置企业 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_id")]
public string EnterpriseId { get; set; } = default!;
/// <summary>
/// 获取或设置企业名称(需使用平台私钥解密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_name")]
[System.Text.Json.Serialization.JsonPropertyName("ent_name")]
[WechatTenpayBusinessSensitiveProperty]
public string EnterpriseName { get; set; } = default!;
/// <summary>
/// 获取或设置企业账户 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_acct_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_acct_id")]
public string EnterpriseAccountId { get; set; } = default!;
/// <summary>
/// 获取或设置银行卡号后 4 位。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_account_number_last4")]
[System.Text.Json.Serialization.JsonPropertyName("bank_account_number_last4")]
public string? BankAccountNumberLast4String { get; set; }
}
public class FailedReason
{
/// <summary>
/// 获取或设置失败类型。
/// </summary>
[Newtonsoft.Json.JsonProperty("failed_type")]
[System.Text.Json.Serialization.JsonPropertyName("failed_type")]
public string? FailedType { get; set; }
/// <summary>
/// 获取或设置失败原因详情。
/// </summary>
[Newtonsoft.Json.JsonProperty("failed_detail")]
[System.Text.Json.Serialization.JsonPropertyName("failed_detail")]
public string? FailedDetail { get; set; }
}
}
/// <summary>
/// 获取或设置平台支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("out_payment_id")]
public string OutPaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("payment_id")]
[System.Text.Json.Serialization.JsonPropertyName("payment_id")]
public string PaymentId { get; set; } = default!;
/// <summary>
/// 获取或设置支付金额(单位:分)。
/// </summary>
[Newtonsoft.Json.JsonProperty("amount")]
[System.Text.Json.Serialization.JsonPropertyName("amount")]
public int Amount { get; set; }
/// <summary>
/// 获取或设置币种。
/// </summary>
[Newtonsoft.Json.JsonProperty("currency")]
[System.Text.Json.Serialization.JsonPropertyName("currency")]
public string Currency { get; set; } = default!;
/// <summary>
/// 获取或设置收款方信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("payee")]
[System.Text.Json.Serialization.JsonPropertyName("payee")]
public Types.Payee Payee { get; set; } = default!;
/// <summary>
/// 获取或设置微企付用户 OpenId。
/// </summary>
[Newtonsoft.Json.JsonProperty("user_openid")]
[System.Text.Json.Serialization.JsonPropertyName("user_openid")]
public string? UserOpenId { get; set; }
/// <summary>
/// 获取或设置订单状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("pay_status")]
[System.Text.Json.Serialization.JsonPropertyName("pay_status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置附言。
/// </summary>
[Newtonsoft.Json.JsonProperty("memo")]
[System.Text.Json.Serialization.JsonPropertyName("memo")]
public string? Memo { get; set; }
/// <summary>
/// 获取或设置失败原因。
/// </summary>
[Newtonsoft.Json.JsonProperty("failed_reason")]
[System.Text.Json.Serialization.JsonPropertyName("failed_reason")]
public Types.FailedReason? FailedReason { get; set; }
/// <summary>
/// 获取或设置附加信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("attachment")]
[System.Text.Json.Serialization.JsonPropertyName("attachment")]
public string? Attachment { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/product-applications/{request_no}/links 接口的请求。</para>
/// </summary>
public class CreateMSEPayProductApplicationLinkRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置微企付开户申请单号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string RequestNumber { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,39 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/product-applications/{request_no}/links 接口的响应。</para>
/// </summary>
public class CreateMSEPayProductApplicationLinkResponse : WechatTenpayBusinessResponse
{
public static class Types
{
public class PCWebData
{
/// <summary>
/// 获取或设置 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("url")]
[System.Text.Json.Serialization.JsonPropertyName("url")]
public string Url { get; set; } = default!;
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
}
}
/// <summary>
/// 获取或设置 PC Web 跳转链接信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("pc_web")]
[System.Text.Json.Serialization.JsonPropertyName("pc_web")]
public Types.PCWebData PCWebData { get; set; } = default!;
}
}

View File

@ -0,0 +1,248 @@
using System.Collections.Generic;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/product-applications 接口的请求。</para>
/// </summary>
[WechatTenpayBusinessSensitive]
public class CreateMSEPayProductApplicationRequest : WechatTenpayBusinessRequest
{
public static class Types
{
public class BusinessLicense
{
/// <summary>
/// 获取或设置工商注册类型。
/// </summary>
[Newtonsoft.Json.JsonProperty("business_register_type")]
[System.Text.Json.Serialization.JsonPropertyName("business_register_type")]
public string BusinessRegisterType { get; set; } = string.Empty;
/// <summary>
/// 获取或设置统一社会信用证代码。
/// </summary>
[Newtonsoft.Json.JsonProperty("unified_social_credit_code")]
[System.Text.Json.Serialization.JsonPropertyName("unified_social_credit_code")]
public string UnifiedSocialCreditCode { get; set; } = string.Empty;
/// <summary>
/// 获取或设置商户名称。
/// </summary>
[Newtonsoft.Json.JsonProperty("merchant_name")]
[System.Text.Json.Serialization.JsonPropertyName("merchant_name")]
public string MerchantName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置商户简称。
/// </summary>
[Newtonsoft.Json.JsonProperty("merchant_short_name")]
[System.Text.Json.Serialization.JsonPropertyName("merchant_short_name")]
public string MerchantShortName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置法人姓名(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("legal_person_name")]
[System.Text.Json.Serialization.JsonPropertyName("legal_person_name")]
[WechatTenpayBusinessSensitiveProperty]
public string LegalPersonName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置营业期限(格式:["yyyy-MM-dd", "yyyy-MM-dd"],长期用 "长期" 表示)。
/// </summary>
[Newtonsoft.Json.JsonProperty("validity_period")]
[System.Text.Json.Serialization.JsonPropertyName("validity_period")]
public IList<string> ValidityPeriodStrings { get; set; } = new List<string>();
/// <summary>
/// 获取或设置营业执照照片 FileId。
/// </summary>
[Newtonsoft.Json.JsonProperty("photocopy_file_id")]
[System.Text.Json.Serialization.JsonPropertyName("photocopy_file_id")]
public string PhotoCopyFileId { get; set; } = string.Empty;
}
public class IdCard
{
/// <summary>
/// 获取或设置身份证姓名(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("name")]
[System.Text.Json.Serialization.JsonPropertyName("name")]
[WechatTenpayBusinessSensitiveProperty]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 获取或设置身份证号码(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("number")]
[System.Text.Json.Serialization.JsonPropertyName("number")]
[WechatTenpayBusinessSensitiveProperty]
public string IdNumber { get; set; } = string.Empty;
/// <summary>
/// 获取或设置身份证期限(格式:["yyyy-MM-dd", "yyyy-MM-dd"],长期用 "长期" 表示)。
/// </summary>
[Newtonsoft.Json.JsonProperty("validity_period")]
[System.Text.Json.Serialization.JsonPropertyName("validity_period")]
public IList<string> ValidityPeriodStrings { get; set; } = new List<string>();
/// <summary>
/// 获取或设置身份证人像面照片 FileId。
/// </summary>
[Newtonsoft.Json.JsonProperty("front_photocopy_file_id")]
[System.Text.Json.Serialization.JsonPropertyName("front_photocopy_file_id")]
public string FrontPhotoCopyFileId { get; set; } = string.Empty;
/// <summary>
/// 获取或设置身份证国徽面照片 FileId。
/// </summary>
[Newtonsoft.Json.JsonProperty("back_photocopy_file_id")]
[System.Text.Json.Serialization.JsonPropertyName("back_photocopy_file_id")]
public string BackPhotoCopyFileId { get; set; } = string.Empty;
}
public class Contact
{
/// <summary>
/// 获取或设置联系人手机号(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("mobile_number")]
[System.Text.Json.Serialization.JsonPropertyName("mobile_number")]
[WechatTenpayBusinessSensitiveProperty]
public string MobileNumber { get; set; } = string.Empty;
}
public class PayeeAccount
{
/// <summary>
/// 获取或设置账户类型。
/// </summary>
[Newtonsoft.Json.JsonProperty("account_type")]
[System.Text.Json.Serialization.JsonPropertyName("account_type")]
public string AccountType { get; set; } = string.Empty;
/// <summary>
/// 获取或设置开户名称(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_account_name")]
[System.Text.Json.Serialization.JsonPropertyName("bank_account_name")]
[WechatTenpayBusinessSensitiveProperty]
public string BankAccountName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置银行账号(需使用微企付公钥加密)。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_account_number")]
[System.Text.Json.Serialization.JsonPropertyName("bank_account_number")]
[WechatTenpayBusinessSensitiveProperty]
public string BankAccountNumber { get; set; } = string.Empty;
/// <summary>
/// 获取或设置开户银行。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_name")]
[System.Text.Json.Serialization.JsonPropertyName("bank_name")]
public string BankName { get; set; } = string.Empty;
/// <summary>
/// 获取或设置开户银行联行号。与字段 <see cref="BankBranchName"/> 二选一。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_branch_id")]
[System.Text.Json.Serialization.JsonPropertyName("bank_branch_id")]
public string? BankBranchId { get; set; }
/// <summary>
/// 获取或设置开户银行全称(含支行)。与字段 <see cref="BankBranchId"/> 二选一。
/// </summary>
[Newtonsoft.Json.JsonProperty("bank_branch_name")]
[System.Text.Json.Serialization.JsonPropertyName("bank_branch_name")]
public string? BankBranchName { get; set; }
}
public class Product
{
/// <summary>
/// 获取或设置产品名称。
/// </summary>
[Newtonsoft.Json.JsonProperty("product_name")]
[System.Text.Json.Serialization.JsonPropertyName("product_name")]
public string ProductName { get; set; } = string.Empty;
}
public class NotifyConfig
{
/// <summary>
/// 获取或设置普通企业开户结果通知 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("server_notify_url")]
[System.Text.Json.Serialization.JsonPropertyName("server_notify_url")]
public string? ServerNotifyUrl { get; set; }
/// <summary>
/// 获取或设置前端成功回调 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("web_success_url")]
[System.Text.Json.Serialization.JsonPropertyName("web_success_url")]
public string? WebSuccessUrl { get; set; }
/// <summary>
/// 获取或设置前端异常回调刷新 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("web_refresh_url")]
[System.Text.Json.Serialization.JsonPropertyName("web_refresh_url")]
public string? WebRefreshUrl { get; set; }
}
}
/// <summary>
/// 获取或设置业务申请编号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_request_no")]
[System.Text.Json.Serialization.JsonPropertyName("out_request_no")]
public string OutRequestNumber { get; set; } = string.Empty;
/// <summary>
/// 获取或设置营业执照信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("business_license")]
[System.Text.Json.Serialization.JsonPropertyName("business_license")]
public Types.BusinessLicense? BusinessLicense { get; set; }
/// <summary>
/// 获取或设置法人身份证信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("legal_person_id_card")]
[System.Text.Json.Serialization.JsonPropertyName("legal_person_id_card")]
public Types.IdCard? LegalPersonIdCard { get; set; }
/// <summary>
/// 获取或设置联系人信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("contact_info")]
[System.Text.Json.Serialization.JsonPropertyName("contact_info")]
public Types.Contact Contact { get; set; } = new Types.Contact();
/// <summary>
/// 获取或设置收款账户列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("payee_accounts")]
[System.Text.Json.Serialization.JsonPropertyName("payee_accounts")]
public IList<Types.PayeeAccount> PayeeAccountList { get; set; } = new List<Types.PayeeAccount>();
/// <summary>
/// 获取或设置开通产品列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("products")]
[System.Text.Json.Serialization.JsonPropertyName("products")]
public IList<Types.Product> ProductList { get; set; } = new List<Types.Product>();
/// <summary>
/// 获取或设置回调配置。
/// </summary>
[Newtonsoft.Json.JsonProperty("notify_url")]
[System.Text.Json.Serialization.JsonPropertyName("notify_url")]
public Types.NotifyConfig NotifyConfig { get; set; } = new Types.NotifyConfig();
}
}

View File

@ -0,0 +1,22 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/product-applications 接口的响应。</para>
/// </summary>
public class CreateMSEPayProductApplicationResponse : WechatTenpayBusinessResponse
{
/// <summary>
/// 获取或设置业务申请编号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_request_no")]
[System.Text.Json.Serialization.JsonPropertyName("out_request_no")]
public string OutRequestNumber { get; set; } = default!;
/// <summary>
/// 获取或设置微企付开户申请单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("request_no")]
[System.Text.Json.Serialization.JsonPropertyName("request_no")]
public string RequestNumber { get; set; } = default!;
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/product-applications/out-request-no/{out_request_no} 接口的请求。</para>
/// </summary>
public class GetMSEPayProductApplicationByOutRequestNumberRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置业务申请编号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string OutRequestNumber { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,9 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/product-applications/out-request-no/{out_request_no} 接口的响应。</para>
/// </summary>
public class GetMSEPayProductApplicationByOutRequestNumberResponse : GetMSEPayProductApplicationByRequestNumberResponse
{
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/product-applications/{request_no} 接口的请求。</para>
/// </summary>
public class GetMSEPayProductApplicationByRequestNumberRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置微企付开户申请单号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string RequestNumber { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,83 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [GET] /mse-pay/product-applications/{request_no} 接口的响应。</para>
/// </summary>
public class GetMSEPayProductApplicationByRequestNumberResponse : WechatTenpayBusinessResponse
{
public static class Types
{
public class Product
{
public static class Types
{
public class Account
{
/// <summary>
/// 获取或设置企业账户 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_acct_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_acct_id")]
public string EnterpriseAccountId { get; set; } = default!;
}
}
/// <summary>
/// 获取或设置产品名称。
/// </summary>
[Newtonsoft.Json.JsonProperty("product_name")]
[System.Text.Json.Serialization.JsonPropertyName("product_name")]
public string ProductName { get; set; } = default!;
/// <summary>
/// 获取或设置开通状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置账户列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("accounts")]
[System.Text.Json.Serialization.JsonPropertyName("accounts")]
public Types.Account[]? AccountList { get; set; }
}
}
/// <summary>
/// 获取或设置业务申请编号。
/// </summary>
[Newtonsoft.Json.JsonProperty("out_request_no")]
[System.Text.Json.Serialization.JsonPropertyName("out_request_no")]
public string OutRequestNumber { get; set; } = default!;
/// <summary>
/// 获取或设置微企付开户申请单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("request_no")]
[System.Text.Json.Serialization.JsonPropertyName("request_no")]
public string RequestNumber { get; set; } = default!;
/// <summary>
/// 获取或设置企业 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("ent_id")]
[System.Text.Json.Serialization.JsonPropertyName("ent_id")]
public string EnterpriseId { get; set; } = default!;
/// <summary>
/// 获取或设置开通状态。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[System.Text.Json.Serialization.JsonPropertyName("status")]
public string Status { get; set; } = default!;
/// <summary>
/// 获取或设置开通产品列表。
/// </summary>
[Newtonsoft.Json.JsonProperty("product_details")]
[System.Text.Json.Serialization.JsonPropertyName("product_details")]
public Types.Product[] ProductList { get; set; } = default!;
}
}

View File

@ -0,0 +1,15 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/redirects 接口的请求。</para>
/// </summary>
public class CreateMSEPayRedirectLinkRequest : WechatTenpayBusinessRequest
{
/// <summary>
/// 获取或设置微企付支付单号。
/// </summary>
[Newtonsoft.Json.JsonProperty("id")]
[System.Text.Json.Serialization.JsonPropertyName("id")]
public string PaymentId { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,122 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Models
{
/// <summary>
/// <para>表示 [POST] /mse-pay/redirects 接口的响应。</para>
/// </summary>
public class CreateMSEPayRedirectLinkResponse : WechatTenpayBusinessResponse
{
public static class Types
{
public class PCWebData
{
/// <summary>
/// 获取或设置 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("url")]
[System.Text.Json.Serialization.JsonPropertyName("url")]
public string Url { get; set; } = default!;
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
}
public class PCPluginData
{
/// <summary>
/// 获取或设置 Key。
/// </summary>
[Newtonsoft.Json.JsonProperty("key")]
[System.Text.Json.Serialization.JsonPropertyName("key")]
public string Key { get; set; } = default!;
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
}
public class WxaQrcodeData
{
/// <summary>
/// 获取或设置 URL。
/// </summary>
[Newtonsoft.Json.JsonProperty("url")]
[System.Text.Json.Serialization.JsonPropertyName("url")]
public string Url { get; set; } = default!;
}
public class MiniProgramData
{
/// <summary>
/// 获取或设置小程序 AppId。
/// </summary>
[Newtonsoft.Json.JsonProperty("mp_appid")]
[System.Text.Json.Serialization.JsonPropertyName("mp_appid")]
public string AppId { get; set; } = default!;
/// <summary>
/// 获取或设置小程序页面路径。
/// </summary>
[Newtonsoft.Json.JsonProperty("mp_path")]
[System.Text.Json.Serialization.JsonPropertyName("mp_path")]
public string PagePath { get; set; } = default!;
/// <summary>
/// 获取或设置小程序原始 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("mp_username")]
[System.Text.Json.Serialization.JsonPropertyName("mp_username")]
public string Username { get; set; } = default!;
/// <summary>
/// 获取或设置过期时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("expire_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("expire_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset ExpireTime { get; set; }
}
}
/// <summary>
/// 获取或设置 PC Web 跳转链接信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("pc_web")]
[System.Text.Json.Serialization.JsonPropertyName("pc_web")]
public Types.PCWebData PCWebData { get; set; } = default!;
/// <summary>
/// 获取或设置 PC 弹窗组件参数信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("pc_plugin")]
[System.Text.Json.Serialization.JsonPropertyName("pc_plugin")]
public Types.PCPluginData? PCPluginData { get; set; }
/// <summary>
/// 获取或设置小程序二维码参数信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("wx_qrcode")]
[System.Text.Json.Serialization.JsonPropertyName("wx_qrcode")]
public Types.WxaQrcodeData? WxaQrcodeData { get; set; }
/// <summary>
/// 获取或设置小程序参数信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("miniprogram")]
[System.Text.Json.Serialization.JsonPropertyName("miniprogram")]
public Types.MiniProgramData? MiniProgramData { get; set; }
}
}

View File

@ -0,0 +1,26 @@
## SKIT.FlurlHttpClient.Wechat.TenpayBusiness
[![GitHub Stars](https://img.shields.io/github/stars/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat?logo=github&label=Stars)](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat) [![GitHub Forks](https://img.shields.io/github/forks/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat?logo=github&label=Forks)](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat) [![NuGet Download](https://img.shields.io/nuget/dt/SKIT.FlurlHttpClient.Wechat.TenpayBusiness.svg?sanitize=true&label=Downloads)](https://www.nuget.org/packages/SKIT.FlurlHttpClient.Wechat.TenpayBusiness) [![License](https://img.shields.io/github/license/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat?label=License)](https://mit-license.org/)
基于 `Flurl.Http` 的腾讯微企付 API v3 版客户端。
---
### 【功能特性】
- 基于腾讯微企付 v3 版 API 封装。
- 请求时自动生成签名,无需开发者手动干预。
- 提供了腾讯微企付所需的 RSA、SHA-256、SM3、SM4 等算法工具类。
- 提供了解析回调通知事件等扩展方法。
---
### 【开发文档】
[点此查看](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat)。
---
### 【更新日志】
[点此查看](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/blob/main/CHANGELOG.md)。

View File

@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461; net47; netstandard2.0; net5.0; net6.0</TargetFrameworks>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
</PropertyGroup>
<PropertyGroup>
<PackageId>SKIT.FlurlHttpClient.Wechat.TenpayBusinessV3</PackageId>
<PackageIcon>LOGO.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat</PackageProjectUrl>
<PackageTags>Flurl.Http Tencent Tenpay 腾讯 微企付</PackageTags>
<Version>0.0.1-alpha.1</Version>
<Description>基于 Flurl.Http 的腾讯微企付 API v3 版客户端。</Description>
<Authors>Fu Diwei</Authors>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git</RepositoryUrl>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<Deterministic>true</Deterministic>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="../../LOGO.png" Pack="true" PackagePath="/" />
<None Include="README.md" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" Condition="'$(TargetFramework)' == 'net461' Or '$(TargetFramework)' == 'net47'" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="'$(TargetFramework)' == 'net461'" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Common" Version="2.5.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,61 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Settings
{
public class Credentials
{
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.PlatformId"/> 的副本。
/// </summary>
public string PlatformId { get; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.PlatformCertificateSerialNumber"/> 的副本。
/// </summary>
public string PlatformCertificateSerialNumber { get; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.PlatformCertificatePrivateKey"/> 的副本。
/// </summary>
public string PlatformCertificatePrivateKey { get; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.TBEPCertificateSerialNumber"/> 的副本。
/// </summary>
public string TBEPCertificateSerialNumber { get; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.TBEPCertificatePublicKey"/> 的副本。
/// </summary>
public string TBEPCertificatePublicKey { get; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.AutoEncryptRequestSensitivePropertyAlgorithm"/> 的副本。
/// </summary>
public string SensitivePropertyEncryptionAlgorithm { get; set; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.SensitivePropertyEncryptionSM4Key"/> 的副本。
/// </summary>
public string? SensitivePropertyEncryptionSM4Key { get; set; }
/// <summary>
/// 初始化客户端时 <see cref="WechatTenpayBusinessClientOptions.SensitivePropertyEncryptionSM4IV"/> 的副本。
/// </summary>
public string? SensitivePropertyEncryptionSM4IV { get; set; }
internal Credentials(WechatTenpayBusinessClientOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
PlatformId = options.PlatformId;
PlatformCertificateSerialNumber = options.PlatformCertificateSerialNumber;
PlatformCertificatePrivateKey = options.PlatformCertificatePrivateKey;
TBEPCertificateSerialNumber = options.TBEPCertificateSerialNumber;
TBEPCertificatePublicKey = options.TBEPCertificatePublicKey;
SensitivePropertyEncryptionAlgorithm = options.SensitivePropertyEncryptionAlgorithm;
SensitivePropertyEncryptionSM4Key = options.SensitivePropertyEncryptionSM4Key;
SensitivePropertyEncryptionSM4IV = options.SensitivePropertyEncryptionSM4IV;
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Web;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
internal static class FileHttpContentBuilder
{
public static MultipartFormDataContent Build(string fileName, byte[] fileBytes, string fileContentType, string fileMetaJson, string formDataName = "file")
{
return Build(fileName: fileName, fileBytes: fileBytes, fileContentType: fileContentType, fileMetaJson: fileMetaJson, formDataName: formDataName, (_) => { }, (_) => { });
}
public static MultipartFormDataContent Build(string fileName, byte[] fileBytes, string fileContentType, string fileMetaJson, string formDataName, Action<HttpContent> configureMetaHttpContent, Action<HttpContent> configureFileHttpContent)
{
if (fileName == null) throw new ArgumentNullException(nameof(fileName));
if (fileMetaJson == null) throw new ArgumentNullException(nameof(fileMetaJson));
if (formDataName == null) throw new ArgumentNullException(nameof(formDataName));
if (configureFileHttpContent == null) throw new ArgumentNullException(nameof(configureFileHttpContent));
fileBytes = fileBytes ?? Array.Empty<byte>();
fileContentType = string.IsNullOrEmpty(fileContentType) ? "application/octet-stream" : fileContentType;
formDataName = formDataName.Replace("\"", "");
ByteArrayContent metaContent = new ByteArrayContent(Encoding.UTF8.GetBytes(fileMetaJson));
metaContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
configureMetaHttpContent(metaContent);
ByteArrayContent fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(fileContentType);
configureFileHttpContent(fileContent);
string boundary = "--BOUNDARY--" + DateTimeOffset.Now.Ticks.ToString("x");
MultipartFormDataContent httpContent = new MultipartFormDataContent(boundary);
httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/form-data; boundary={boundary}");
httpContent.Add(metaContent, $"\"{Constants.FormDataFields.FORMDATA_META}\"");
httpContent.Add(fileContent, $"\"{formDataName}\"", $"\"{HttpUtility.UrlEncode(fileName)}\"");
return httpContent;
}
}
}

View File

@ -0,0 +1,24 @@
using System.IO;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
internal static class FileNameToContentTypeMapper
{
public static string? GetContentTypeForImage(string fileName)
{
string extension = Path.GetExtension(fileName ?? "/")?.ToLower() ?? string.Empty;
switch (extension)
{
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".bmp":
return "image/bmp";
case ".png":
return "image/png";
}
return null;
}
}
}

View File

@ -0,0 +1,191 @@
using System;
using System.Collections;
using System.Reflection;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
internal static class ReflectionUtility
{
public delegate (bool Modified, string NewValue) ReplacePropertyStringValueReplacementHandler(object target, PropertyInfo currentProp, string oldValue);
public static void ReplacePropertyStringValue<T>(ref T obj, ReplacePropertyStringValueReplacementHandler replacement)
{
InnerReplacePropertyStringValue(ref obj, replacement);
}
private static void InnerReplacePropertyStringValue<T>(ref T obj, ReplacePropertyStringValueReplacementHandler replacement)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (replacement == null) throw new ArgumentNullException(nameof(replacement));
Type objType = obj.GetType();
if (!objType.IsClass)
throw new NotSupportedException();
if (objType.IsArray || obj is IList || obj is IDictionary)
{
InnerReplaceEachCollectionPropertyStringValue(ref obj, objType, replacement, null);
}
else
{
foreach (var childProp in objType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (!childProp.CanWrite)
continue;
Type propType = childProp.PropertyType;
if (propType == typeof(string))
{
string value = (string)childProp.GetValue(obj, null)!;
if (value is null)
continue;
var result = replacement(obj, childProp, value);
if (result.Modified)
{
childProp.SetValue(obj, result.NewValue);
}
}
else if (propType.IsClass)
{
object? value = childProp.GetValue(obj, null);
if (value is null)
continue;
InnerReplacePropertyStringValue(ref value, replacement);
childProp.SetValue(obj, value);
}
else
{
object? value = childProp.GetValue(obj, null);
if (value is null)
continue;
InnerReplaceEachCollectionPropertyStringValue(ref value, propType, replacement, childProp);
}
}
}
}
private static void InnerReplaceEachCollectionPropertyStringValue<T>(ref T obj, Type objType, ReplacePropertyStringValueReplacementHandler replacement, PropertyInfo? currentProp)
{
if (objType.IsArray)
{
var array = (obj as Array)!;
for (int i = 0, len = array.Length; i < len; i++)
{
object? element = array.GetValue(i);
if (element is null)
continue;
Type elementType = element.GetType();
if (elementType == typeof(string))
{
if (currentProp == null)
continue;
if (!currentProp.CanWrite)
continue;
var oldValue = (string)element!;
var resHandler = replacement(obj!, currentProp, oldValue);
if (resHandler.Modified && !array.IsReadOnly)
{
array.SetValue(resHandler.NewValue, i);
}
}
else if (elementType.IsClass)
{
InnerReplacePropertyStringValue(ref element, replacement);
//if (!array.IsReadOnly)
//{
// array.SetValue(element, i);
//}
}
else
{
continue;
}
}
}
else if (obj is IList)
{
var list = (obj as IList)!;
for (int i = 0, len = list.Count; i < len; i++)
{
object? element = list[i];
if (element is null)
continue;
Type elementType = element.GetType();
if (elementType == typeof(string))
{
if (currentProp == null)
continue;
if (!currentProp.CanWrite)
continue;
var oldValue = (string)element!;
var resHandler = replacement(obj, currentProp, oldValue);
if (resHandler.Modified && !list.IsReadOnly)
{
list[i] = resHandler.NewValue;
}
}
else if (elementType.IsClass)
{
InnerReplacePropertyStringValue(ref element, replacement);
//if (!list.IsReadOnly)
//{
// list[i] = element;
//}
}
else
{
continue;
}
}
}
else if (obj is IDictionary)
{
var dict = (obj as IDictionary)!;
foreach (DictionaryEntry entry in dict)
{
object? entryValue = entry.Value;
if (entryValue is null)
continue;
Type entryValueType = entryValue.GetType();
if (entryValueType == typeof(string))
{
if (currentProp == null)
continue;
if (!currentProp.CanWrite)
continue;
string oldValue = (string)entryValue!;
var resHandler = replacement(obj, currentProp, oldValue);
if (resHandler.Modified && !dict.IsReadOnly)
{
dict[entry.Key] = resHandler.NewValue;
}
}
else if (entryValueType.IsClass)
{
InnerReplacePropertyStringValue(ref entryValue, replacement);
//if (!dict.IsReadOnly)
//{
// dict[entry.Key] = entryValue;
//}
}
else
{
continue;
}
}
}
}
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
/// <summary>
/// RSA 算法工具类。
/// </summary>
public static class RSAUtility
{
private const string RSA_CIPHER_ALGORITHM_ECB = "RSA/ECB";
private const string RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1 = "OAEPWITHSHA1ANDMGF1PADDING";
private const string RSA_SIGNER_ALGORITHM_SHA256 = "SHA-256withRSA";
/// <summary>
/// 使用私钥基于 SHA-256 算法生成签名。
/// </summary>
/// <param name="privateKeyBytes">PKCS#8 私钥字节数组。</param>
/// <param name="plainBytes">待签名的数据字节数组。</param>
/// <returns>签名字节数组。</returns>
public static byte[] SignWithSHA256(byte[] privateKeyBytes, byte[] plainBytes)
{
if (privateKeyBytes == null) throw new ArgumentNullException(nameof(privateKeyBytes));
if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
return SignWithSHA256(rsaKeyParams, plainBytes);
}
/// <summary>
/// 使用私钥基于 SHA-256 算法生成签名。
/// </summary>
/// <param name="privateKey">PKCS#8 私钥PEM 格式)。</param>
/// <param name="plainText">待签名的文本数据。</param>
/// <returns>经 Base64 编码的签名。</returns>
public static string SignWithSHA256(string privateKey, string plainText)
{
if (privateKey == null) throw new ArgumentNullException(nameof(privateKey));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
byte[] privateKeyBytes = ConvertPkcs8PrivateKeyToByteArray(privateKey);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] signBytes = SignWithSHA256(privateKeyBytes, plainBytes);
return Convert.ToBase64String(signBytes);
}
/// <summary>
/// 使用公钥基于 SHA-256 算法验证签名。
/// </summary>
/// <param name="publicKeyBytes">PKCS#8 公钥字节数据。</param>
/// <param name="plainBytes">待验证的数据字节数据。</param>
/// <param name="signBytes">待验证的签名字节数据。</param>
/// <returns>验证结果。</returns>
public static bool VerifyWithSHA256(byte[] publicKeyBytes, byte[] plainBytes, byte[] signBytes)
{
if (publicKeyBytes == null) throw new ArgumentNullException(nameof(publicKeyBytes));
if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes));
if (signBytes == null) throw new ArgumentNullException(nameof(signBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes);
return VerifyWithSHA256(rsaKeyParams, plainBytes, signBytes);
}
/// <summary>
/// 使用公钥基于 SHA-256 算法验证签名。
/// </summary>
/// <param name="publicKey">PKCS#8 公钥PEM 格式)。</param>
/// <param name="plainText">待验证的文本数据。</param>
/// <param name="signature">经 Base64 编码的待验证的签名。</param>
/// <returns>验证结果。</returns>
public static bool VerifyWithSHA256(string publicKey, string plainText, string signature)
{
if (publicKey == null) throw new ArgumentNullException(nameof(publicKey));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
if (signature == null) throw new ArgumentNullException(nameof(signature));
byte[] publicKeyBytes = ConvertPkcs8PublicKeyToByteArray(publicKey);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] signBytes = Convert.FromBase64String(signature);
return VerifyWithSHA256(publicKeyBytes, plainBytes, signBytes);
}
/// <summary>
/// 使用私钥基于 ECB 模式解密数据。
/// </summary>
/// <param name="privateKeyBytes">PKCS#8 私钥字节数据。</param>
/// <param name="cipherBytes">待解密的数据字节数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>解密后的数据字节数组。</returns>
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (privateKeyBytes == null) throw new ArgumentNullException(nameof(privateKeyBytes));
if (cipherBytes == null) throw new ArgumentNullException(nameof(cipherBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
return DecryptWithECB(rsaKeyParams, cipherBytes, paddingAlgorithm);
}
/// <summary>
/// 使用私钥基于 ECB 模式解密数据。
/// </summary>
/// <param name="privateKey">PKCS#8 私钥PEM 格式)。</param>
/// <param name="cipherText">经 Base64 编码的待解密数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>解密后的文本数据。</returns>
public static string DecryptWithECB(string privateKey, string cipherText, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (privateKey == null) throw new ArgumentNullException(nameof(privateKey));
if (cipherText == null) throw new ArgumentNullException(nameof(cipherText));
byte[] privateKeyBytes = ConvertPkcs8PrivateKeyToByteArray(privateKey);
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingAlgorithm);
return Encoding.UTF8.GetString(plainBytes);
}
/// <summary>
/// 使用公钥基于 ECB 模式加密数据。
/// </summary>
/// <param name="publicKeyBytes">PKCS#8 公钥字节数据。</param>
/// <param name="plainBytes">待加密的数据字节数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>加密后的数据字节数组。</returns>
public static byte[] EncryptWithECB(byte[] publicKeyBytes, byte[] plainBytes, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (publicKeyBytes == null) throw new ArgumentNullException(nameof(publicKeyBytes));
if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes);
return EncryptWithECB(rsaKeyParams, plainBytes, paddingAlgorithm);
}
/// <summary>
/// 使用公钥基于 ECB 模式加密数据。
/// </summary>
/// <param name="publicKey">PKCS#8 公钥PEM 格式)。</param>
/// <param name="plainText">待加密的文本数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>经 Base64 编码的加密数据。</returns>
public static string EncryptWithECB(string publicKey, string plainText, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (publicKey == null) throw new ArgumentNullException(nameof(publicKey));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
byte[] publicKeyBytes = ConvertPkcs8PublicKeyToByteArray(publicKey);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = EncryptWithECB(publicKeyBytes, plainBytes, paddingAlgorithm);
return Convert.ToBase64String(cipherBytes);
}
private static byte[] ConvertPkcs8PrivateKeyToByteArray(string privateKey)
{
privateKey = privateKey
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "");
privateKey = Regex.Replace(privateKey, "\\s+", "");
return Convert.FromBase64String(privateKey);
}
private static byte[] ConvertPkcs8PublicKeyToByteArray(string publicKey)
{
publicKey = publicKey
.Replace("-----BEGIN PUBLIC KEY-----", "")
.Replace("-----END PUBLIC KEY-----", "");
publicKey = Regex.Replace(publicKey, "\\s+", "");
return Convert.FromBase64String(publicKey);
}
private static byte[] SignWithSHA256(RsaKeyParameters rsaKeyParams, byte[] plainBytes)
{
ISigner signer = SignerUtilities.GetSigner(RSA_SIGNER_ALGORITHM_SHA256);
signer.Init(true, rsaKeyParams);
signer.BlockUpdate(plainBytes, 0, plainBytes.Length);
return signer.GenerateSignature();
}
private static bool VerifyWithSHA256(RsaKeyParameters rsaKeyParams, byte[] plainBytes, byte[] signBytes)
{
ISigner signer = SignerUtilities.GetSigner(RSA_SIGNER_ALGORITHM_SHA256);
signer.Init(false, rsaKeyParams);
signer.BlockUpdate(plainBytes, 0, plainBytes.Length);
return signer.VerifySignature(signBytes);
}
private static byte[] EncryptWithECB(RsaKeyParameters rsaKeyParams, byte[] plainBytes, string paddingAlgorithm)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingAlgorithm}");
cipher.Init(true, rsaKeyParams);
return cipher.DoFinal(plainBytes);
}
private static byte[] DecryptWithECB(RsaKeyParameters rsaKeyParams, byte[] cipherBytes, string paddingAlgorithm)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingAlgorithm}");
cipher.Init(false, rsaKeyParams);
return cipher.DoFinal(cipherBytes);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
/// <summary>
/// SHA-256 算法工具类。
/// </summary>
public static class SHA256Utility
{
/// <summary>
/// 获取 SHA-256 信息摘要。
/// </summary>
/// <param name="bytes">信息字节数组。</param>
/// <returns>信息摘要字节数组。</returns>
public static byte[] Hash(byte[] bytes)
{
if (bytes == null) throw new ArgumentNullException(nameof(bytes));
using SHA256 sha = SHA256.Create();
return sha.ComputeHash(bytes);
}
/// <summary>
/// 获取 SHA-256 信息摘要。
/// </summary>
/// <param name="message">文本信息。</param>
/// <returns>信息摘要。</returns>
public static string Hash(string message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
byte[] msgBytes = Encoding.UTF8.GetBytes(message);
byte[] hashBytes = Hash(msgBytes);
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}
}

View File

@ -0,0 +1,370 @@
using System;
using System.Text;
using Org.BouncyCastle.Crypto;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
/// <summary>
/// SM3 算法工具类。
/// </summary>
public static class SM3Utility
{
internal static class BitOperator
{
public static int URShift(int number, int bits)
{
if (number >= 0)
return number >> bits;
else
return (number >> bits) + (2 << ~bits);
}
public static int URShift(int number, long bits)
{
return URShift(number, (int)bits);
}
public static long URShift(long number, int bits)
{
if (number >= 0)
return number >> bits;
else
return (number >> bits) + (2L << ~bits);
}
public static long URShift(long number, long bits)
{
return URShift(number, (int)bits);
}
}
internal abstract class GeneralDigest : IDigest
{
private const int ByteLength = 64;
private readonly byte[] XBuf;
private int XBufOff;
private long ByteCount;
public abstract string AlgorithmName { get; }
protected GeneralDigest()
{
XBuf = new byte[4];
}
protected GeneralDigest(GeneralDigest t)
{
XBuf = new byte[t.XBuf.Length];
Array.Copy(t.XBuf, 0, XBuf, 0, t.XBuf.Length);
XBufOff = t.XBufOff;
ByteCount = t.ByteCount;
}
public void Update(byte input)
{
XBuf[XBufOff++] = input;
if (XBufOff == XBuf.Length)
{
ProcessWord(XBuf, 0);
XBufOff = 0;
}
ByteCount++;
}
public void BlockUpdate(byte[] input, int inOff, int length)
{
//更新当前消息摘要
while ((XBufOff != 0) && (length > 0))
{
Update(input[inOff]);
inOff++;
length--;
}
//处理完整的消息摘要
while (length > XBuf.Length)
{
ProcessWord(input, inOff);
inOff += XBuf.Length;
length -= XBuf.Length;
ByteCount += XBuf.Length;
}
//填充剩余的消息摘要
while (length > 0)
{
Update(input[inOff]);
inOff++;
length--;
}
}
public void Finish()
{
long bitLength = (ByteCount << 3);
//添加字节
Update(unchecked((byte)128));
while (XBufOff != 0) Update(unchecked((byte)0));
ProcessLength(bitLength);
ProcessBlock();
}
public virtual void Reset()
{
ByteCount = 0;
XBufOff = 0;
Array.Clear(XBuf, 0, XBuf.Length);
}
public int GetByteLength()
{
return ByteLength;
}
internal abstract void ProcessWord(byte[] input, int inOff);
internal abstract void ProcessLength(long bitLength);
internal abstract void ProcessBlock();
public abstract int GetDigestSize();
public abstract int DoFinal(byte[] output, int outOff);
}
internal sealed class SM3Digest : GeneralDigest
{
private const int DIGEST_LENGTH = 32;
private const int T_1 = 0x79cc4519;
private const int T_2 = 0x7a879d8a;
private static readonly int[] IV = new int[] { 0x7380166f, 0x4914b2b9, 0x172442d7, unchecked((int)0xda8a0600), unchecked((int)0xa96f30bc), 0x163138aa, unchecked((int)0xe38dee4d), unchecked((int)0xb0fb0e4e) };
private static readonly int[] X0 = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private readonly int[] _v1 = new int[8];
private readonly int[] _v2 = new int[8];
private readonly int[] _x = new int[68];
private int _xOff;
public override string AlgorithmName { get { return "SM3"; } }
public SM3Digest()
: base()
{
Reset();
}
public SM3Digest(SM3Digest t)
: base(t)
{
Array.Copy(t._x, 0, _x, 0, t._x.Length);
_xOff = t._xOff;
Array.Copy(t._v1, 0, _v1, 0, t._v1.Length);
}
private static void ConvertIntegerToBigEndian(int num, byte[] bytes, int offset)
{
bytes[offset] = (byte)(BitOperator.URShift(num, 24));
bytes[++offset] = (byte)(BitOperator.URShift(num, 16));
bytes[++offset] = (byte)(BitOperator.URShift(num, 8));
bytes[++offset] = (byte)(num);
}
private static int Rotate(int x, int n)
{
return (x << n) | (BitOperator.URShift(x, (32 - n)));
}
private static int P0(int x)
{
return (x) ^ Rotate(x, 9) ^ Rotate(x, 17);
}
private static int P1(int x)
{
return (x) ^ Rotate(x, 15) ^ Rotate(x, 23);
}
private static int FFOne(int x, int y, int z)
{
return (x ^ y ^ z);
}
private static int FFSecond(int x, int y, int z)
{
return ((x & y) | (x & z) | (y & z));
}
private static int GGOne(int x, int y, int z)
{
return (x ^ y ^ z);
}
private static int GGSecond(int x, int y, int z)
{
return ((x & y) | (~x & z));
}
public override void Reset()
{
base.Reset();
Array.Copy(IV, 0, _v1, 0, IV.Length);
_xOff = 0;
Array.Copy(X0, 0, _x, 0, X0.Length);
}
internal override void ProcessWord(byte[] input, int inOff)
{
int n = input[inOff] << 24;
n |= (input[++inOff] & 0xff) << 16;
n |= (input[++inOff] & 0xff) << 8;
n |= (input[++inOff] & 0xff);
_x[_xOff] = n;
if (++_xOff == 16)
{
ProcessBlock();
}
}
internal override void ProcessLength(long bitLength)
{
if (_xOff > 14)
{
ProcessBlock();
}
_x[14] = (int)(BitOperator.URShift(bitLength, 32));
_x[15] = (int)(bitLength & unchecked((int)0xffffffff));
}
internal override void ProcessBlock()
{
int j;
int[] ww = _x;
int[] ww_ = new int[64];
for (j = 16; j < 68; j++)
{
ww[j] = P1(ww[j - 16] ^ ww[j - 9] ^ (Rotate(ww[j - 3], 15))) ^ (Rotate(ww[j - 13], 7)) ^ ww[j - 6];
}
for (j = 0; j < 64; j++)
{
ww_[j] = ww[j] ^ ww[j + 4];
}
int[] vv = _v1;
int[] vv_ = _v2;
Array.Copy(vv, 0, vv_, 0, IV.Length);
int SS1, SS2, TT1, TT2;
int aaa;
for (j = 0; j < 16; j++)
{
aaa = Rotate(vv_[0], 12);
SS1 = aaa + vv_[4] + Rotate(T_1, j);
SS1 = Rotate(SS1, 7);
SS2 = SS1 ^ aaa;
TT1 = FFOne(vv_[0], vv_[1], vv_[2]) + vv_[3] + SS2 + ww_[j];
TT2 = GGOne(vv_[4], vv_[5], vv_[6]) + vv_[7] + SS1 + ww[j];
vv_[3] = vv_[2];
vv_[2] = Rotate(vv_[1], 9);
vv_[1] = vv_[0];
vv_[0] = TT1;
vv_[7] = vv_[6];
vv_[6] = Rotate(vv_[5], 19);
vv_[5] = vv_[4];
vv_[4] = P0(TT2);
}
for (j = 16; j < 64; j++)
{
aaa = Rotate(vv_[0], 12);
SS1 = aaa + vv_[4] + Rotate(T_2, j);
SS1 = Rotate(SS1, 7);
SS2 = SS1 ^ aaa;
TT1 = FFSecond(vv_[0], vv_[1], vv_[2]) + vv_[3] + SS2 + ww_[j];
TT2 = GGSecond(vv_[4], vv_[5], vv_[6]) + vv_[7] + SS1 + ww[j];
vv_[3] = vv_[2];
vv_[2] = Rotate(vv_[1], 9);
vv_[1] = vv_[0];
vv_[0] = TT1;
vv_[7] = vv_[6];
vv_[6] = Rotate(vv_[5], 19);
vv_[5] = vv_[4];
vv_[4] = P0(TT2);
}
for (j = 0; j < 8; j++)
{
vv[j] ^= vv_[j];
}
_xOff = 0;
Array.Copy(X0, 0, _x, 0, X0.Length);
}
public override int GetDigestSize()
{
return DIGEST_LENGTH;
}
public override int DoFinal(byte[] output, int outOff)
{
Finish();
for (int i = 0; i < 8; i++)
{
ConvertIntegerToBigEndian(_v1[i], output, outOff + i * 4);
}
Reset();
return DIGEST_LENGTH;
}
}
/// <summary>
/// 获取 SM3 哈希值。
/// </summary>
/// <param name="bytes">信息字节数组。</param>
/// <returns>哈希字节数组。</returns>
public static byte[] Hash(byte[] bytes)
{
if (bytes == null) throw new ArgumentNullException(nameof(bytes));
SM3Digest sm3 = new SM3Digest();
sm3.BlockUpdate(bytes, 0, bytes.Length);
byte[] hashBytes = new byte[sm3.GetDigestSize()];
sm3.DoFinal(hashBytes, 0);
return hashBytes;
}
/// <summary>
/// 获取 SM3 哈希值。
/// </summary>
/// <param name="message">文本信息。</param>
/// <returns>哈希值。</returns>
public static string Hash(string message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
byte[] msgBytes = Encoding.UTF8.GetBytes(message);
byte[] hashBytes = Hash(msgBytes);
return BitConverter.ToString(hashBytes).Replace("-", "");
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.Utilities
{
/// <summary>
/// SM4 算法工具类。
/// </summary>
public static class SM4Utility
{
// REF: https://github.com/bcgit/bc-csharp/blob/master/crypto/src/security/CipherUtilities.cs
private const string SM4_ALG_NAME = "SM4";
private const string SM4_CIPHER_ALGORITHM_CBC = "SM4/CBC";
private const string SM4_CIPHER_PADDING_PKCS7PADDING = "PKCS7PADDING";
/// <summary>
/// 基于 CBC 模式解密数据。
/// </summary>
/// <param name="keyBytes">密钥字节数据。</param>
/// <param name="ivBytes">偏移量字节数据。</param>
/// <param name="cipherBytes">待解密的数据字节数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="SM4_CIPHER_PADDING_PKCS7PADDING"/></param>
/// <returns>解密后的数据字节数组。</returns>
public static byte[] DecryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] cipherBytes, string paddingMode = SM4_CIPHER_PADDING_PKCS7PADDING)
{
if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes));
if (cipherBytes == null) throw new ArgumentNullException(nameof(cipherBytes));
KeyParameter sm4KeyParams = ParameterUtilities.CreateKeyParameter(SM4_ALG_NAME, keyBytes);
ParametersWithIV sm4keyParamsWithIv = new ParametersWithIV(sm4KeyParams, ivBytes);
return DecryptWithCBC(sm4keyParamsWithIv, cipherBytes, paddingMode);
}
/// <summary>
/// 基于 CBC 模式解密数据。
/// </summary>
/// <param name="key">经 Base64 编码的密钥。</param>
/// <param name="iv">>经 Base64 编码的偏移量。</param>
/// <param name="cipherText">经 Base64 编码的待解密数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="SM4_CIPHER_PADDING_PKCS7PADDING"/></param>
/// <returns>解密后的文本数据。</returns>
public static string DecryptWithCBC(string key, string iv, string cipherText, string paddingMode = SM4_CIPHER_PADDING_PKCS7PADDING)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (cipherText == null) throw new ArgumentNullException(nameof(cipherText));
byte[] keyBytes = Convert.FromBase64String(key);
byte[] ivBytes = Convert.FromBase64String(iv);
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] plainBytes = DecryptWithCBC(keyBytes, ivBytes, cipherBytes, paddingMode);
return Encoding.UTF8.GetString(plainBytes);
}
/// <summary>
/// 基于 CBC 模式加密数据。
/// </summary>
/// <param name="keyBytes">密钥字节数组。</param>
/// <param name="ivBytes">偏移量字节数据。</param>
/// <param name="plainBytes">待加密的数据字节数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="SM4_CIPHER_PADDING_PKCS7PADDING"/></param>
/// <returns>加密后的数据字节数组。</returns>
public static byte[] EncryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes, string paddingMode = SM4_CIPHER_PADDING_PKCS7PADDING)
{
if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes));
if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes));
KeyParameter sm4KeyParams = ParameterUtilities.CreateKeyParameter(SM4_ALG_NAME, keyBytes);
ParametersWithIV sm4keyParamsWithIv = new ParametersWithIV(sm4KeyParams, ivBytes);
return EncryptWithCBC(sm4keyParamsWithIv, plainBytes, paddingMode);
}
/// <summary>
/// 基于 CBC 模式加密数据。
/// </summary>
/// <param name="key">经 Base64 编码的密钥。</param>
/// <param name="iv">>经 Base64 编码的偏移量。</param>
/// <param name="plainText">待加密的文本数据。</param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="SM4_CIPHER_PADDING_PKCS7PADDING"/></param>
/// <returns>经 Base64 编码的加密数据。</returns>
public static string EncryptWithCBC(string key, string iv, string plainText, string paddingMode = SM4_CIPHER_PADDING_PKCS7PADDING)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
byte[] keyBytes = Convert.FromBase64String(key);
byte[] ivBytes = Convert.FromBase64String(iv);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = EncryptWithCBC(keyBytes, ivBytes, plainBytes, paddingMode);
return Convert.ToBase64String(cipherBytes);
}
private static byte[] EncryptWithCBC(ICipherParameters sm4KeyParams, byte[] plainBytes, string paddingMode)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{SM4_CIPHER_ALGORITHM_CBC}/{paddingMode}");
cipher.Init(true, sm4KeyParams);
return cipher.DoFinal(plainBytes);
}
private static byte[] DecryptWithCBC(ICipherParameters sm4KeyParams, byte[] cipherBytes, string paddingMode)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{SM4_CIPHER_ALGORITHM_CBC}/{paddingMode}");
cipher.Init(false, sm4KeyParams);
return cipher.DoFinal(cipherBytes);
}
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 一个腾讯微企付 API HTTP 客户端。
/// </summary>
public class WechatTenpayBusinessClient : CommonClientBase, ICommonClient
{
/// <summary>
/// 获取当前客户端使用的腾讯微企付平台凭证。
/// </summary>
public Settings.Credentials Credentials { get; }
/// <summary>
/// 获取是否自动加密请求中的敏感信息字段。
/// </summary>
protected bool AutoEncryptRequestSensitiveProperty { get; }
/// <summary>
/// 获取是否自动解密请求中的敏感信息字段。
/// </summary>
protected bool AutoDecryptResponseSensitiveProperty { get; }
/// <summary>
/// 用指定的配置项初始化 <see cref="WechatTenpayBusinessClient"/> 类的新实例。
/// </summary>
/// <param name="options">配置项。</param>
public WechatTenpayBusinessClient(WechatTenpayBusinessClientOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
Credentials = new Settings.Credentials(options);
AutoEncryptRequestSensitiveProperty = options.AutoEncryptRequestSensitiveProperty;
AutoDecryptResponseSensitiveProperty = options.AutoDecryptResponseSensitiveProperty;
FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayBusinessEndpoints.DEFAULT;
FlurlClient.Headers.Remove(FlurlHttpClient.Constants.HttpHeaders.Accept);
FlurlClient.Headers.Remove(FlurlHttpClient.Constants.HttpHeaders.AcceptLanguage);
FlurlClient.WithHeader(FlurlHttpClient.Constants.HttpHeaders.Accept, "application/json");
FlurlClient.WithTimeout(TimeSpan.FromMilliseconds(options.Timeout));
Interceptors.Add(new Interceptors.WechatTenpayBusinessRequestSignatureInterceptor(
signAlg: options.SignAlgorithm,
platformId: options.PlatformId,
platformCertSn: options.PlatformCertificateSerialNumber,
platformCertPk: options.PlatformCertificatePrivateKey
));
}
/// <summary>
/// 使用当前客户端生成一个新的 <see cref="IFlurlRequest"/> 对象。
/// </summary>
/// <param name="request"></param>
/// <param name="method"></param>
/// <param name="urlSegments"></param>
/// <returns></returns>
public IFlurlRequest CreateRequest(WechatTenpayBusinessRequest request, HttpMethod method, params object[] urlSegments)
{
IFlurlRequest flurlRequest = FlurlClient.Request(urlSegments).WithVerb(method);
if (AutoEncryptRequestSensitiveProperty)
{
this.EncryptRequestSensitiveProperty(request);
}
if (request.Timeout != null)
{
flurlRequest.WithTimeout(TimeSpan.FromMilliseconds(request.Timeout.Value));
}
if (request.TBEPEncryption != null)
{
flurlRequest.Headers.Remove("TBEP-Encrypt");
flurlRequest.WithHeader("TBEP-Encrypt", $"enc_key=\"{request.TBEPEncryption.EncryptedKey}\",iv=\"{Convert.ToBase64String(Encoding.UTF8.GetBytes(request.TBEPEncryption.IV))}\",tbep_serial_number=\"{request.TBEPEncryption.CertificateSerialNumber}\",algorithm=\"{request.TBEPEncryption.Algorithm}\"");
}
return flurlRequest;
}
/// <summary>
/// 异步发起请求。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="flurlRequest"></param>
/// <param name="httpContent"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<T> SendRequestAsync<T>(IFlurlRequest flurlRequest, HttpContent? httpContent = null, CancellationToken cancellationToken = default)
where T : WechatTenpayBusinessResponse, new()
{
if (flurlRequest == null) throw new ArgumentNullException(nameof(flurlRequest));
if (httpContent != null)
{
if (string.IsNullOrEmpty(httpContent.Headers.ContentType?.MediaType))
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
try
{
using IFlurlResponse flurlResponse = await base.SendRequestAsync(flurlRequest, httpContent, cancellationToken);
return await WrapResponseWithJsonAsync<T>(flurlResponse, cancellationToken);
}
catch (FlurlHttpException ex)
{
throw new WechatTenpayBusinessException(ex.Message, ex);
}
}
/// <summary>
/// 异步发起请求。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="flurlRequest"></param>
/// <param name="data"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<T> SendRequestWithJsonAsync<T>(IFlurlRequest flurlRequest, object? data = null, CancellationToken cancellationToken = default)
where T : WechatTenpayBusinessResponse, new()
{
if (flurlRequest == null) throw new ArgumentNullException(nameof(flurlRequest));
try
{
bool isSimpleRequest = data == null ||
flurlRequest.Verb == HttpMethod.Get ||
flurlRequest.Verb == HttpMethod.Head ||
flurlRequest.Verb == HttpMethod.Options;
using IFlurlResponse flurlResponse = isSimpleRequest ?
await base.SendRequestAsync(flurlRequest, null, cancellationToken) :
await base.SendRequestWithJsonAsync(flurlRequest, data, cancellationToken);
return await WrapResponseWithJsonAsync<T>(flurlResponse, cancellationToken);
}
catch (FlurlHttpException ex)
{
throw new WechatTenpayBusinessException(ex.Message, ex);
}
}
private new async Task<TResponse> WrapResponseWithJsonAsync<TResponse>(IFlurlResponse flurlResponse, CancellationToken cancellationToken = default)
where TResponse : WechatTenpayBusinessResponse, new()
{
TResponse result = await base.WrapResponseWithJsonAsync<TResponse>(flurlResponse, cancellationToken);
string? strTBEPEncryption = flurlResponse.Headers.GetAll("TBEP-Encrypt").FirstOrDefault();
if (!string.IsNullOrEmpty(strTBEPEncryption))
{
IDictionary<string, string?> dictTBEPEncryption = strTBEPEncryption
.Split(',')
.Select(s => s.Trim().Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries))
.ToDictionary(
k => k[0],
v => v.Length > 1 ? v[1].TrimStart('\"').TrimEnd('\"') : null
);
result.TBEPEncryption = new WechatTenpayBusinessResponseTBEPEncryption();
result.TBEPEncryption.PlatformId = dictTBEPEncryption["platform_id"];
result.TBEPEncryption.EncryptedKey = dictTBEPEncryption["enc_key"];
result.TBEPEncryption.IV = dictTBEPEncryption["iv"];
result.TBEPEncryption.CertificateSerialNumber = dictTBEPEncryption["platform_serial_number"];
result.TBEPEncryption.Algorithm = dictTBEPEncryption["algorithm"];
if (AutoDecryptResponseSensitiveProperty && result.IsSuccessful())
{
this.DecryptResponseSensitiveProperty(result);
}
}
return result;
}
}
}

View File

@ -0,0 +1,80 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 一个用于构造 <see cref="WechatTenpayBusinessClient"/> 时使用的配置项。
/// </summary>
public class WechatTenpayBusinessClientOptions
{
/// <summary>
/// 获取或设置请求超时时间(单位:毫秒)。
/// <para>默认值30000</para>
/// </summary>
public int Timeout { get; set; } = 30 * 1000;
/// <summary>
/// 获取或设置腾讯微企付 API 域名。
/// <para>默认值:<see cref="WechatTenpayBusinessEndpoints.DEFAULT"/></para>
/// </summary>
public string Endpoints { get; set; } = WechatTenpayBusinessEndpoints.DEFAULT;
/// <summary>
/// 获取或设置腾讯微企付 API 签名认证方式。
/// <para>默认值:<see cref="Constants.SignAlgorithms.SHA245_WITH_RSA"/></para>
/// </summary>
public string SignAlgorithm { get; set; } = Constants.SignAlgorithms.SHA245_WITH_RSA;
/// <summary>
/// 获取或设置腾讯微企付平台账号。
/// </summary>
public string PlatformId { get; set; } = default!;
/// <summary>
/// 获取或设置腾讯微企付平台 API 证书序列号。
/// </summary>
public string PlatformCertificateSerialNumber { get; set; } = default!;
/// <summary>
/// 获取或设置腾讯微企付平台 API 证书私钥。
/// </summary>
public string PlatformCertificatePrivateKey { get; set; } = default!;
/// <summary>
/// 获取或设置腾讯微企付证书序列号。
/// </summary>
public string TBEPCertificateSerialNumber { get; set; } = default!;
/// <summary>
/// 获取或设置腾讯微企付证书公钥。
/// </summary>
public string TBEPCertificatePublicKey { get; set; } = default!;
/// <summary>
/// 获取或设置是否自动加密请求中的敏感字段数据。
/// <para>注意:启用该功能需配合 <see cref="SensitivePropertyEncryptionSM4Key"/> 和 <see cref="SensitivePropertyEncryptionSM4IV"/> 使用。</para>
/// </summary>
public bool AutoEncryptRequestSensitiveProperty { get; set; }
/// <summary>
/// 获取或设置自动加密请求重敏感字段数据时使用的算法。
/// <para>默认值:<see cref="Constants.EncryptionAlgorithms.RSA_OAEP_WITH_SM4_128_CBC"/></para>
/// </summary>
public string SensitivePropertyEncryptionAlgorithm { get; set; } = Constants.EncryptionAlgorithms.RSA_OAEP_WITH_SM4_128_CBC;
/// <summary>
/// 获取或设置自动加密请求中的敏感字段数据时 SM4 算法所需密钥。
/// </summary>
public string? SensitivePropertyEncryptionSM4Key { get; set; }
/// <summary>
/// 获取或设置自动加密请求中的敏感字段数据时 SM4 算法所需偏移量。
/// </summary>
public string? SensitivePropertyEncryptionSM4IV { get; set; }
/// <summary>
/// 获取或设置是否自动解密响应中的敏感字段数据。
/// </summary>
public bool AutoDecryptResponseSensitiveProperty { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 腾讯微企付 API 接口域名。
/// </summary>
public static class WechatTenpayBusinessEndpoints
{
/// <summary>
/// 主域名(默认)。
/// </summary>
public const string DEFAULT = "https://api-business.tenpay.com/v3";
/// <summary>
/// 测试域名。
/// </summary>
public const string DEVELOPMENT = "https://dev-api-business.tenpay.com/v3";
}
}

View File

@ -0,0 +1,71 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 表示腾讯微企付 API 回调通知事件的基类。
/// </summary>
[Serializable]
public class WechatTenpayBusinessEvent
{
/// <summary>
/// 获取或设置 API 版本。
/// </summary>
[Newtonsoft.Json.JsonProperty("api_version")]
[System.Text.Json.Serialization.JsonPropertyName("api_version")]
public string? ApiVersion { get; set; }
/// <summary>
/// 获取或设置通知 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("event_id")]
[System.Text.Json.Serialization.JsonPropertyName("event_id")]
public string EventId { get; set; } = default!;
/// <summary>
/// 获取或设置通知类型。
/// </summary>
[Newtonsoft.Json.JsonProperty("event_type")]
[System.Text.Json.Serialization.JsonPropertyName("event_type")]
public string EventType { get; set; } = default!;
/// <summary>
/// 获取或设置是否是生产环境。
/// </summary>
[Newtonsoft.Json.JsonProperty("live_mode")]
[System.Text.Json.Serialization.JsonPropertyName("live_mode")]
public bool IsLiveMode { get; set; }
/// <summary>
/// 获取或设置之前回调通知失败的次数。
/// </summary>
[Newtonsoft.Json.JsonProperty("pending_webhooks")]
[System.Text.Json.Serialization.JsonPropertyName("pending_webhooks")]
[System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)]
public int PendingWebhookTimes { get; set; }
/// <summary>
/// 获取或设置通知创建时间。
/// </summary>
[Newtonsoft.Json.JsonProperty("create_time")]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.RFC3339DateTimeOffsetConverter))]
[System.Text.Json.Serialization.JsonPropertyName("create_time")]
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.RFC3339DateTimeOffsetConverter))]
public DateTimeOffset CreateTime { get; set; }
}
/// <summary>
/// 表示腾讯微企付 API 回调通知事件的基类。
/// </summary>
[Serializable]
public class WechatTenpayBusinessEvent<TEventContent> : WechatTenpayBusinessEvent
where TEventContent : class, new()
{
/// <summary>
/// 获取或设置通知内容。
/// </summary>
[Newtonsoft.Json.JsonProperty("event_content")]
[System.Text.Json.Serialization.JsonPropertyName("event_content")]
public TEventContent EventContent { get; set; } = default!;
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 当调用腾讯微企付 API 出错时引发的异常。
/// </summary>
public class WechatTenpayBusinessException : CommonExceptionBase
{
/// <inheritdoc/>
public WechatTenpayBusinessException()
{
}
/// <inheritdoc/>
public WechatTenpayBusinessException(string message)
: base(message)
{
}
/// <inheritdoc/>
public WechatTenpayBusinessException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,54 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 表示腾讯微企付 API 请求的基类。
/// </summary>
public abstract class WechatTenpayBusinessRequest : ICommonRequest
{
/// <summary>
/// 获取或设置请求超时时间(单位:毫秒)。如果不指定将使用构造 <see cref="WechatTenpayBusinessClient"/> 时的 <see cref="WechatTenpayBusinessClientOptions.Timeout"/> 参数,这在需要指定特定耗时请求(比如上传或下载文件)的超时时间时很有用。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public virtual int? Timeout { get; set; }
/// <summary>
/// 获取或设置请求使用的腾讯微企付敏感字段加密参数。
/// <para>如果启用了 <see cref="WechatTenpayBusinessClientOptions.AutoEncryptRequestSensitiveProperty"/> 参数,将由系统自动生成。</para>
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public virtual WechatTenpayBusinessRequestTBEPEncryption? TBEPEncryption { get; set; }
}
public sealed class WechatTenpayBusinessRequestTBEPEncryption
{
/// <summary>
/// 获取或设置加密后的密钥值。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string EncryptedKey { get; set; } = string.Empty;
/// <summary>
/// 获取或设置 CBC IV。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string IV { get; set; } = string.Empty;
/// <summary>
/// 获取或设置微企付证书序列号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string CertificateSerialNumber { get; set; } = string.Empty;
/// <summary>
/// 获取或设置加密算法。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string Algorithm { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,150 @@
using System.Collections.Generic;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness
{
/// <summary>
/// 表示腾讯微企付 API 响应的基类。
/// </summary>
public abstract class WechatTenpayBusinessResponse : ICommonResponse
{
/// <summary>
///
/// </summary>
int ICommonResponse.RawStatus { get; set; }
/// <summary>
///
/// </summary>
IDictionary<string, string> ICommonResponse.RawHeaders { get; set; } = default!;
/// <summary>
///
/// </summary>
byte[] ICommonResponse.RawBytes { get; set; } = default!;
/// <summary>
/// 获取原始的 HTTP 响应状态码。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public int RawStatus
{
get { return ((ICommonResponse)this).RawStatus; }
internal set { ((ICommonResponse)this).RawStatus = value; }
}
/// <summary>
/// 获取原始的 HTTP 响应表头集合。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public IDictionary<string, string> RawHeaders
{
get { return ((ICommonResponse)this).RawHeaders; }
internal set { ((ICommonResponse)this).RawHeaders = value; }
}
/// <summary>
/// 获取原始的 HTTP 响应正文。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public byte[] RawBytes
{
get { return ((ICommonResponse)this).RawBytes; }
internal set { ((ICommonResponse)this).RawBytes = value; }
}
/// <summary>
/// 获取腾讯微企付 API 返回的敏感字段加密参数。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public WechatTenpayBusinessResponseTBEPEncryption? TBEPEncryption { get; internal set; }
/// <summary>
/// 获取腾讯微企付请求链路 ID。
/// </summary>
[Newtonsoft.Json.JsonProperty("trace_id")]
[System.Text.Json.Serialization.JsonPropertyName("trace_id")]
public virtual string? TraceId { get; set; }
/// <summary>
/// 获取腾讯微企付 API 返回的错误详细信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("error")]
[System.Text.Json.Serialization.JsonPropertyName("error")]
public virtual WechatTenpayBusinessResponseError? Error { get; set; }
/// <summary>
/// 获取一个值,该值指示调用腾讯微企付 API 是否成功(即 HTTP 状态码为 200、202 或 204
/// </summary>
/// <returns></returns>
public virtual bool IsSuccessful()
{
return (RawStatus == 200 || RawStatus == 202 || RawStatus == 204) && string.IsNullOrEmpty(Error?.Code);
}
}
public sealed class WechatTenpayBusinessResponseError
{
/// <summary>
/// 获取或设置错误代码。
/// </summary>
[Newtonsoft.Json.JsonProperty("code")]
[System.Text.Json.Serialization.JsonPropertyName("code")]
public string? Code { get; set; }
/// <summary>
/// 获取或设置错误描述。
/// </summary>
[Newtonsoft.Json.JsonProperty("desc")]
[System.Text.Json.Serialization.JsonPropertyName("desc")]
public string? Description { get; set; }
/// <summary>
/// 获取或设置错误详细信息。
/// </summary>
[Newtonsoft.Json.JsonProperty("detail")]
[System.Text.Json.Serialization.JsonPropertyName("detail")]
public IDictionary<string, string>? Details { get; set; }
}
public sealed class WechatTenpayBusinessResponseTBEPEncryption
{
/// <summary>
/// 获取或设置平台账号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? PlatformId { get; set; }
/// <summary>
/// 获取或设置加密后的密钥值。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? EncryptedKey { get; set; }
/// <summary>
/// 获取或设置 CBC IV。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? IV { get; set; }
/// <summary>
/// 获取或设置平台证书序列号。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? CertificateSerialNumber { get; set; }
/// <summary>
/// 获取或设置加密算法。
/// </summary>
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? Algorithm { get; set; }
}
}

View File

@ -1,6 +1,6 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
{
public static class SignAlgorithms
public static class SignSchemes
{
/// <summary>
/// WECHATPAY2-SHA256-RSA2048。

View File

@ -0,0 +1,72 @@
using System;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
internal static class WechatTenpayClientSignExtensions
{
public static bool VerifySignature(this WechatTenpayClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048, out _);
}
public static bool VerifySignature(this WechatTenpayClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, string strSignScheme)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, strSignScheme, out _);
}
public static bool VerifySignature(this WechatTenpayClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, out Exception? error)
{
return VerifySignature(client, strTimestamp, strNonce, strBody, strSignature, strSerialNumber, Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048, out error);
}
public static bool VerifySignature(this WechatTenpayClient client, string strTimestamp, string strNonce, string strBody, string strSignature, string strSerialNumber, string strSignScheme, out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
switch (strSignScheme)
{
case Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048:
{
if (client.PlatformCertificateManager != null)
{
try
{
var cert = client.PlatformCertificateManager.GetEntry(strSerialNumber);
if (!cert.HasValue)
{
error = new Exceptions.WechatTenpayEventVerificationException("There is no platform certificate matched the serial number.");
return false;
}
error = null;
return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(timestamp: strTimestamp, nonce: strNonce, body: strBody),
signature: strSignature
);
}
catch (Exception ex)
{
error = ex;
return false;
}
}
error = new Exception("There is no platform certificate in the certificate manager.");
return false;
}
default:
{
error = new Exception("Unsupported sign scheme.");
return false;
}
}
}
private static string GetPlainTextForSignature(string timestamp, string nonce, string body)
{
return $"{timestamp}\n{nonce}\n{body}\n";
}
}
}

View File

@ -47,38 +47,10 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
if (callbackSignature == null) throw new ArgumentNullException(nameof(callbackSignature));
if (callbackSerialNumber == null) throw new ArgumentNullException(nameof(callbackSerialNumber));
if (client.PlatformCertificateManager != null)
{
try
{
var cert = client.PlatformCertificateManager.GetEntry(callbackSerialNumber);
if (!cert.HasValue)
{
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate matched the serial number.");
return false;
}
error = null;
return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody),
signature: callbackSignature
);
}
catch (Exception ex)
{
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", ex);
return false;
}
}
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate in the manager.");
return false;
}
private static string GetPlainTextForSignature(string timestamp, string nonce, string body)
{
return $"{timestamp}\n{nonce}\n{body}\n";
bool ret = WechatTenpayClientSignExtensions.VerifySignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, out error);
if (error != null)
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", error);
return ret;
}
}
}

View File

@ -79,7 +79,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
newValue = Utilities.RSAUtility.EncryptWithECBByCertificate(
certificate: certificate,
plainText: oldValue,
paddingAlgorithm: "PKCS1PADDING"
paddingMode: "PKCS1PADDING"
);
}

View File

@ -81,7 +81,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
newValue = Utilities.RSAUtility.DecryptWithECB(
privateKey: client.Credentials.MerchantCertificatePrivateKey,
cipherText: oldValue,
paddingAlgorithm: "PKCS1PADDING"
paddingMode: "PKCS1PADDING"
);
}

View File

@ -84,38 +84,10 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
if (responseSignature == null) throw new ArgumentNullException(nameof(responseSignature));
if (responseSerialNumber == null) throw new ArgumentNullException(nameof(responseSerialNumber));
if (client.PlatformCertificateManager != null)
{
try
{
var cert = client.PlatformCertificateManager.GetEntry(responseSerialNumber)!;
if (!cert.HasValue)
{
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate matched the serial number.");
return false;
}
error = null;
return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(timestamp: responseTimestamp, nonce: responseNonce, body: responseBody),
signature: responseSignature
);
}
catch (Exception ex)
{
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", ex);
return false;
}
}
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate in the manager.");
return false;
}
private static string GetPlainTextForSignature(string timestamp, string nonce, string body)
{
return $"{timestamp}\n{nonce}\n{body}\n";
bool ret = WechatTenpayClientSignExtensions.VerifySignature(client, responseTimestamp, responseNonce, responseBody, responseSignature, responseSerialNumber, out error);
if (error != null)
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", error);
return ret;
}
}
}

View File

@ -51,7 +51,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Interceptors
switch (_scheme)
{
case Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048:
case Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048:
{
try
{

View File

@ -113,15 +113,15 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
/// </summary>
/// <param name="privateKeyBytes">PKCS#8 私钥字节数据。</param>
/// <param name="cipherBytes">待解密的数据字节数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>解密后的数据字节数组。</returns>
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (privateKeyBytes == null) throw new ArgumentNullException(nameof(privateKeyBytes));
if (cipherBytes == null) throw new ArgumentNullException(nameof(cipherBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes);
return DecryptWithECB(rsaKeyParams, cipherBytes, paddingAlgorithm);
return DecryptWithECB(rsaKeyParams, cipherBytes, paddingMode);
}
/// <summary>
@ -129,16 +129,16 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
/// </summary>
/// <param name="privateKey">PKCS#8 私钥PEM 格式)。</param>
/// <param name="cipherText">经 Base64 编码的待解密数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>解密后的文本数据。</returns>
public static string DecryptWithECB(string privateKey, string cipherText, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
public static string DecryptWithECB(string privateKey, string cipherText, string paddingMode = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (privateKey == null) throw new ArgumentNullException(nameof(privateKey));
if (cipherText == null) throw new ArgumentNullException(nameof(cipherText));
byte[] privateKeyBytes = ConvertPkcs8PrivateKeyToByteArray(privateKey);
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingAlgorithm);
byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingMode);
return Encoding.UTF8.GetString(plainBytes);
}
@ -147,15 +147,15 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
/// </summary>
/// <param name="publicKeyBytes">PKCS#8 公钥字节数据。</param>
/// <param name="plainBytes">待加密的数据字节数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>加密后的数据字节数组。</returns>
public static byte[] EncryptWithECB(byte[] publicKeyBytes, byte[] plainBytes, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
public static byte[] EncryptWithECB(byte[] publicKeyBytes, byte[] plainBytes, string paddingMode = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (publicKeyBytes == null) throw new ArgumentNullException(nameof(publicKeyBytes));
if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes));
RsaKeyParameters rsaKeyParams = (RsaKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes);
return EncryptWithECB(rsaKeyParams, plainBytes, paddingAlgorithm);
return EncryptWithECB(rsaKeyParams, plainBytes, paddingMode);
}
/// <summary>
@ -163,16 +163,16 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
/// </summary>
/// <param name="publicKey">PKCS#8 公钥PEM 格式)。</param>
/// <param name="plainText">待加密的文本数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>经 Base64 编码的加密数据。</returns>
public static string EncryptWithECB(string publicKey, string plainText, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
public static string EncryptWithECB(string publicKey, string plainText, string paddingMode = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (publicKey == null) throw new ArgumentNullException(nameof(publicKey));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
byte[] publicKeyBytes = ConvertPkcs8PublicKeyToByteArray(publicKey);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = EncryptWithECB(publicKeyBytes, plainBytes, paddingAlgorithm);
byte[] cipherBytes = EncryptWithECB(publicKeyBytes, plainBytes, paddingMode);
return Convert.ToBase64String(cipherBytes);
}
@ -181,16 +181,16 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
/// </summary>
/// <param name="certificate">证书PEM 格式)。</param>
/// <param name="plainText">待加密的文本数据。</param>
/// <param name="paddingAlgorithm">填充算法。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <param name="paddingMode">填充模式。(默认值:<see cref="RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1"/></param>
/// <returns>经 Base64 编码的加密数据。</returns>
public static string EncryptWithECBByCertificate(string certificate, string plainText, string paddingAlgorithm = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
public static string EncryptWithECBByCertificate(string certificate, string plainText, string paddingMode = RSA_CIPHER_PADDING_OAEP_WITH_SHA1_AND_MGF1)
{
if (certificate == null) throw new ArgumentNullException(nameof(certificate));
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
RsaKeyParameters rsaKeyParams = ConvertCertificateToPublicKeyParams(certificate);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = EncryptWithECB(rsaKeyParams, plainBytes, paddingAlgorithm);
byte[] cipherBytes = EncryptWithECB(rsaKeyParams, plainBytes, paddingMode);
return Convert.ToBase64String(cipherBytes);
}
@ -305,16 +305,16 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
return signer.VerifySignature(signBytes);
}
private static byte[] EncryptWithECB(RsaKeyParameters rsaKeyParams, byte[] plainBytes, string paddingAlgorithm)
private static byte[] EncryptWithECB(RsaKeyParameters rsaKeyParams, byte[] plainBytes, string paddingMode)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingAlgorithm}");
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingMode}");
cipher.Init(true, rsaKeyParams);
return cipher.DoFinal(plainBytes);
}
private static byte[] DecryptWithECB(RsaKeyParameters rsaKeyParams, byte[] cipherBytes, string paddingAlgorithm)
private static byte[] DecryptWithECB(RsaKeyParameters rsaKeyParams, byte[] cipherBytes, string paddingMode)
{
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingAlgorithm}");
IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_ECB}/{paddingMode}");
cipher.Init(false, rsaKeyParams);
return cipher.DoFinal(cipherBytes);
}

View File

@ -56,7 +56,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
FlurlClient.WithTimeout(TimeSpan.FromMilliseconds(options.Timeout));
Interceptors.Add(new Interceptors.WechatTenpayRequestSignatureInterceptor(
scheme: options.SignAlgorithm,
scheme: options.SignScheme,
mchId: options.MerchantId,
mchCertSn: options.MerchantCertificateSerialNumber,
mchCertPk: options.MerchantCertificatePrivateKey

View File

@ -34,9 +34,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// 获取或设置微信支付 API 签名认证方式。
/// <para>默认值:<see cref="Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048"/></para>
/// <para>默认值:<see cref="Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048"/></para>
/// </summary>
public string SignAlgorithm { get; set; } = Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048;
public string SignScheme { get; set; } = Constants.SignSchemes.WECHATPAY2_SHA256_RSA2048;
/// <summary>
/// 获取或设置微信支付商户号。
@ -51,7 +51,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// <i>(本参数已废弃,请使用 <see cref="MerchantCertificateSerialNumber"/> 参数)</i>
/// </summary>
[Obsolete("本参数已废弃,请使用 `MerchantCertificateSerialNumber` 参数")]
[Obsolete("本参数已废弃,请使用 `MerchantCertificateSerialNumber` 参数", error: true)]
public string MerchantCertSerialNumber { get { return MerchantCertificateSerialNumber; } set { MerchantCertificateSerialNumber = value; } }
/// <summary>
@ -62,7 +62,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// <i>(本参数已废弃,请使用 <see cref="MerchantCertificatePrivateKey"/> 参数)</i>
/// </summary>
[Obsolete("本参数已废弃,请使用 `MerchantCertificatePrivateKey` 参数")]
[Obsolete("本参数已废弃,请使用 `MerchantCertificatePrivateKey` 参数", error: true)]
public string MerchantCertPrivateKey { get { return MerchantCertificatePrivateKey; } set { MerchantCertificatePrivateKey = value; } }
/// <summary>
@ -85,7 +85,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// <i>(本参数已废弃,请使用 <see cref="PlatformCertificateManager"/> 参数)</i>
/// </summary>
[Obsolete("本参数已废弃,请使用 `PlatformCertificateManager` 参数")]
[Obsolete("本参数已废弃,请使用 `PlatformCertificateManager` 参数", error: true)]
public Settings.CertificateManager CertificateManager { get { return PlatformCertificateManager; } set { PlatformCertificateManager = value; } }
/// <summary>

View File

@ -17,7 +17,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// <i>(本参数已废弃,请使用 <see cref="WechatpayCertificateSerialNumber"/> 参数)</i>
/// </summary>
[Obsolete("本参数已废弃,请使用 `WechatpayCertificateSerialNumber` 参数")]
[Obsolete("本参数已废弃,请使用 `WechatpayCertificateSerialNumber` 参数", error: true)]
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string? WechatpayCertSerialNumber { get { return WechatpayCertificateSerialNumber; } set { WechatpayCertificateSerialNumber = value; } }

View File

@ -87,7 +87,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <summary>
/// <i>(本参数已废弃,请使用 <see cref="WechatpayCertificateSerialNumber"/> 参数)</i>
/// </summary>
[Obsolete("本参数已废弃,请使用 `WechatpayCertificateSerialNumber` 参数")]
[Obsolete("本参数已废弃,请使用 `WechatpayCertificateSerialNumber` 参数", error: true)]
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string WechatpayCertSerialNumber { get { return WechatpayCertificateSerialNumber; } set { WechatpayCertificateSerialNumber = value; } }

View File

@ -0,0 +1 @@
appsettings.local.json

View File

@ -0,0 +1,18 @@
{
"api_version": "",
"event_id": "",
"event_type": "pay.succeeded",
"live_mode": false,
"pending_webhooks": 0,
"create_time": "2020-01-01T01:02:03+08:00",
"event_content": {
"payment_id": "",
"purchaser_id": "",
"out_payment_id": "",
"bargainor_ent_id": "",
"amount": 0,
"status": "",
"pay_succ_time": "2020-01-01T01:02:03+08:00",
"failed_reason": ""
}
}

View File

@ -0,0 +1,25 @@
{
"api_version": "",
"event_id": "",
"event_type": "product.application.finish",
"live_mode": false,
"pending_webhooks": 0,
"create_time": "2020-01-01T01:02:03+08:00",
"event_content": {
"request_no": "",
"out_request_no": "",
"ent_id": "",
"status": "",
"product_details": [
{
"product_name": "",
"status": "",
"accounts": [
{
"ent_acct_id": ""
}
]
}
]
}
}

View File

@ -0,0 +1,5 @@
{
"download_url": "",
"expire_time": "2021-06-08T10:34:56+08:00",
"bill_status": ""
}

View File

@ -0,0 +1,6 @@
{
"payment_id": "",
"out_payment_id": "",
"status": "",
"close_time": "2021-06-08T10:34:56+08:00"
}

View File

@ -0,0 +1,33 @@
{
"purchaser_type": "",
"out_payment_id": "",
"amount": 0,
"currency": "",
"expire_time": "2021-06-08T10:34:56+08:00",
"payee": {
"ent_id": "",
"ent_name": "",
"ent_acct_id": "",
"bank_account_number_last4": ""
},
"memo": "",
"goods": [
{
"good_name": "",
"good_number": "0",
"good_amount": "0"
}
],
"attachment": "",
"risk_control": {
"device_id": "",
"payer_client_ip": "",
"payer_ua": "",
"create_time": "2021-06-08T10:34:56+08:00",
"pick_type": "",
"pick_description": ""
},
"notify_url": {
"server_notify_url": ""
}
}

View File

@ -0,0 +1,14 @@
{
"payment_id": "",
"out_payment_id": "",
"amount": 0,
"currency": "",
"payee": {
"ent_id": "",
"ent_name": "",
"ent_acct_id": "",
"bank_account_number_last4": ""
},
"pay_status": "",
"memo": ""
}

View File

@ -0,0 +1,20 @@
{
"payment_id": "",
"out_payment_id": "",
"user_openid": "",
"amount": 0,
"currency": "",
"payee": {
"ent_id": "",
"ent_name": "",
"ent_acct_id": "",
"bank_account_number_last4": ""
},
"pay_status": "",
"memo": "",
"failed_reason": {
"failed_type": "",
"failed_detail": ""
},
"attachment": ""
}

View File

@ -0,0 +1,6 @@
{
"pc_web": {
"url": "",
"expire_time": "2020-01-01T01:02:03+08:00"
}
}

View File

@ -0,0 +1,42 @@
{
"out_request_no": "",
"business_license": {
"business_register_type": "",
"unified_social_credit_code": "",
"merchant_name": "",
"merchant_short_name": "",
"legal_person_name": "",
"validity_period": [ "2024-01-01", "长期" ],
"photocopy_file_id": ""
},
"legal_person_id_card": {
"name": "",
"number": "",
"validity_period": [ "2024-01-01", "长期" ],
"front_photocopy_file_id": "",
"back_photocopy_file_id": ""
},
"contact_info": {
"mobile_number": ""
},
"payee_accounts": [
{
"account_type": "",
"bank_account_name": "",
"bank_account_number": "",
"bank_name": "",
"bank_branch_id": "",
"bank_branch_name": ""
}
],
"products": [
{
"product_name": ""
}
],
"notify_url": {
"server_notify_url": "",
"web_success_url": "",
"web_refresh_url": ""
}
}

View File

@ -0,0 +1,17 @@
{
"request_no": "",
"out_request_no": "",
"ent_id": "",
"status": "",
"product_details": [
{
"product_name": "",
"status": "",
"accounts": [
{
"ent_acct_id": ""
}
]
}
]
}

View File

@ -0,0 +1,19 @@
{
"pc_web": {
"url": "",
"expire_time": "2020-01-01T01:02:03+08:00"
},
"pc_plugin": {
"key": "",
"expire_time": "2020-01-01T01:02:03+08:00"
},
"wx_qrcode": {
"url": ""
},
"miniprogram": {
"mp_path": "",
"mp_appid": "",
"mp_username": "",
"expire_time": "2020-01-01T01:02:03+08:00"
}
}

View File

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Remove=".gitignore" />
<None Remove="appsettings.local.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
<Content Include="appsettings.*.json" Condition="'$(Configuration)' == 'Debug'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="ModelSamples/**/*.json" />
<Content Include="EventSamples/**/*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.TenpayBusiness\SKIT.FlurlHttpClient.Wechat.TenpayBusiness.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.TenpayBusiness",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests
{
public class TestCase_SM3UtilityTests
{
[Fact(DisplayName = "测试用例:计算 SM3 哈希值")]
public void TestSM3Hash()
{
byte[] msgBytes = Convert.FromBase64String("QXdlc29tZSBTS0lULkZsdXJsSHR0cENsaWVudC5XZWNoYXQuVGVucGF5QnVzaW5lc3Mh");
string expectedHashText = "A7A58FCEDDDEE4BD2E05887E5F4D8B7D662357BE474F3821CA858EE1CFFB4B83";
string actualHashText = BitConverter.ToString(Utilities.SM3Utility.Hash(msgBytes)).Replace("-", "");
Assert.Equal(actualHashText, expectedHashText, ignoreCase: true);
}
}
}

View File

@ -0,0 +1,33 @@
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests
{
public class TestCase_SM4UtilityTests
{
[Fact(DisplayName = "测试用例SM4 加密")]
public void TestSM4Encrypt()
{
string key = "MDAwMDAwMDAwMDAwMDAwMA==";
string iv = "OGE2YzRkZGQ4YTZjNGRkZA==";
string plainText = "Awesome SKIT.FlurlHttpClient.Wechat.TenpayBusiness!";
string actualCipher = Utilities.SM4Utility.EncryptWithCBC(key: key, iv: iv, plainText: plainText);
string expectedCipher = "Fm3z4Ipjuaj4oQLfxpTrvoZm5JdbjvjrJo3PRhvSsOppk8/PN+izH3Wo9Rz6V85mpq6X1cGul8U7jjaAl1PWpg==";
Assert.Equal(expectedCipher, actualCipher);
}
[Fact(DisplayName = "测试用例SM4 解密")]
public void TestSM4Decrypt()
{
string key = "MDAwMDAwMDAwMDAwMDAwMA==";
string iv = "OGE2YzRkZGQ4YTZjNGRkZA==";
string cipherText = "Fm3z4Ipjuaj4oQLfxpTrvoZm5JdbjvjrJo3PRhvSsOppk8/PN+izH3Wo9Rz6V85mpq6X1cGul8U7jjaAl1PWpg==";
string actualPlain = Utilities.SM4Utility.DecryptWithCBC(key: key, iv: iv, cipherText: cipherText);
string expectedPlain = "Awesome SKIT.FlurlHttpClient.Wechat.TenpayBusiness!";
Assert.Equal(expectedPlain, actualPlain);
}
}
}

View File

@ -0,0 +1,19 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayBusiness.UnitTests
{
internal class TestClients
{
static TestClients()
{
Instance = new WechatTenpayBusinessClient(new WechatTenpayBusinessClientOptions()
{
PlatformId = TestConfigs.WechatPlatformId,
PlatformCertificateSerialNumber = TestConfigs.WechatPlatformCertSerialNumber,
PlatformCertificatePrivateKey = TestConfigs.WechatPlatformCertPrivateKey,
TBEPCertificateSerialNumber = TestConfigs.WechatTBEPCertSerialNumber,
TBEPCertificatePublicKey = TestConfigs.WechatTBEPCertPrivateKey
});
}
public static readonly WechatTenpayBusinessClient Instance;
}
}

Some files were not shown because too many files have changed in this diff Show More