From 7205686f7190584af1e08e87c99ee2ae61d27655 Mon Sep 17 00:00:00 2001 From: RHQYZ Date: Thu, 20 Jan 2022 23:20:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenpayv2):=20=E5=AF=BC=E5=85=A5=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LOGO.png | Bin 0 -> 31499 bytes SKIT.FlurlHttpClient.Wechat.sln | 14 + .../Properties/AssemblyInfo.cs | 3 + .../Models/Platform_NLP/NLPNERResponse.cs | 114 ++++- .../{ => Properties}/AssemblyInfo.cs | 0 .../Constants/SignTypes.cs | 15 + .../Boolean/YesOrNoBooleanConverter.cs | 29 ++ .../YesOrNoNullableBooleanConverter.cs | 50 ++ .../PureDigitalTextDateTimeOffsetConverter.cs | 29 ++ ...italTextNullableDateTimeOffsetConverter.cs | 55 +++ .../FlattenNArrayObjectConverterBase.cs | 222 +++++++++ .../Boolean/YesOrNoBooleanConverter.cs} | 9 +- .../YesOrNoNullableBooleanConverter.cs} | 10 +- .../PureDigitalTextDateTimeOffsetConverter.cs | 19 + ...italTextNullableDateTimeOffsetConverter.cs | 40 ++ .../FlattenNArrayObjectConverterBase.cs | 206 +++++++++ ...yClientExecuteMerchantCustomsExtensions.cs | 68 +++ .../WechatTenpayClientExecutePayExtensions.cs | 32 ++ ...hatTenpayClientExecutePayITILExtensions.cs | 30 ++ ...echatTenpayClientExecuteToolsExtensions.cs | 31 ++ ...MerchantCustomsCustomDeclarationRequest.cs | 106 +++++ ...erchantCustomsCustomDeclarationResponse.cs | 75 +++ ...MerchantCustomsCustomDeclarationRequest.cs | 43 ++ ...erchantCustomsCustomDeclarationResponse.cs | 157 +++++++ ...MerchantCustomsCustomDeclarationRequest.cs | 50 ++ ...erchantCustomsCustomDeclarationResponse.cs | 61 +++ .../Models/Pay/CreatePayMicroPayRequest.cs | 288 ++++++++++++ .../Models/Pay/CreatePayMicroPayResponse.cs | 270 +++++++++++ .../PayITIL/SubmitPayITILReportRequest.cs | 97 ++++ .../PayITIL/SubmitPayITILReportResponse.cs | 270 +++++++++++ .../Tools/ToolsAuthCodeToOpenIdRequest.cs | 29 ++ .../Tools/ToolsAuthCodeToOpenIdResponse.cs | 36 ++ .../Properties/AssemblyInfo.cs | 3 + .../README.md | 31 ++ ...KIT.FlurlHttpClient.Wechat.TenpayV2.csproj | 47 ++ .../Settings/Credentials.cs | 43 ++ .../Settings/HttpClientFactory.cs | 37 ++ .../Utilities/HMACUtility.cs | 44 ++ .../Utilities/Internal/JsonUtility.cs | 431 ++++++++++++++++++ .../Utilities/Internal/XmlUtility.cs | 21 + .../Utilities/MD5Utility.cs | 39 ++ .../WechatTenpayClient.cs | 153 +++++++ .../WechatTenpayClientOptions.cs | 46 ++ .../WechatTenpayEndpoints.cs | 23 + .../WechatTenpayException.cs | 27 ++ .../WechatTenpayRequest.cs | 56 +++ .../WechatTenpayResponse.cs | 149 ++++++ .../TextualStringIListWithCommaConverter.cs | 53 ++- ...ryMarketingBusifavorUserCouponsResponse.cs | 4 +- ...oreServiceOrderByOutOrderNumberResponse.cs | 2 +- .../{ => Properties}/AssemblyInfo.cs | 0 ...rchantCustomsCustomDeclarationRequest.json | 14 + ...chantCustomsCustomDeclarationResponse.json | 20 + ...rchantCustomsCustomDeclarationRequest.json | 11 + ...chantCustomsCustomDeclarationResponse.json | 38 ++ ...rchantCustomsCustomDeclarationRequest.json | 8 + ...chantCustomsCustomDeclarationResponse.json | 18 + .../Pay/CreatePayMicroPayRequest.json | 17 + .../Pay/CreatePayMicroPayResponse.json | 21 + .../PayITIL/SubmitPayITILReportRequest.json | 10 + .../PayITIL/SubmitPayITILReportResponse.json | 5 + .../Tools/ToolsAuthCodeToOpenIdRequest.json | 9 + .../Tools/ToolsAuthCodeToOpenIdResponse.json | 14 + ...ttpClient.Wechat.TenpayV2.UnitTests.csproj | 39 ++ .../TestCase_CodeReviewAnalyzer.cs | 57 +++ .../TestCase_JsonConverterTest.cs | 85 ++++ .../TestClients.cs | 16 + .../TestConfigs.cs | 43 ++ .../appsettings.json | 10 + .../appsettings.local.json | 10 + .../CodeStyleUtil.cs | 20 +- ...IT.FlurlHttpClient.Wechat.TestTools.csproj | 5 +- 72 files changed, 4092 insertions(+), 45 deletions(-) create mode 100644 LOGO.png create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/Properties/AssemblyInfo.cs rename src/SKIT.FlurlHttpClient.Wechat.OpenAI/{ => Properties}/AssemblyInfo.cs (100%) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Constants/SignTypes.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoBooleanConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoNullableBooleanConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs rename src/{SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedBooleanConverter.cs => SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoBooleanConverter.cs} (61%) rename src/{SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedNullableBooleanConverter.cs => SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoNullableBooleanConverter.cs} (73%) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteMerchantCustomsExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayITILExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteToolsExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdResponse.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Properties/AssemblyInfo.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/README.md create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/Credentials.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/HttpClientFactory.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/HMACUtility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/JsonUtility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/XmlUtility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/MD5Utility.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClient.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClientOptions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayEndpoints.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayResponse.cs rename src/SKIT.FlurlHttpClient.Wechat.TenpayV3/{ => Properties}/AssemblyInfo.cs (100%) create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdRequest.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdResponse.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_CodeReviewAnalyzer.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterTest.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestClients.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestConfigs.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.json create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.local.json diff --git a/LOGO.png b/LOGO.png new file mode 100644 index 0000000000000000000000000000000000000000..ebcd2d3fa982690b27e81aea66c7306f2381c1e0 GIT binary patch literal 31499 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 literal 0 HcmV?d00001 diff --git a/SKIT.FlurlHttpClient.Wechat.sln b/SKIT.FlurlHttpClient.Wechat.sln index dccc6969..9a1259d8 100644 --- a/SKIT.FlurlHttpClient.Wechat.sln +++ b/SKIT.FlurlHttpClient.Wechat.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3E34ADB9-1F5 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api", "src\SKIT.FlurlHttpClient.Wechat.Api\SKIT.FlurlHttpClient.Wechat.Api.csproj", "{082C1F69-7932-473F-A700-49584371BE8C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV2", "src\SKIT.FlurlHttpClient.Wechat.TenpayV2\SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj", "{18DEF654-1EDF-46C7-8430-685D6236E9C5}" +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.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}" @@ -28,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj", "{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj", "{574A567A-6D2C-49F6-9A98-0133CA9B007D}" +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.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}" @@ -54,6 +58,10 @@ Global {082C1F69-7932-473F-A700-49584371BE8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {082C1F69-7932-473F-A700-49584371BE8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {082C1F69-7932-473F-A700-49584371BE8C}.Release|Any CPU.Build.0 = Release|Any CPU + {18DEF654-1EDF-46C7-8430-685D6236E9C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18DEF654-1EDF-46C7-8430-685D6236E9C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18DEF654-1EDF-46C7-8430-685D6236E9C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18DEF654-1EDF-46C7-8430-685D6236E9C5}.Release|Any CPU.Build.0 = Release|Any CPU {6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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 @@ -106,12 +114,17 @@ Global {7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Release|Any CPU.Build.0 = Release|Any CPU + {574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {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} {CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} {7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} @@ -126,6 +139,7 @@ Global {D1B321C9-3004-4645-A78D-A85C152062FA} = {35C901ED-C234-4A91-9561-AD89B3BB788D} {65E51735-73CE-4E9B-AA65-4BF5E4C8A705} = {35C901ED-C234-4A91-9561-AD89B3BB788D} {7667F0D3-B41D-43C2-B69D-A68FE230EBF7} = {35C901ED-C234-4A91-9561-AD89B3BB788D} + {574A567A-6D2C-49F6-9A98-0133CA9B007D} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/Properties/AssemblyInfo.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..2c280722 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SKIT.FlurlHttpClient.Wechat.Ads.UnitTests")] \ No newline at end of file diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/Platform_NLP/NLPNERResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/Platform_NLP/NLPNERResponse.cs index 060fbe64..31fcc298 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/Platform_NLP/NLPNERResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Models/Platform_NLP/NLPNERResponse.cs @@ -1,5 +1,10 @@ using System; using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Nodes; namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models.Platform { @@ -38,7 +43,8 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models.Platform /// [Newtonsoft.Json.JsonProperty("norm")] [System.Text.Json.Serialization.JsonPropertyName("norm")] - public object Norm { get; set; } = default!; + [System.Text.Json.Serialization.JsonConverter(typeof(DynamicObjectConverter))] + public dynamic Norm { get; set; } = default!; } } @@ -49,4 +55,110 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Models.Platform [System.Text.Json.Serialization.JsonPropertyName("result")] public Types.Result[] ResultList { get; set; } = default!; } + + public class DynamicObjectConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + return base.CanConvert(typeToConvert) || typeof(IDynamicMetaObjectProvider).IsAssignableFrom(typeToConvert); + } + + public override dynamic? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return ReadValue(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, dynamic? value, JsonSerializerOptions options) + { + } + + private object? ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.None: + case JsonTokenType.Null: + return null; + + case JsonTokenType.True: + return true; + + case JsonTokenType.False: + return false; + + case JsonTokenType.Number: + return reader.TryGetInt64(out long longValue) ? longValue : reader.GetDouble(); + + case JsonTokenType.String: + return reader.GetString(); + + case JsonTokenType.StartObject: + return ReadObject(ref reader, options); + + case JsonTokenType.StartArray: + return ReadArray(ref reader, options); + + default: + return JsonNode.Parse(ref reader, new JsonNodeOptions() { PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive }); + } + } + + private object? ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + IDictionary expandoObject = new ExpandoObject(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.PropertyName: + { + string key = reader.GetString()!; + if (!reader.Read()) + { + throw new JsonException("Unexpected end when reading ExpandoObject."); + } + + object? value = ReadValue(ref reader, options); + expandoObject[key] = value; + } + break; + + case JsonTokenType.Comment: + break; + + case JsonTokenType.EndObject: + return expandoObject; + } + } + + throw new JsonException("Unexpected end when reading ExpandoObject."); + } + + private object? ReadArray(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + IList list = new List(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.Comment: + break; + + case JsonTokenType.EndArray: + return list.ToArray(); + + default: + { + object? element = ReadValue(ref reader, options); + list.Add(element); + } + break; + } + } + + throw new JsonException("Unexpected end when reading ExpandoObject."); + } + } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/AssemblyInfo.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Properties/AssemblyInfo.cs similarity index 100% rename from src/SKIT.FlurlHttpClient.Wechat.OpenAI/AssemblyInfo.cs rename to src/SKIT.FlurlHttpClient.Wechat.OpenAI/Properties/AssemblyInfo.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Constants/SignTypes.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Constants/SignTypes.cs new file mode 100644 index 00000000..e1008a56 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Constants/SignTypes.cs @@ -0,0 +1,15 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Constants +{ + public static class SignTypes + { + /// + /// MD5。 + /// + public const string MD5 = "MD5"; + + /// + /// HMAC-SHA256。 + /// + public const string HMAC_SHA256 = "HMAC-SHA256"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoBooleanConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoBooleanConverter.cs new file mode 100644 index 00000000..b2e3afb3 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoBooleanConverter.cs @@ -0,0 +1,29 @@ +using System; + +namespace Newtonsoft.Json.Converters +{ + internal class YesOrNoBooleanConverter : JsonConverter + { + private readonly JsonConverter _converter = new YesOrNoNullableBooleanConverter(); + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return _converter.ReadJson(reader, objectType, existingValue, hasExistingValue, serializer) ?? default; + } + + public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer) + { + _converter.WriteJson(writer, value, serializer); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoNullableBooleanConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoNullableBooleanConverter.cs new file mode 100644 index 00000000..7be9f420 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Boolean/YesOrNoNullableBooleanConverter.cs @@ -0,0 +1,50 @@ +using System; + +namespace Newtonsoft.Json.Converters +{ + internal class YesOrNoNullableBooleanConverter : JsonConverter + { + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override bool? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, bool? existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer) + { + if (reader.TokenType == Newtonsoft.Json.JsonToken.Null) + { + return existingValue; + } + else if (reader.TokenType == Newtonsoft.Json.JsonToken.Boolean) + { + return serializer.Deserialize(reader); + } + else if (reader.TokenType == Newtonsoft.Json.JsonToken.String) + { + string? value = serializer.Deserialize(reader); + if (value == null) + return existingValue; + + if ("Y".Equals(value)) + return true; + else if ("N".Equals(value)) + return false; + } + + throw new Newtonsoft.Json.JsonReaderException(); + } + + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, bool? value, Newtonsoft.Json.JsonSerializer serializer) + { + if (value.HasValue) + writer.WriteValue(value.Value ? "Y" : "N"); + else + writer.WriteNull(); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs new file mode 100644 index 00000000..81a70998 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs @@ -0,0 +1,29 @@ +using System; + +namespace Newtonsoft.Json.Converters +{ + internal class PureDigitalTextDateTimeOffsetConverter : JsonConverter + { + private readonly JsonConverter _converter = new PureDigitalTextNullableDateTimeOffsetConverter(); + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override DateTimeOffset ReadJson(JsonReader reader, Type objectType, DateTimeOffset existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return _converter.ReadJson(reader, objectType, existingValue, hasExistingValue, serializer) ?? default; + } + + public override void WriteJson(JsonWriter writer, DateTimeOffset value, JsonSerializer serializer) + { + _converter.WriteJson(writer, value, serializer); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs new file mode 100644 index 00000000..0ec15768 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; + +namespace Newtonsoft.Json.Converters +{ + internal class PureDigitalTextNullableDateTimeOffsetConverter : JsonConverter + { + internal const string DATETIME_FORMAT = "yyyyMMddHHmmss"; + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override DateTimeOffset? ReadJson(JsonReader reader, Type objectType, DateTimeOffset? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return existingValue; + } + else if (reader.TokenType == JsonToken.String) + { + string? value = serializer.Deserialize(reader); + if (value == null) + return existingValue; + + if (DateTimeOffset.TryParseExact(value, DATETIME_FORMAT, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out DateTimeOffset result)) + return result; + + if (DateTimeOffset.TryParse(value, out result)) + return result; + } + else if (reader.TokenType == JsonToken.Date) + { + reader.DateFormatString = DATETIME_FORMAT; + return serializer.Deserialize(reader); + } + + throw new JsonReaderException(); + } + + public override void WriteJson(JsonWriter writer, DateTimeOffset? value, JsonSerializer serializer) + { + if (value.HasValue) + writer.WriteValue(value.Value.ToString(DATETIME_FORMAT, DateTimeFormatInfo.InvariantInfo)); + else + writer.WriteNull(); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs new file mode 100644 index 00000000..59673084 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; + +namespace Newtonsoft.Json.Converters +{ + internal static class FlattenNArrayObjectConverterBase + { + public const string PROPERTY_WILDCARD_NARRAY_ELEMENT = "$n"; + public const string PROPERTY_NAME_NARRAY = "#n"; + } + + internal abstract partial class FlattenNArrayObjectConverterBase : JsonConverter + where T : class, new() + { + private sealed class InnerTypedJsonProperty + { + public string PropertyName { get; } + + public PropertyInfo PropertyInfo { get; } + + public Type PropertyType { get { return PropertyInfo.PropertyType; } } + + public bool IsNArrayProperty { get; } + + public InnerTypedJsonProperty(string propertyName, PropertyInfo propertyInfo, bool isNArrayProperty) + { + PropertyName = propertyName; + PropertyInfo = propertyInfo; + IsNArrayProperty = isNArrayProperty; + } + } + + private const string PROPERTY_WILDCARD_NARRAY_ELEMENT = FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; + private const string PROPERTY_NAME_NARRAY = FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; + + private static readonly Hashtable _mappedTypeJsonProperties = new Hashtable(); + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return existingValue; + } + else if (reader.TokenType == JsonToken.StartObject) + { + InnerTypedJsonProperty[] typedJsonProperties = GetTypedJsonProperties(objectType); + if (typedJsonProperties.Count(p => p.IsNArrayProperty) != 1) + throw new JsonSerializationException("The number of `$n` properties must be only one."); + + JObject jObject = JObject.Load(reader); + T tObject = new T(); + + foreach (JProperty jKey in jObject.Properties()) + { + InnerTypedJsonProperty? typedJsonProperty = typedJsonProperties.SingleOrDefault(e => e.PropertyName == jKey.Name); + if (typedJsonProperty != null) + { + // 处理普通属性 + object? value = serializer is null ? + jObject[typedJsonProperty.PropertyName]?.ToObject(typedJsonProperty.PropertyType) : + jObject[typedJsonProperty.PropertyName]?.ToObject(typedJsonProperty.PropertyType, serializer); + typedJsonProperty.PropertyInfo.SetValue(tObject, value); + } + else if (TryMatchNArrayIndex(jKey.Name, out int index)) + { + // 处理 $n 属性 + InnerTypedJsonProperty narrayJsonProperty = typedJsonProperties.Single(e => e.IsNArrayProperty); + object? value = narrayJsonProperty.PropertyInfo.GetValue(tObject); + + Array array = CreateOrExpandNArray(value, narrayJsonProperty.PropertyType.GetElementType()!, index + 1); + object? element = CreateOrUpdateNArrayElement(array, index, jKey.Name, jKey.Value, serializer); + narrayJsonProperty.PropertyInfo.SetValue(tObject, array); + } + else if (serializer?.MissingMemberHandling == MissingMemberHandling.Error) + { + throw new JsonSerializationException($"Could not find member `{jKey.Name}` on object of type `{objectType.Name}`."); + } + } + + return tObject; + } + + throw new JsonSerializationException(); + } + + public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + throw new NotImplementedException(); + } + + private static InnerTypedJsonProperty[] GetTypedJsonProperties(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string mappedTypeKey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); + InnerTypedJsonProperty[]? typedJsonProperties = (InnerTypedJsonProperty[]?)_mappedTypeJsonProperties[mappedTypeKey]; + + if (typedJsonProperties == null) + { + typedJsonProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => + (p.CanRead && !p.GetCustomAttributes(inherit: true).Any()) && + (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) + ) + .Select(p => + { + string name = p.GetCustomAttribute(inherit: true)?.PropertyName ?? p.Name; + return new InnerTypedJsonProperty + ( + propertyName: name, + propertyInfo: p, + isNArrayProperty: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass + ); + }) + .ToArray(); + _mappedTypeJsonProperties[mappedTypeKey] = typedJsonProperties; + } + + return typedJsonProperties; + } + + private static bool TryMatchNArrayIndex(string key, out int index) + { + Regex regex = new Regex(@"(_)(\d+)", RegexOptions.Compiled); + if (regex.IsMatch(key)) + { + string str = regex.Match(key).Groups[2].Value; + index = int.Parse(str); + return true; + } + + index = -1; + return false; + } + + private static Array CreateOrExpandNArray(object? array, Type elementType, int capacity) + { + if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + + if (array == null) + { + return Array.CreateInstance(elementType, capacity); + } + + Array src = (Array)array; + if (src.Length < capacity) + { + Array dst = Array.CreateInstance(elementType, capacity); + Array.Copy(src, dst, src.Length); + return dst; + } + + return src; + } + + private static object CreateOrUpdateNArrayElement(Array array, int index, string jKey, JToken? jValue, JsonSerializer? serializer = null) + { + if (array == null) throw new ArgumentNullException(nameof(array)); + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); + + object? element = array.GetValue(index); + Type elementType = array.GetType().GetElementType()!; + + if (element == null) + { + + if (elementType.IsAbstract || elementType.IsInterface) + { + throw new NotSupportedException(); + } + else if (elementType.IsArray) + { + element = Array.CreateInstance(elementType, 0); + } + else + { + element = Activator.CreateInstance(elementType); + } + + array.SetValue(element, index); + } + + InnerTypedJsonProperty? typedJsonProperty = GetTypedJsonProperties(elementType) + .SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), jKey)); + if (typedJsonProperty != null) + { + serializer = serializer ?? JsonSerializer.CreateDefault(); + foreach (JsonConverterAttribute attribute in typedJsonProperty.PropertyInfo.GetCustomAttributes(inherit: true)) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance(attribute.ConverterType, attribute.ConverterParameters)!; + serializer.Converters.Add(converter); + } + + object? obj = jValue?.ToObject(typedJsonProperty.PropertyType, serializer); + typedJsonProperty.PropertyInfo.SetValue(element, obj); + } + + return element!; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedBooleanConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoBooleanConverter.cs similarity index 61% rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedBooleanConverter.cs rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoBooleanConverter.cs index 10600d94..569474f8 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedBooleanConverter.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoBooleanConverter.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace System.Text.Json.Converters { - internal class StringTypedBooleanConverter : JsonConverter + internal class YesOrNoBooleanConverter : JsonConverter { - private readonly JsonConverter _converter = new StringTypedNullableBooleanConverter(); + private readonly JsonConverter _converter = new YesOrNoNullableBooleanConverter(); public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedNullableBooleanConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoNullableBooleanConverter.cs similarity index 73% rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedNullableBooleanConverter.cs rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoNullableBooleanConverter.cs index b17c469b..1060d326 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/System.Text.Json/Boolean/StringTypedNullableBooleanConverter.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Boolean/YesOrNoNullableBooleanConverter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json.Converters { - internal class StringTypedNullableBooleanConverter : JsonConverter + internal class YesOrNoNullableBooleanConverter : JsonConverter { public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -18,15 +18,15 @@ namespace System.Text.Json.Converters { return false; } - else if (reader.TokenType == JsonTokenType.String) + else if (reader.TokenType == System.Text.Json.JsonTokenType.String) { string? value = reader.GetString(); if (value == null) return null; - if ("true".Equals(value, StringComparison.OrdinalIgnoreCase)) + if ("Y".Equals(value)) return true; - else if ("false".Equals(value, StringComparison.OrdinalIgnoreCase)) + else if ("N".Equals(value)) return false; } @@ -36,7 +36,7 @@ namespace System.Text.Json.Converters public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) { if (value.HasValue) - writer.WriteStringValue(value.Value ? "true" : "false"); + writer.WriteStringValue(value.Value ? "Y" : "N"); else writer.WriteNullValue(); } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs new file mode 100644 index 00000000..37ad3823 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextDateTimeOffsetConverter.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace System.Text.Json.Converters +{ + internal class PureDigitalTextDateTimeOffsetConverter : JsonConverter + { + private readonly JsonConverter _converter = new PureDigitalTextNullableDateTimeOffsetConverter(); + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return _converter.Read(ref reader, typeToConvert, options) ?? default; + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + _converter.Write(writer, value, options); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs new file mode 100644 index 00000000..1e1572e3 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/DateTimeOffset/PureDigitalTextNullableDateTimeOffsetConverter.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.Text.Json.Serialization; + +namespace System.Text.Json.Converters +{ + internal class PureDigitalTextNullableDateTimeOffsetConverter : JsonConverter + { + private const string DATETIME_FORMAT = Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter.DATETIME_FORMAT; + + public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + else if (reader.TokenType == JsonTokenType.String) + { + string? value = reader.GetString(); + if (value == null) + return null; + + if (DateTimeOffset.TryParseExact(value, DATETIME_FORMAT, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out DateTimeOffset result)) + return result; + + if (DateTimeOffset.TryParse(value, out result)) + return result; + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) + { + if (value.HasValue) + writer.WriteStringValue(value.Value.ToString(DATETIME_FORMAT, DateTimeFormatInfo.InvariantInfo)); + else + writer.WriteNullValue(); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs new file mode 100644 index 00000000..c0db7526 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs @@ -0,0 +1,206 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace System.Text.Json.Converters +{ + internal static class FlattenNArrayObjectConverterBase + { + public const string PROPERTY_WILDCARD_NARRAY_ELEMENT = Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; + public const string PROPERTY_NAME_NARRAY = Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; + } + + internal class FlattenNArrayObjectConverterBase : JsonConverter + where T : class, new() + { + private sealed class InnerTypedJsonProperty + { + public string PropertyName { get; } + + public PropertyInfo PropertyInfo { get; } + + public Type PropertyType { get { return PropertyInfo.PropertyType; } } + + public bool IsNArrayProperty { get; } + + public InnerTypedJsonProperty(string propertyName, PropertyInfo propertyInfo, bool isNArrayProperty) + { + PropertyName = propertyName; + PropertyInfo = propertyInfo; + IsNArrayProperty = isNArrayProperty; + } + } + + private const string PROPERTY_WILDCARD_NARRAY_ELEMENT = FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; + private const string PROPERTY_NAME_NARRAY = FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; + + private static readonly Hashtable _mappedTypeJsonProperties = new Hashtable(); + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + InnerTypedJsonProperty[] typedJsonProperties = GetTypedJsonProperties(typeToConvert); + if (typedJsonProperties.Count(p => p.IsNArrayProperty) != 1) + throw new JsonException("The number of `$n` properties must be only one."); + + JsonElement jElement = JsonDocument.ParseValue(ref reader).RootElement.Clone(); + T tObject = new T(); + + foreach (JsonProperty jKey in jElement.EnumerateObject()) + { + InnerTypedJsonProperty? typedJsonProperty = typedJsonProperties.SingleOrDefault(e => e.PropertyName == jKey.Name); + if (typedJsonProperty != null) + { + // 处理普通属性 + object? value = options is null ? + JsonSerializer.Deserialize(jKey.Value, typedJsonProperty.PropertyType, options) : + JsonSerializer.Deserialize(jKey.Value, typedJsonProperty.PropertyType, options); + typedJsonProperty.PropertyInfo.SetValue(tObject, value); + } + else if (TryMatchNArrayIndex(jKey.Name, out int index)) + { + // 处理 $n 属性 + InnerTypedJsonProperty narrayJsonProperty = typedJsonProperties.Single(e => e.IsNArrayProperty); + object? value = narrayJsonProperty.PropertyInfo.GetValue(tObject); + + Array array = CreateOrExpandNArray(value, narrayJsonProperty.PropertyType.GetElementType()!, index + 1); + object? element = CreateOrUpdateNArrayElement(array, index, jKey.Name, jKey.Value, options); + narrayJsonProperty.PropertyInfo.SetValue(tObject, array); + } + } + return tObject; + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + throw new NotImplementedException(); + } + + private static InnerTypedJsonProperty[] GetTypedJsonProperties(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string mappedTypeKey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); + InnerTypedJsonProperty[]? typedJsonProperties = (InnerTypedJsonProperty[]?)_mappedTypeJsonProperties[mappedTypeKey]; + + if (typedJsonProperties == null) + { + typedJsonProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => + (p.CanRead && !p.GetCustomAttributes(inherit: true).Any()) && + (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) + ) + .Select(p => + { + string name = p.GetCustomAttribute(inherit: true)?.Name ?? p.Name; + return new InnerTypedJsonProperty + ( + propertyName: name, + propertyInfo: p, + isNArrayProperty: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass + ); + }) + .ToArray(); + _mappedTypeJsonProperties[mappedTypeKey] = typedJsonProperties; + } + + return typedJsonProperties; + } + + private static bool TryMatchNArrayIndex(string key, out int index) + { + Regex regex = new Regex(@"(_)(\d+)", RegexOptions.Compiled); + if (regex.IsMatch(key)) + { + string str = regex.Match(key).Groups[2].Value; + index = int.Parse(str); + return true; + } + + index = -1; + return false; + } + + private static Array CreateOrExpandNArray(object? array, Type elementType, int capacity) + { + if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + + if (array == null) + { + return Array.CreateInstance(elementType, capacity); + } + + Array src = (Array)array; + if (src.Length < capacity) + { + Array dst = Array.CreateInstance(elementType, capacity); + Array.Copy(src, dst, src.Length); + return dst; + } + + return src; + } + + private static object CreateOrUpdateNArrayElement(Array array, int index, string jKey, JsonElement jValue, JsonSerializerOptions? serializerOptions = null) + { + if (array == null) throw new ArgumentNullException(nameof(array)); + if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); + + object? element = array.GetValue(index); + Type elementType = array.GetType().GetElementType()!; + + if (element == null) + { + + if (elementType.IsAbstract || elementType.IsInterface) + { + throw new NotSupportedException(); + } + else if (elementType.IsArray) + { + element = Array.CreateInstance(elementType, 0); + } + else + { + element = Activator.CreateInstance(elementType); + } + + array.SetValue(element, index); + } + + InnerTypedJsonProperty? typedJsonProperty = GetTypedJsonProperties(elementType) + .SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), jKey)); + if (typedJsonProperty != null) + { + serializerOptions = (serializerOptions == null) ? new JsonSerializerOptions() : new JsonSerializerOptions(serializerOptions); + foreach (JsonConverterAttribute attribute in typedJsonProperty.PropertyInfo.GetCustomAttributes(inherit: true)) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance(attribute.ConverterType!); + serializerOptions.Converters.Add(converter!); + } + + object? obj = JsonSerializer.Deserialize(jValue, typedJsonProperty.PropertyType, serializerOptions)!; + typedJsonProperty.PropertyInfo.SetValue(element, obj); + } + + return element!; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteMerchantCustomsExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteMerchantCustomsExtensions.cs new file mode 100644 index 00000000..dfecbda5 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteMerchantCustomsExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + public static class WechatTenpayClientExecuteMerchantCustomsExtensions + { + /// + /// 异步调用 [POST] /cgi-bin/mch/customs/customdeclareorder 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/external/declarecustom.php?chapter=18_1 + /// + /// + /// + /// + /// + public static async Task ExecuteCreateMerchantCustomsCustomDeclarationAsync(this WechatTenpayClient client, Models.CreateMerchantCustomsCustomDeclarationRequest 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, "cgi-bin", "mch", "customs", "customdeclareorder"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + + /// + /// 异步调用 [POST] /cgi-bin/mch/customs/customdeclarequery 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/external/declarecustom.php?chapter=18_2 + /// + /// + /// + /// + /// + public static async Task ExecuteQueryMerchantCustomsCustomDeclarationAsync(this WechatTenpayClient client, Models.QueryMerchantCustomsCustomDeclarationRequest 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, "cgi-bin", "mch", "customs", "customdeclarequery"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + + /// + /// 异步调用 [POST] /cgi-bin/mch/customs/customdeclareredeclare 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/external/declarecustom.php?chapter=18_4&index=3 + /// + /// + /// + /// + /// + public static async Task ExecuteRedeclareMerchantCustomsCustomDeclarationAsync(this WechatTenpayClient client, Models.RedeclareMerchantCustomsCustomDeclarationRequest 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, "cgi-bin", "mch", "customs", "customdeclareredeclare"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayExtensions.cs new file mode 100644 index 00000000..77385b95 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + public static class WechatTenpayClientExecutePayExtensions + { + /// + /// 异步调用 [POST] /pay/micropay 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/micropay_sl.php?chapter=9_10&index=1 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/danpin.php?chapter=9_101&index=1 + /// + /// + /// + /// + /// + public static async Task ExecuteCreatePayMicroPayAsync(this WechatTenpayClient client, Models.CreatePayMicroPayRequest 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, "pay", "micropay"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayITILExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayITILExtensions.cs new file mode 100644 index 00000000..693f6696 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecutePayITILExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + public static class WechatTenpayClientExecutePayITILExtensions + { + /// + /// 异步调用 [POST] /payitil/report 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_14&index=8 + /// + /// + /// + /// + /// + public static async Task ExecuteSubmitPayITILReportAsync(this WechatTenpayClient client, Models.SubmitPayITILReportRequest 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, "payitil", "report"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteToolsExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteToolsExtensions.cs new file mode 100644 index 00000000..c89f4877 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Extensions/WechatTenpayClientExecuteToolsExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + public static class WechatTenpayClientExecuteToolsExtensions + { + /// + /// 异步调用 [POST] /tools/authcodetoopenid 接口。 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_13&index=9 + /// REF: https://pay.weixin.qq.com/wiki/doc/api/micropay_sl.php?chapter=9_12&index=8 + /// + /// + /// + /// + /// + public static async Task ExecuteToolsAuthCodeToOpenIdAsync(this WechatTenpayClient client, Models.ToolsAuthCodeToOpenIdRequest 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, "tools", "authcodetoopenid"); + + return await client.SendRequestWithXmlAsync(flurlReq, data: request, cancellationToken: cancellationToken); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.cs new file mode 100644 index 00000000..b94a6bb3 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.cs @@ -0,0 +1,106 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclareorder 接口的请求。 + /// + public class CreateMerchantCustomsCustomDeclarationRequest : WechatTenpaySignableRequest + { + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string? OutTradeNumber { get; set; } + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置海关。 + /// + [Newtonsoft.Json.JsonProperty("customs")] + [System.Text.Json.Serialization.JsonPropertyName("customs")] + public string Customs { get; set; } = string.Empty; + + /// + /// 获取或设置商户海关备案号。 + /// + [Newtonsoft.Json.JsonProperty("mch_customs_no")] + [System.Text.Json.Serialization.JsonPropertyName("mch_customs_no")] + public string MerchantCustomsNumber { get; set; } = string.Empty; + + /// + /// 获取或设置关税(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("duty")] + [System.Text.Json.Serialization.JsonPropertyName("duty")] + public int? Duty { get; set; } + + /// + /// 获取或设置报关类型。 + /// + [Newtonsoft.Json.JsonProperty("action_type")] + [System.Text.Json.Serialization.JsonPropertyName("action_type")] + public string? ActionType { get; set; } + + /// + /// 获取或设置币种。 + /// + [Newtonsoft.Json.JsonProperty("fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type")] + public string? FeeType { get; set; } + + /// + /// 获取或设置应付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("order_fee")] + [System.Text.Json.Serialization.JsonPropertyName("order_fee")] + public int? OrderFee { get; set; } + + /// + /// 获取或设置物流费(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("transport_fee")] + [System.Text.Json.Serialization.JsonPropertyName("transport_fee")] + public int? TransportFee { get; set; } + + /// + /// 获取或设置商品价格(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("product_fee")] + [System.Text.Json.Serialization.JsonPropertyName("product_fee")] + public int? ProductFee { get; set; } + + /// + /// 获取或设置证件类型。 + /// + [Newtonsoft.Json.JsonProperty("cert_type")] + [System.Text.Json.Serialization.JsonPropertyName("cert_type")] + public string? CertificateType { get; set; } + + /// + /// 获取或设置证件号码。 + /// + [Newtonsoft.Json.JsonProperty("cert_id")] + [System.Text.Json.Serialization.JsonPropertyName("cert_id")] + public string? CertificateId { get; set; } + + /// + /// 获取或设置证件姓名。 + /// + [Newtonsoft.Json.JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? CertificateName { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.cs new file mode 100644 index 00000000..fc991b94 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.cs @@ -0,0 +1,75 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclareorder 接口的响应。 + /// + public class CreateMerchantCustomsCustomDeclarationResponse : WechatTenpaySignableResponse + { + /// + /// 获取或设置状态码。 + /// + [Newtonsoft.Json.JsonProperty("state")] + [System.Text.Json.Serialization.JsonPropertyName("state")] + public string State { get; set; } = default!; + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = default!; + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置微信子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_id")] + public string? SubOrderId { get; set; } + + /// + /// 获取或设置最后更新时间。 + /// + [Newtonsoft.Json.JsonProperty("modify_time")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("modify_time")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + public DateTimeOffset ModifyTime { get; set; } + + /// + /// 获取或设置订购人和支付人身份信息校验结果。 + /// + [Newtonsoft.Json.JsonProperty("cert_check_result")] + [System.Text.Json.Serialization.JsonPropertyName("cert_check_result")] + public string? CertificateCheckResult { get; set; } + + /// + /// 获取或设置验核机构。 + /// + [Newtonsoft.Json.JsonProperty("verify_department")] + [System.Text.Json.Serialization.JsonPropertyName("verify_department")] + public string? VerifyDepartment { get; set; } + + /// + /// 获取或设置验核机构交易流水号。 + /// + [Newtonsoft.Json.JsonProperty("verify_department_trade_id")] + [System.Text.Json.Serialization.JsonPropertyName("verify_department_trade_id")] + public string? VerifyDepartmentTradeId { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.cs new file mode 100644 index 00000000..46d0cbeb --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.cs @@ -0,0 +1,43 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclarequery 接口的请求。 + /// + public class QueryMerchantCustomsCustomDeclarationRequest : WechatTenpaySignableRequest + { + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string? OutTradeNumber { get; set; } + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置微信子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_id")] + public string? SubOrderId { get; set; } + + /// + /// 获取或设置海关。 + /// + [Newtonsoft.Json.JsonProperty("customs")] + [System.Text.Json.Serialization.JsonPropertyName("customs")] + public string Customs { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs new file mode 100644 index 00000000..2804ad0b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs @@ -0,0 +1,157 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclarequery 接口的响应。 + /// + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponseClassNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponseClassSystemTextJsonConverter))] + public class QueryMerchantCustomsCustomDeclarationResponse : WechatTenpaySignableResponse + { + public static class Types + { + public class Record + { + /// + /// 获取或设置状态码。 + /// + [Newtonsoft.Json.JsonProperty("state_$n")] + [System.Text.Json.Serialization.JsonPropertyName("state_$n")] + public string State { get; set; } = default!; + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no_$n")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no_$n")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置微信子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_id_$n")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_id_$n")] + public string? SubOrderId { get; set; } + + /// + /// 获取或设置海关。 + /// + [Newtonsoft.Json.JsonProperty("customs_$n")] + [System.Text.Json.Serialization.JsonPropertyName("customs_$n")] + public string Customs { get; set; } = default!; + + /// + /// 获取或设置商户海关备案号。 + /// + [Newtonsoft.Json.JsonProperty("mch_customs_no_$n")] + [System.Text.Json.Serialization.JsonPropertyName("mch_customs_no_$n")] + public string MerchantCustomsNumber { get; set; } = default!; + + /// + /// 获取或设置关税(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("duty_$n")] + [System.Text.Json.Serialization.JsonPropertyName("duty_$n")] + public int? Duty { get; set; } + + /// + /// 获取或设置币种。 + /// + [Newtonsoft.Json.JsonProperty("fee_type_$n")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type_$n")] + public string? FeeType { get; set; } + + /// + /// 获取或设置应付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("order_fee_$n")] + [System.Text.Json.Serialization.JsonPropertyName("order_fee_$n")] + public int? OrderFee { get; set; } + + /// + /// 获取或设置物流费(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("transport_fee_$n")] + [System.Text.Json.Serialization.JsonPropertyName("transport_fee_$n")] + public int? TransportFee { get; set; } + + /// + /// 获取或设置商品价格(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("product_fee_$n")] + [System.Text.Json.Serialization.JsonPropertyName("product_fee_$n")] + public int? ProductFee { get; set; } + + /// + /// 获取或设置最后更新时间。 + /// + [Newtonsoft.Json.JsonProperty("modify_time_$n")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("modify_time_$n")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + public DateTimeOffset ModifyTime { get; set; } + + /// + /// 获取或设置订购人和支付人身份信息校验结果。 + /// + [Newtonsoft.Json.JsonProperty("cert_check_result_$n")] + [System.Text.Json.Serialization.JsonPropertyName("cert_check_result_$n")] + public string? CertificateCheckResult { get; set; } + + /// + /// 获取或设置申报结果说明。 + /// + [Newtonsoft.Json.JsonProperty("explanation_$n")] + [System.Text.Json.Serialization.JsonPropertyName("explanation_$n")] + public string? Explanation { get; set; } + } + } + + internal static class Converters + { + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + { + } + + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + { + } + } + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置记录列表。 + /// + [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + public Types.Record[] RecordList { get; set; } = default!; + + /// + /// 获取或设置记录总数。 + /// + [Newtonsoft.Json.JsonProperty("count")] + [System.Text.Json.Serialization.JsonPropertyName("count")] + public int RecordCount { get; set; } + + /// + /// 获取或设置验核机构。 + /// + [Newtonsoft.Json.JsonProperty("verify_department")] + [System.Text.Json.Serialization.JsonPropertyName("verify_department")] + public string? VerifyDepartment { get; set; } + + /// + /// 获取或设置验核机构交易流水号。 + /// + [Newtonsoft.Json.JsonProperty("verify_department_trade_id")] + [System.Text.Json.Serialization.JsonPropertyName("verify_department_trade_id")] + public string? VerifyDepartmentTradeId { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.cs new file mode 100644 index 00000000..9101f11f --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.cs @@ -0,0 +1,50 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclareredeclare 接口的请求。 + /// + public class RedeclareMerchantCustomsCustomDeclarationRequest : WechatTenpaySignableRequest + { + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string? OutTradeNumber { get; set; } + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置微信子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_id")] + public string? SubOrderId { get; set; } + + /// + /// 获取或设置海关。 + /// + [Newtonsoft.Json.JsonProperty("customs")] + [System.Text.Json.Serialization.JsonPropertyName("customs")] + public string Customs { get; set; } = string.Empty; + + /// + /// 获取或设置商户海关备案号。 + /// + [Newtonsoft.Json.JsonProperty("mch_customs_no")] + [System.Text.Json.Serialization.JsonPropertyName("mch_customs_no")] + public string MerchantCustomsNumber { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.cs new file mode 100644 index 00000000..66b6a5bc --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.cs @@ -0,0 +1,61 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /cgi-bin/mch/customs/customdeclareredeclare 接口的响应。 + /// + public class RedeclareMerchantCustomsCustomDeclarationResponse : WechatTenpaySignableResponse + { + /// + /// 获取或设置状态码。 + /// + [Newtonsoft.Json.JsonProperty("state")] + [System.Text.Json.Serialization.JsonPropertyName("state")] + public string State { get; set; } = default!; + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = default!; + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置商户子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_no")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_no")] + public string? SubOrderNumber { get; set; } + + /// + /// 获取或设置微信子订单号。 + /// + [Newtonsoft.Json.JsonProperty("sub_order_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_order_id")] + public string? SubOrderId { get; set; } + + /// + /// 获取或设置最后更新时间。 + /// + [Newtonsoft.Json.JsonProperty("modify_time")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("modify_time")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextDateTimeOffsetConverter))] + public DateTimeOffset ModifyTime { get; set; } + + /// + /// 获取或设置申报结果说明。 + /// + [Newtonsoft.Json.JsonProperty("explanation")] + [System.Text.Json.Serialization.JsonPropertyName("explanation")] + public string? Explanation { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayRequest.cs new file mode 100644 index 00000000..8e37736f --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayRequest.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /pay/micropay 接口的请求。 + /// + public class CreatePayMicroPayRequest : WechatTenpaySignableRequest + { + public static class Types + { + public class Detail + { + public static class Types + { + public class GoodsDetail + { + /// + /// 获取或设置商户侧商品编码。 + /// + [Newtonsoft.Json.JsonProperty("goods_id")] + [System.Text.Json.Serialization.JsonPropertyName("goods_id")] + public string MerchantGoodsId { get; set; } = string.Empty; + + /// + /// 获取或设置微信侧商品编码。 + /// + [Newtonsoft.Json.JsonProperty("wxpay_goods_id")] + [System.Text.Json.Serialization.JsonPropertyName("wxpay_goods_id")] + public string? WechatpayGoodsId { get; set; } + + /// + /// 获取或设置商品分类。 + /// + [Newtonsoft.Json.JsonProperty("goods_category")] + [System.Text.Json.Serialization.JsonPropertyName("goods_category")] + public string? GoodsCategory { get; set; } + + /// + /// 获取或设置商品名称。 + /// + [Newtonsoft.Json.JsonProperty("goods_name")] + [System.Text.Json.Serialization.JsonPropertyName("goods_name")] + public string? GoodsName { get; set; } + + /// + /// 获取或设置商品数量。 + /// + [Newtonsoft.Json.JsonProperty("quantity")] + [System.Text.Json.Serialization.JsonPropertyName("quantity")] + public int Quantity { get; set; } + + /// + /// 获取或设置商品单价(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("price")] + [System.Text.Json.Serialization.JsonPropertyName("price")] + public int Price { get; set; } + + /// + /// 获取或设置商品描述。 + /// + [Newtonsoft.Json.JsonProperty("body")] + [System.Text.Json.Serialization.JsonPropertyName("body")] + public string? Body { get; set; } + } + } + + /// + /// 获取或设置订单原价(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("cost_price")] + [System.Text.Json.Serialization.JsonPropertyName("cost_price")] + public int? CostPrice { get; set; } + + /// + /// 获取或设置商品小票 ID。 + /// + [Newtonsoft.Json.JsonProperty("receipt_id")] + [System.Text.Json.Serialization.JsonPropertyName("receipt_id")] + public string? ReceiptId { get; set; } + + /// + /// 获取或设置单品列表。 + /// + [Newtonsoft.Json.JsonProperty("goods_detail")] + [System.Text.Json.Serialization.JsonPropertyName("goods_detail")] + public List? GoodsList { get; set; } + } + + public class Scene + { + /// + /// 获取或设置门店编号。 + /// + [Newtonsoft.Json.JsonProperty("id")] + [System.Text.Json.Serialization.JsonPropertyName("id")] + public string StoreId { get; set; } = string.Empty; + + /// + /// 获取或设置门店名称。 + /// + [Newtonsoft.Json.JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? StoreName { get; set; } + + /// + /// 获取或设置地区编码。 + /// + [Newtonsoft.Json.JsonProperty("area_code")] + [System.Text.Json.Serialization.JsonPropertyName("area_code")] + public string? StoreAreaCode { get; set; } + + /// + /// 获取或设置详细地址。 + /// + [Newtonsoft.Json.JsonProperty("address")] + [System.Text.Json.Serialization.JsonPropertyName("address")] + public string? StoreAddress { get; set; } + } + } + + internal static class Converters + { + internal class ResponsePropertyDetailNewtonsoftJsonConverter : Newtonsoft.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + + internal class ResponsePropertyDetailSystemTextJsonConverter : System.Text.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + + internal class ResponsePropertySceneNewtonsoftJsonConverter : Newtonsoft.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + + internal class ResponsePropertySceneSystemTextJsonConverter : System.Text.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + } + + /// + /// 获取或设置接口版本号。 + /// + [Newtonsoft.Json.JsonProperty("version")] + [System.Text.Json.Serialization.JsonPropertyName("version")] + public string? Version { get; set; } + + /// + /// 获取或设置子商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mch_id")] + public string? SubMerchantId { get; set; } + + /// + /// 获取或设置子商户 AppId。 + /// + [Newtonsoft.Json.JsonProperty("sub_appid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] + public string? SubAppId { get; set; } + + /// + /// 获取或设置商品描述。 + /// + [Newtonsoft.Json.JsonProperty("body")] + [System.Text.Json.Serialization.JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = string.Empty; + + /// + /// 获取或设置订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("total_fee")] + public int TotalFee { get; set; } + + /// + /// 获取或设置货币类型。 + /// + [Newtonsoft.Json.JsonProperty("fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type")] + public string? FeeType { get; set; } + + /// + /// 获取或设置付款码。 + /// + [Newtonsoft.Json.JsonProperty("auth_code")] + [System.Text.Json.Serialization.JsonPropertyName("auth_code")] + public string AuthCode { get; set; } = string.Empty; + + /// + /// 获取或设置附加数据。 + /// + [Newtonsoft.Json.JsonProperty("attach")] + [System.Text.Json.Serialization.JsonPropertyName("attach")] + public string? Attachment { get; set; } + + /// + /// 获取或设置终端设备号。 + /// + [Newtonsoft.Json.JsonProperty("device_info")] + [System.Text.Json.Serialization.JsonPropertyName("device_info")] + public string? DeviceInfo { get; set; } + + /// + /// 获取或设置用户终端 IP。 + /// + [Newtonsoft.Json.JsonProperty("spbill_create_ip")] + [System.Text.Json.Serialization.JsonPropertyName("spbill_create_ip")] + public string ClientIp { get; set; } = string.Empty; + + /// + /// 获取或设置订单优惠标记。 + /// + [Newtonsoft.Json.JsonProperty("goods_tag")] + [System.Text.Json.Serialization.JsonPropertyName("goods_tag")] + public string? GoodsTag { get; set; } + + /// + /// 获取或设置指定付款方式编码。 + /// + [Newtonsoft.Json.JsonProperty("limit_pay")] + [System.Text.Json.Serialization.JsonPropertyName("limit_pay")] + public string? LimitPayCode { get; set; } + + /// + /// 获取或设置交易起始时间。 + /// + [Newtonsoft.Json.JsonProperty("time_start")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("time_start")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? StartTime { get; set; } + + /// + /// 获取或设置交易结束时间。 + /// + [Newtonsoft.Json.JsonProperty("time_expire")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("time_expire")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? ExpireTime { get; set; } + + /// + /// 获取或设置商品信息。 + /// + [Newtonsoft.Json.JsonProperty("detail")] + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponsePropertyDetailNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonPropertyName("detail")] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponsePropertyDetailSystemTextJsonConverter))] + public Types.Detail? Detail { get; set; } + + /// + /// 获取或设置场景信息。 + /// + [Newtonsoft.Json.JsonProperty("scene_info")] + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponsePropertySceneNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonPropertyName("scene_info")] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponsePropertySceneSystemTextJsonConverter))] + public Types.Scene? Scene { get; set; } + + /// + /// 获取或设置是否分账。 + /// + [Newtonsoft.Json.JsonProperty("profit_sharing")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.YesOrNoNullableBooleanConverter))] + [System.Text.Json.Serialization.JsonPropertyName("profit_sharing")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.YesOrNoNullableBooleanConverter))] + public bool? IsProfitSharing { get; set; } + + /// + /// 获取或设置是否开放电子发票入口。 + /// + [Newtonsoft.Json.JsonProperty("receipt")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.YesOrNoNullableBooleanConverter))] + [System.Text.Json.Serialization.JsonPropertyName("receipt")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.YesOrNoNullableBooleanConverter))] + public bool? IsReceiptOpen { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayResponse.cs new file mode 100644 index 00000000..09ffd55c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayMicroPayResponse.cs @@ -0,0 +1,270 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /pay/micropay 接口的响应。 + /// + public class CreatePayMicroPayResponse : WechatTenpaySignableResponse + { + public static class Types + { + public class Promotion + { + public static class Types + { + public class GoodsDetail + { + /// + /// 获取或设置商品编码。 + /// + [Newtonsoft.Json.JsonProperty("goods_id")] + [System.Text.Json.Serialization.JsonPropertyName("goods_id")] + public string GoodsId { get; set; } = default!; + + /// + /// 获取或设置商品数量。 + /// + [Newtonsoft.Json.JsonProperty("quantity")] + [System.Text.Json.Serialization.JsonPropertyName("quantity")] + public int Quantity { get; set; } + + /// + /// 获取或设置商品单价(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("price")] + [System.Text.Json.Serialization.JsonPropertyName("price")] + public int Price { get; set; } + + /// + /// 获取或设置商品优惠金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("discount_amount")] + [System.Text.Json.Serialization.JsonPropertyName("discount_amount")] + public int DiscountAmount { get; set; } + + /// + /// 获取或设置商品备注。 + /// + [Newtonsoft.Json.JsonProperty("goods_remark")] + [System.Text.Json.Serialization.JsonPropertyName("goods_remark")] + public string? GoodsRemark { get; set; } + } + } + + /// + /// 获取或设置券或者立减优惠 ID。 + /// + [Newtonsoft.Json.JsonProperty("promotion_id")] + [System.Text.Json.Serialization.JsonPropertyName("promotion_id")] + public string PromotionId { get; set; } = default!; + + /// + /// 获取或设置优惠名称。 + /// + [Newtonsoft.Json.JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// 获取或设置优惠范围。 + /// + [Newtonsoft.Json.JsonProperty("scope")] + [System.Text.Json.Serialization.JsonPropertyName("scope")] + public string? Scope { get; set; } + + /// + /// 获取或设置优惠类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// 获取或设置优惠券面额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("amount")] + [System.Text.Json.Serialization.JsonPropertyName("amount")] + public int Amount { get; set; } + + /// + /// 获取或设置活动 ID。 + /// + [Newtonsoft.Json.JsonProperty("activity_id")] + [System.Text.Json.Serialization.JsonPropertyName("activity_id")] + public string? ActivityId { get; set; } + + /// + /// 获取或设置微信出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("wxpay_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("wxpay_contribute")] + public int? WechatpayContribute { get; set; } + + /// + /// 获取或设置商户出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("merchant_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("merchant_contribute")] + public int? MerchantContribute { get; set; } + + /// + /// 获取或设置其他出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("other_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("other_contribute")] + public int? OtherContribute { get; set; } + + /// + /// 获取或设置单品列表。 + /// + [Newtonsoft.Json.JsonProperty("goods_detail")] + [System.Text.Json.Serialization.JsonPropertyName("goods_detail")] + public Types.GoodsDetail[]? GoodsList { get; set; } + } + } + + internal static class Converters + { + internal class ResponsePropertyPromotionListNewtonsoftJsonConverter : Newtonsoft.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + + internal class ResponsePropertyPromotionListSystemTextJsonConverter : System.Text.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + } + + /// + /// 获取或设置子商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mch_id")] + public string? SubMerchantId { get; set; } + + /// + /// 获取或设置子商户 AppId。 + /// + [Newtonsoft.Json.JsonProperty("sub_appid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] + public string? SubAppId { get; set; } + + /// + /// 获取或设置终端设备号。 + /// + [Newtonsoft.Json.JsonProperty("device_info")] + [System.Text.Json.Serialization.JsonPropertyName("device_info")] + public string? DeviceInfo { get; set; } + + /// + /// 获取或设置用户唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("openid")] + [System.Text.Json.Serialization.JsonPropertyName("openid")] + public string OpenId { get; set; } = default!; + + /// + /// 获取或设置用户是否订阅该公众号标识。 + /// + [Newtonsoft.Json.JsonProperty("is_subscribe")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.YesOrNoBooleanConverter))] + [System.Text.Json.Serialization.JsonPropertyName("is_subscribe")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.YesOrNoBooleanConverter))] + public bool IsSubscribed { get; set; } + + /// + /// 获取或设置交易类型。 + /// + [Newtonsoft.Json.JsonProperty("trade_type")] + [System.Text.Json.Serialization.JsonPropertyName("trade_type")] + public string TradeType { get; set; } = default!; + + /// + /// 获取或设置付款银行。 + /// + [Newtonsoft.Json.JsonProperty("bank_type")] + [System.Text.Json.Serialization.JsonPropertyName("bank_type")] + public string BankType { get; set; } = default!; + + /// + /// 获取或设置订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("total_fee")] + public int TotalFee { get; set; } + + /// + /// 获取或设置货币类型。 + /// + [Newtonsoft.Json.JsonProperty("fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type")] + public string? FeeType { get; set; } + + /// + /// 获取或设置应结订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("settlement_total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("settlement_total_fee")] + public int? SettlementFee { get; set; } + + /// + /// 获取或设置代金券金额。 + /// + [Newtonsoft.Json.JsonProperty("coupon_fee")] + [System.Text.Json.Serialization.JsonPropertyName("coupon_fee")] + public int? FouponFee { get; set; } + + /// + /// 获取或设置现金支付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee")] + public int CashFee { get; set; } + + /// + /// 获取或设置现金支付货币类型。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee_type")] + public string? CashFeeType { get; set; } + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = default!; + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置附加数据。 + /// + [Newtonsoft.Json.JsonProperty("attach")] + [System.Text.Json.Serialization.JsonPropertyName("attach")] + public string? Attachment { get; set; } + + /// + /// 获取或设置支付完成时间。 + /// + [Newtonsoft.Json.JsonProperty("time_end")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("time_end")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? EndTime { get; set; } + + /// + /// 获取或设置优惠信息。 + /// + [Newtonsoft.Json.JsonProperty("promotion_detail")] + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponsePropertyPromotionListNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonPropertyName("promotion_detail")] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponsePropertyPromotionListSystemTextJsonConverter))] + public Types.Promotion[]? PromotionList { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportRequest.cs new file mode 100644 index 00000000..0c7f7338 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportRequest.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /payitil/report 接口的请求。 + /// + public class SubmitPayITILReportRequest : WechatTenpaySignableRequest + { + public static class Types + { + public class Trade + { + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = string.Empty; + + /// + /// 获取或设置交易开始时间。 + /// + [Newtonsoft.Json.JsonProperty("begin_time")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("begin_time")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? BeginTime { get; set; } + + /// + /// 获取或设置交易完成时间。 + /// + [Newtonsoft.Json.JsonProperty("end_time")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("end_time")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? EndTime { get; set; } + + /// + /// 获取或设置交易状态。 + /// + [Newtonsoft.Json.JsonProperty("state")] + [System.Text.Json.Serialization.JsonPropertyName("state")] + public string? State { get; set; } + + /// + /// 获取或设置错误描述信息。 + /// + [Newtonsoft.Json.JsonProperty("err_msg")] + [System.Text.Json.Serialization.JsonPropertyName("err_msg")] + public string? ErrorMessage { get; set; } + } + } + + internal static class Converters + { + internal class ResponsePropertyTradeListNewtonsoftJsonConverter : Newtonsoft.Json.Converters.TextualObjectInJsonFormatConverterBase> + { + } + + internal class ResponsePropertyTradeListSystemTextJsonConverter : System.Text.Json.Converters.TextualObjectInJsonFormatConverterBase> + { + } + } + + /// + /// 获取或设置终端设备号。 + /// + [Newtonsoft.Json.JsonProperty("device_info")] + [System.Text.Json.Serialization.JsonPropertyName("device_info")] + public string? DeviceInfo { get; set; } + + /// + /// 获取或设置访问接口 IP。 + /// + [Newtonsoft.Json.JsonProperty("user_ip")] + [System.Text.Json.Serialization.JsonPropertyName("user_ip")] + public string UserIp { get; set; } = string.Empty; + + /// + /// 获取或设置订单优惠标记。 + /// + [Newtonsoft.Json.JsonProperty("interface_url")] + [System.Text.Json.Serialization.JsonPropertyName("interface_url")] + public string InterfaceUrl { get; set; } = string.Empty; + + /// + /// 获取或设置上报交易数据列表。 + /// + [Newtonsoft.Json.JsonProperty("trades")] + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponsePropertyTradeListNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonPropertyName("trades")] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponsePropertyTradeListSystemTextJsonConverter))] + public IList TradeList { get; set; } = new List(); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportResponse.cs new file mode 100644 index 00000000..728e1a87 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/PayITIL/SubmitPayITILReportResponse.cs @@ -0,0 +1,270 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /payitil/report 接口的响应。 + /// + public class SubmitPayITILReportResponse : WechatTenpaySignableResponse + { + public static class Types + { + public class Promotion + { + public static class Types + { + public class GoodsDetail + { + /// + /// 获取或设置商品编码。 + /// + [Newtonsoft.Json.JsonProperty("goods_id")] + [System.Text.Json.Serialization.JsonPropertyName("goods_id")] + public string GoodsId { get; set; } = default!; + + /// + /// 获取或设置商品数量。 + /// + [Newtonsoft.Json.JsonProperty("quantity")] + [System.Text.Json.Serialization.JsonPropertyName("quantity")] + public int Quantity { get; set; } + + /// + /// 获取或设置商品单价(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("price")] + [System.Text.Json.Serialization.JsonPropertyName("price")] + public int Price { get; set; } + + /// + /// 获取或设置商品优惠金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("discount_amount")] + [System.Text.Json.Serialization.JsonPropertyName("discount_amount")] + public int DiscountAmount { get; set; } + + /// + /// 获取或设置商品备注。 + /// + [Newtonsoft.Json.JsonProperty("goods_remark")] + [System.Text.Json.Serialization.JsonPropertyName("goods_remark")] + public string? GoodsRemark { get; set; } + } + } + + /// + /// 获取或设置券或者立减优惠 ID。 + /// + [Newtonsoft.Json.JsonProperty("promotion_id")] + [System.Text.Json.Serialization.JsonPropertyName("promotion_id")] + public string PromotionId { get; set; } = default!; + + /// + /// 获取或设置优惠名称。 + /// + [Newtonsoft.Json.JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// 获取或设置优惠范围。 + /// + [Newtonsoft.Json.JsonProperty("scope")] + [System.Text.Json.Serialization.JsonPropertyName("scope")] + public string? Scope { get; set; } + + /// + /// 获取或设置优惠类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// 获取或设置优惠券面额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("amount")] + [System.Text.Json.Serialization.JsonPropertyName("amount")] + public int Amount { get; set; } + + /// + /// 获取或设置活动 ID。 + /// + [Newtonsoft.Json.JsonProperty("activity_id")] + [System.Text.Json.Serialization.JsonPropertyName("activity_id")] + public string? ActivityId { get; set; } + + /// + /// 获取或设置微信出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("wxpay_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("wxpay_contribute")] + public int? WechatpayContribute { get; set; } + + /// + /// 获取或设置商户出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("merchant_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("merchant_contribute")] + public int? MerchantContribute { get; set; } + + /// + /// 获取或设置其他出资(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("other_contribute")] + [System.Text.Json.Serialization.JsonPropertyName("other_contribute")] + public int? OtherContribute { get; set; } + + /// + /// 获取或设置单品列表。 + /// + [Newtonsoft.Json.JsonProperty("goods_detail")] + [System.Text.Json.Serialization.JsonPropertyName("goods_detail")] + public Types.GoodsDetail[]? GoodsList { get; set; } + } + } + + internal static class Converters + { + internal class ResponsePropertyPromotionListNewtonsoftJsonConverter : Newtonsoft.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + + internal class ResponsePropertyPromotionListSystemTextJsonConverter : System.Text.Json.Converters.TextualObjectInJsonFormatConverterBase + { + } + } + + /// + /// 获取或设置子商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mch_id")] + public string? SubMerchantId { get; set; } + + /// + /// 获取或设置子商户 AppId。 + /// + [Newtonsoft.Json.JsonProperty("sub_appid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] + public string? SubAppId { get; set; } + + /// + /// 获取或设置终端设备号。 + /// + [Newtonsoft.Json.JsonProperty("device_info")] + [System.Text.Json.Serialization.JsonPropertyName("device_info")] + public string? DeviceInfo { get; set; } + + /// + /// 获取或设置用户唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("openid")] + [System.Text.Json.Serialization.JsonPropertyName("openid")] + public string OpenId { get; set; } = default!; + + /// + /// 获取或设置用户是否订阅该公众号标识。 + /// + [Newtonsoft.Json.JsonProperty("is_subscribe")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.YesOrNoBooleanConverter))] + [System.Text.Json.Serialization.JsonPropertyName("is_subscribe")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.YesOrNoBooleanConverter))] + public bool IsSubscribed { get; set; } + + /// + /// 获取或设置交易类型。 + /// + [Newtonsoft.Json.JsonProperty("trade_type")] + [System.Text.Json.Serialization.JsonPropertyName("trade_type")] + public string TradeType { get; set; } = default!; + + /// + /// 获取或设置付款银行。 + /// + [Newtonsoft.Json.JsonProperty("bank_type")] + [System.Text.Json.Serialization.JsonPropertyName("bank_type")] + public string BankType { get; set; } = default!; + + /// + /// 获取或设置订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("total_fee")] + public int TotalFee { get; set; } + + /// + /// 获取或设置货币类型。 + /// + [Newtonsoft.Json.JsonProperty("fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type")] + public string? FeeType { get; set; } + + /// + /// 获取或设置应结订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("settlement_total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("settlement_total_fee")] + public int? SettlementFee { get; set; } + + /// + /// 获取或设置代金券金额。 + /// + [Newtonsoft.Json.JsonProperty("coupon_fee")] + [System.Text.Json.Serialization.JsonPropertyName("coupon_fee")] + public int? FouponFee { get; set; } + + /// + /// 获取或设置现金支付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee")] + public int CashFee { get; set; } + + /// + /// 获取或设置现金支付货币类型。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee_type")] + public string? CashFeeType { get; set; } + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = default!; + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置附加数据。 + /// + [Newtonsoft.Json.JsonProperty("attach")] + [System.Text.Json.Serialization.JsonPropertyName("attach")] + public string? Attachment { get; set; } + + /// + /// 获取或设置支付完成时间。 + /// + [Newtonsoft.Json.JsonProperty("time_end")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("time_end")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.PureDigitalTextNullableDateTimeOffsetConverter))] + public DateTimeOffset? EndTime { get; set; } + + /// + /// 获取或设置优惠信息。 + /// + [Newtonsoft.Json.JsonProperty("promotion_detail")] + [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponsePropertyPromotionListNewtonsoftJsonConverter))] + [System.Text.Json.Serialization.JsonPropertyName("promotion_detail")] + [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponsePropertyPromotionListSystemTextJsonConverter))] + public Types.Promotion[]? PromotionList { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdRequest.cs new file mode 100644 index 00000000..381a4a5b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdRequest.cs @@ -0,0 +1,29 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /tools/authcodetoopenid 接口的请求。 + /// + public class ToolsAuthCodeToOpenIdRequest : WechatTenpaySignableRequest + { + /// + /// 获取或设置子商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mch_id")] + public string? SubMerchantId { get; set; } + + /// + /// 获取或设置子商户 AppId。 + /// + [Newtonsoft.Json.JsonProperty("sub_appid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] + public string? SubAppId { get; set; } + + /// + /// 获取或设置付款码。 + /// + [Newtonsoft.Json.JsonProperty("auth_code")] + [System.Text.Json.Serialization.JsonPropertyName("auth_code")] + public string AuthCode { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdResponse.cs new file mode 100644 index 00000000..824ae978 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Tools/ToolsAuthCodeToOpenIdResponse.cs @@ -0,0 +1,36 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models +{ + /// + /// 表示 [POST] /tools/authcodetoopenid 接口的响应。 + /// + public class ToolsAuthCodeToOpenIdResponse : WechatTenpaySignableResponse + { + /// + /// 获取或设置子商户号。 + /// + [Newtonsoft.Json.JsonProperty("sub_mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("sub_mch_id")] + public string? SubMerchantId { get; set; } + + /// + /// 获取或设置子商户 AppId。 + /// + [Newtonsoft.Json.JsonProperty("sub_appid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] + public string? SubAppId { get; set; } + + /// + /// 获取或设置用户唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("openid")] + [System.Text.Json.Serialization.JsonPropertyName("openid")] + public string OpenId { get; set; } = default!; + + /// + /// 获取或设置用户在子商户下的唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("sub_openid")] + [System.Text.Json.Serialization.JsonPropertyName("sub_openid")] + public string? SubOpenId { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Properties/AssemblyInfo.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..0b90f6d6 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests")] \ No newline at end of file diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/README.md b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/README.md new file mode 100644 index 00000000..42fba951 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/README.md @@ -0,0 +1,31 @@ +## SKIT.FlurlHttpClient.Wechat.TenpayV2 + +[![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.TenpayV2.svg?sanitize=true&label=Downloads)](https://www.nuget.org/packages/SKIT.FlurlHttpClient.Wechat.TenpayV2) +[![License](https://img.shields.io/github/license/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat?label=License)](https://mit-license.org/) + +基于 `Flurl.Http` 的微信商户平台 API v2 版客户端。 + +**注意**:本库仅仅包含微信支付未提供 v3 版 API 的部分功能,如需微信支付 v3 版 API 客户端,欢迎使用 [`SKIT.FlurlHttpClient.Wechat.TenpayV3`](https://www.nuget.org/packages/SKIT.FlurlHttpClient.Wechat.TenpayV3)。 + +--- + +### 【功能特性】 + +- 基于微信支付 v2 版 API 封装。 +- 支持直连商户、服务商两种模式。 +- 请求时自动生成签名,无需开发者手动干预。 +- 提供了微信支付所需的 MD5、HMAC-SHA-256 等算法工具类。 + +--- + +### 【开发文档】 + +[点此查看](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat)。 + +--- + +### 【更新日志】 + +[点此查看](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/blob/main/CHANGELOG.md)。 \ No newline at end of file diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj new file mode 100644 index 00000000..b190b90e --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj @@ -0,0 +1,47 @@ + + + + net47; netstandard2.0; net5.0; net6.0 + 8.0 + enable + true + + + + SKIT.FlurlHttpClient.Wechat.TenpayV2 + LOGO.png + README.md + MIT + https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat + Flurl.Http Wechat Weixin MicroMessage Tenpay WechatPay WeixinPay Wxpay 微信 微信支付 微信商户 + 1.0.0-beta + 基于 Flurl.Http 的微信支付 API v2 版客户端,支持直连商户、服务商模式,仅包含微信支付未提供 v3 版 API 的部分功能。如需微信支付 v3 版 API 客户端,欢迎使用 `SKIT.FlurlHttpClient.Wechat.TenpayV3`。 + Fu Diwei + git + https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git + + + + true + true + true + true + true + snupkg + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/Credentials.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/Credentials.cs new file mode 100644 index 00000000..dc2df5b7 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/Credentials.cs @@ -0,0 +1,43 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Settings +{ + public class Credentials + { + /// + /// 初始化客户端时 的副本。 + /// + public string MerchantId { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public string MerchantSecret { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public byte[]? MerchantCertificateBytes { get; set; } + + /// + /// 初始化客户端时 的副本。 + /// + public string? MerchantCertificatePassword { get; set; } + + /// + /// 初始化客户端时 的副本。 + /// + public string? AppId { get; } + + internal Credentials(WechatTenpayClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + MerchantId = options.MerchantId; + MerchantSecret = options.MerchantSecret; + MerchantCertificateBytes = options.MerchantCertificateBytes; + MerchantCertificatePassword = options.MerchantCertificatePassword; + AppId = options.AppId; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/HttpClientFactory.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/HttpClientFactory.cs new file mode 100644 index 00000000..e0f67885 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Settings/HttpClientFactory.cs @@ -0,0 +1,37 @@ +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Settings +{ + public class HttpClientFactory : Flurl.Http.Configuration.DefaultHttpClientFactory + { + private readonly byte[]? _certBytes; + private readonly string? _certPassword; + + public HttpClientFactory(byte[]? certBytes, string? certPassword) + { + _certBytes = certBytes; + _certPassword = certPassword; + } + + public override HttpMessageHandler CreateMessageHandler() + { +#if NETFRAMEWORK + WebRequestHandler handler = new WebRequestHandler(); + handler.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None; +#else + HttpClientHandler handler = new HttpClientHandler(); + handler.ServerCertificateCustomValidationCallback = (requestMessage, certificate, chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None; +#endif + + if (_certBytes != null) + { + X509Certificate x509 = (_certPassword == null) ? new X509Certificate2(_certBytes) : new X509Certificate2(_certBytes, _certPassword); + handler.ClientCertificates.Add(x509); + } + + return handler; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/HMACUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/HMACUtility.cs new file mode 100644 index 00000000..67925664 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/HMACUtility.cs @@ -0,0 +1,44 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities +{ + /// + /// HMAC 算法工具类。 + /// + public static class HMACUtility + { + /// + /// 获取 HMAC-SHA-256 消息认证码。 + /// + /// 密钥字节数组。 + /// 信息字节数组。 + /// 信息摘要。 + public static string HashWithSHA256(byte[] secretBytes, byte[] bytes) + { + if (secretBytes == null) throw new ArgumentNullException(nameof(secretBytes)); + if (bytes == null) throw new ArgumentNullException(nameof(bytes)); + + using HMAC hmac = new HMACSHA256(secretBytes); + byte[] hashBytes = hmac.ComputeHash(bytes); + return BitConverter.ToString(hashBytes).Replace("-", ""); + } + + /// + /// 获取 HMAC-SHA-256 消息认证码。 + /// + /// 密钥。 + /// 文本信息。 + /// 信息摘要。 + public static string HashWithSHA256(string secret, string message) + { + if (secret == null) throw new ArgumentNullException(nameof(secret)); + if (message == null) throw new ArgumentNullException(nameof(message)); + + byte[] secretBytes = Encoding.UTF8.GetBytes(secret); + byte[] bytes = Encoding.UTF8.GetBytes(message); + return HashWithSHA256(secretBytes, bytes); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/JsonUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/JsonUtility.cs new file mode 100644 index 00000000..12ae8f66 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/JsonUtility.cs @@ -0,0 +1,431 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities +{ + [Obsolete] + internal static partial class JsonUtility + { + public const string PROPERTY_WILDCARD_NARRAY_ELEMENT = "$n"; + public const string PROPERTY_NAME_NARRAY = "#n"; + + private static bool TryMatchNArrayIndex(string key, out int index) + { + Regex regex = new Regex(@"(_)(\d+)", RegexOptions.Compiled); + if (regex.IsMatch(key)) + { + string str = regex.Match(key).Groups[2].Value; + index = int.Parse(str); + return true; + } + + index = -1; + return false; + } + + private static Array CreateOrExpandNArray(object? array, Type elementType, int capacity) + { + if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + + if (array == null) + { + return Array.CreateInstance(elementType, capacity); + } + + Array src = (Array)array; + if (src.Length < capacity) + { + Array dst = Array.CreateInstance(elementType, capacity); + Array.Copy(src, dst, src.Length); + return dst; + } + + return src; + } + + private static object CreateOrUpdateNArrayElement(Array array, Type elementType, int index, string key, object value, params object?[]? args) + { + if (array == null) throw new ArgumentNullException(nameof(array)); + if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + + static object AppendNArrayElement(Array array, Type elementType, int index) + { + object? element = array.GetValue(index); + if (element == null) + { + if (elementType.IsAbstract || elementType.IsInterface) + { + throw new NotSupportedException(); + } + else if (elementType.IsArray) + { + element = Array.CreateInstance(elementType, 0); + } + else + { + element = Activator.CreateInstance(elementType); + } + } + + array.SetValue(element, index); + return element!; + } + + object? element = AppendNArrayElement(array, elementType, index); + + if (value is Newtonsoft.Json.Linq.JToken jToken) + { + var props = GetTypedNewtonsoftJsonProperties(elementType); + var prop = props.SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), key)); + if (prop != null) + { + Newtonsoft.Json.JsonSerializer? serializer = args?.FirstOrDefault() as Newtonsoft.Json.JsonSerializer; + if (serializer == null) + { + serializer = Newtonsoft.Json.JsonSerializer.CreateDefault(); + } + + foreach (Newtonsoft.Json.JsonConverterAttribute attribute in prop.PropertyInfo.GetCustomAttributes(inherit: true)) + { + Newtonsoft.Json.JsonConverter converter = (Newtonsoft.Json.JsonConverter)Activator.CreateInstance(attribute.ConverterType, attribute.ConverterParameters); + serializer.Converters.Add(converter); + } + + object tmp = jToken.ToObject(prop.PropertyType, serializer); + prop.PropertyInfo.SetValue(element, tmp); + } + } + else if (value is System.Text.Json.JsonElement jElement) + { + var props = GetTypedSystemTextJsonProperties(elementType); + var prop = props.SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), key)); + if (prop != null) + { + System.Text.Json.JsonSerializerOptions? options = (args?.FirstOrDefault() as System.Text.Json.JsonSerializerOptions); + if (options == null) + { + options = new System.Text.Json.JsonSerializerOptions(); + } + else + { + options = new System.Text.Json.JsonSerializerOptions(options); + } + + foreach (System.Text.Json.Serialization.JsonConverterAttribute attribute in prop.PropertyInfo.GetCustomAttributes(inherit: true)) + { + System.Text.Json.Serialization.JsonConverter converter = (System.Text.Json.Serialization.JsonConverter)Activator.CreateInstance(attribute.ConverterType!); + options.Converters.Add(converter!); + } + + object tmp = System.Text.Json.JsonSerializer.Deserialize(jElement, prop.PropertyType, options)!; + prop.PropertyInfo.SetValue(element, tmp); + } + } + else + { + throw new NotSupportedException(); + } + + return element; + } + + public static string SerializeWhenHasNArray(T obj, Newtonsoft.Json.JsonSerializer? serializer = null) + where T : class, new() + { + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + static Newtonsoft.Json.Linq.JToken Flatten(Newtonsoft.Json.Linq.JToken jToken) + { + if (jToken.Type == Newtonsoft.Json.Linq.JTokenType.Array) + { + foreach (Newtonsoft.Json.Linq.JToken? jSubToken in jToken) + { + if (jSubToken == null) + continue; + + Flatten(jSubToken); + } + } + else if (jToken.Type == Newtonsoft.Json.Linq.JTokenType.Object) + { + string[] keys = ((Newtonsoft.Json.Linq.JObject)jToken).Properties().Select(p => p.Name).ToArray(); + foreach (string key in keys) + { + if (!PROPERTY_NAME_NARRAY.Equals(key)) + continue; + + int i = 0; + foreach (Newtonsoft.Json.Linq.JToken? jSubToken in jToken[key]) + { + if (jSubToken == null) + continue; + + foreach (Newtonsoft.Json.Linq.JProperty jSubKey in jSubToken) + { + jToken[jSubKey.Name.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, i.ToString())] = jSubKey.Value; + } + + i++; + } + } + + jToken[PROPERTY_NAME_NARRAY]?.Parent?.Remove(); + } + + return jToken; + } + + //StringBuilder stringBuilder = new StringBuilder(); + //using TextWriter stringWriter = new StringWriter(stringBuilder); + //serializer = serializer ?? Newtonsoft.Json.JsonSerializer.CreateDefault(); + //serializer.Serialize(stringWriter, obj, typeof(T)); + //string rawJson = stringBuilder.ToString(); + + //Newtonsoft.Json.Linq.JToken jToken = Newtonsoft.Json.Linq.JToken.Parse(rawJson); + //return Flatten(jToken).ToString(serializer.Formatting); + + // TODO + return default!; + } + + public static T DeserializeWhenHasNArray(ref Newtonsoft.Json.Linq.JObject jObject, Newtonsoft.Json.JsonSerializer? serializer = null) + where T : class, new() + { + var props = GetTypedNewtonsoftJsonProperties(typeof(T)); + if (props.Count(p => p.IsNArrayProperty) != 1) + throw new Newtonsoft.Json.JsonException("The number of `$n` properties must be only one."); + + T result = new T(); + foreach (Newtonsoft.Json.Linq.JProperty jKey in jObject.Properties()) + { + var prop = props.SingleOrDefault(e => e.PropertyName == jKey.Name); + if (prop != null) + { + // ͨ + object? value = serializer is null ? + jObject[prop.PropertyName]?.ToObject(prop.PropertyType) : + jObject[prop.PropertyName]?.ToObject(prop.PropertyType, serializer); + prop.PropertyInfo.SetValue(result, value); + } + else if (TryMatchNArrayIndex(jKey.Name, out int index)) + { + // $n + var narrProp = props.Single(e => e.IsNArrayProperty); + object? value = narrProp.PropertyInfo.GetValue(result); + + Array array = CreateOrExpandNArray(value, narrProp.PropertyType.GetElementType()!, index + 1); + object? element = CreateOrUpdateNArrayElement(array, narrProp.PropertyType.GetElementType()!, index, jKey.Name, jKey.Value, serializer); + narrProp.PropertyInfo.SetValue(result, array); + } + else if (serializer?.MissingMemberHandling == Newtonsoft.Json.MissingMemberHandling.Error) + { + throw new Newtonsoft.Json.JsonSerializationException($"Could not find member `{jKey.Name}` on object of type `{typeof(T).Name}`."); + } + } + return result; + } + + public static string SerializeWhenHasNArray(T obj, System.Text.Json.JsonSerializerOptions? options = null) + where T : class, new() + { + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + static System.Text.Json.Nodes.JsonNode Flatten( + System.Text.Json.Nodes.JsonNode jNode, + System.Text.Json.JsonSerializerOptions jsonSerializerOptions, + System.Text.Json.JsonDocumentOptions jsonDocumentOptions, + System.Text.Json.Nodes.JsonNodeOptions jsonNodeOptions) + { + if (jNode is System.Text.Json.Nodes.JsonArray jNodeAsArray) + { + foreach (System.Text.Json.Nodes.JsonNode? jSubNode in jNodeAsArray) + { + if (jSubNode == null) + continue; + + Flatten(jSubNode, jsonSerializerOptions, jsonDocumentOptions, jsonNodeOptions); + } + } + else if (jNode is System.Text.Json.Nodes.JsonObject jNodeAsObject) + { + string[] keys = jNodeAsObject.Select(e => e.Key).ToArray(); + foreach (string key in keys) + { + if (!PROPERTY_NAME_NARRAY.Equals(key)) + continue; + + int i = 0; + foreach (System.Text.Json.Nodes.JsonObject? jSubNode in jNodeAsObject[key]!.AsArray()) + { + if (jSubNode == null) + continue; + + foreach (var jSubKey in jSubNode) + { + string? json = jSubKey.Value?.ToJsonString(jsonSerializerOptions); + if (json != null) + jNodeAsObject[jSubKey.Key.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, i.ToString())] = System.Text.Json.Nodes.JsonNode.Parse(json, jsonNodeOptions, jsonDocumentOptions); + } + + i++; + } + } + + jNodeAsObject.Remove(PROPERTY_NAME_NARRAY); + } + + return jNode; + } + + // NOTICE: Ϊ JsonConverter Եʣﲻʹ Newtonsoft.Json лݹѭ + StringBuilder stringBuilder = new StringBuilder(); + using TextWriter stringWriter = new StringWriter(stringBuilder); + Newtonsoft.Json.JsonSerializer serializer = Newtonsoft.Json.JsonSerializer.CreateDefault(); + serializer.Serialize(stringWriter, obj, typeof(T)); + string rawJson = stringBuilder.ToString(); + + System.Text.Json.JsonSerializerOptions jsonSerializerOptions = options ?? new System.Text.Json.JsonSerializerOptions(); + System.Text.Json.JsonDocumentOptions jsonDocumentOptions = new System.Text.Json.JsonDocumentOptions() + { + AllowTrailingCommas = jsonSerializerOptions.AllowTrailingCommas, + CommentHandling = jsonSerializerOptions.ReadCommentHandling, + MaxDepth = jsonSerializerOptions.MaxDepth + }; + System.Text.Json.Nodes.JsonNodeOptions jsonNodeOptions = new System.Text.Json.Nodes.JsonNodeOptions() + { + PropertyNameCaseInsensitive = jsonSerializerOptions.PropertyNameCaseInsensitive + }; + System.Text.Json.Nodes.JsonNode jNode = System.Text.Json.Nodes.JsonNode.Parse(rawJson, jsonNodeOptions, jsonDocumentOptions)!; + return Flatten(jNode, jsonSerializerOptions, jsonDocumentOptions, jsonNodeOptions) + .ToJsonString(new System.Text.Json.JsonSerializerOptions() + { + WriteIndented = jsonSerializerOptions.WriteIndented, + Encoder = jsonSerializerOptions.Encoder + }); + } + + public static T DeserializeWhenHasNArray(ref System.Text.Json.JsonElement jElement, System.Text.Json.JsonSerializerOptions? options = null) + where T : class, new() + { + var props = GetTypedSystemTextJsonProperties(typeof(T)); + if (props.Count(p => p.IsNArrayProperty) != 1) + throw new System.Text.Json.JsonException("The number of `$n` properties must be only one."); + + T result = new T(); + foreach (System.Text.Json.JsonProperty jKey in jElement.EnumerateObject()) + { + var prop = props.SingleOrDefault(e => e.PropertyName == jKey.Name); + if (prop != null) + { + // ͨ + object? value = options is null ? + System.Text.Json.JsonSerializer.Deserialize(jKey.Value, prop.PropertyType, options) : + System.Text.Json.JsonSerializer.Deserialize(jKey.Value, prop.PropertyType, options); + prop.PropertyInfo.SetValue(result, value); + } + else if (TryMatchNArrayIndex(jKey.Name, out int index)) + { + // $n + var narrProp = props.Single(e => e.IsNArrayProperty); + object? value = narrProp.PropertyInfo.GetValue(result); + + Array array = CreateOrExpandNArray(value, narrProp.PropertyType.GetElementType()!, index + 1); + object? element = CreateOrUpdateNArrayElement(array, narrProp.PropertyType.GetElementType()!, index, jKey.Name, jKey.Value, options); + narrProp.PropertyInfo.SetValue(result, array); + } + } + return result; + } + } + + internal partial class JsonUtility + { + private sealed class InnerTypedJsonProperty + { + public string PropertyName { get; } + + public PropertyInfo PropertyInfo { get; } + + public Type PropertyType { get { return PropertyInfo.PropertyType; } } + + public bool IsNArrayProperty { get; } + + public InnerTypedJsonProperty(string propertyName, PropertyInfo propertyInfo, bool isNArrayProperty) + { + PropertyName = propertyName; + PropertyInfo = propertyInfo; + IsNArrayProperty = isNArrayProperty; + } + } + + private static readonly Hashtable _mappedTypeJsonProperties = new Hashtable(); + + private static InnerTypedJsonProperty[] GetTypedNewtonsoftJsonProperties(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string skey = "Newtosoft.Json:" + (type.AssemblyQualifiedName ?? type.GetHashCode().ToString()); + var props = (InnerTypedJsonProperty[]?)_mappedTypeJsonProperties[skey]; + if (props == null) + { + props = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => + (p.CanRead && !p.GetCustomAttributes(inherit: true).Any()) && + (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) + ) + .Select(p => + { + string name = p.GetCustomAttribute(inherit: true)?.PropertyName ?? p.Name; + return new InnerTypedJsonProperty + ( + propertyName: name, + propertyInfo: p, + isNArrayProperty: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass + ); + }) + .ToArray(); + _mappedTypeJsonProperties[skey] = props; + } + + return props; + } + + private static InnerTypedJsonProperty[] GetTypedSystemTextJsonProperties(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string skey = "System.Text.Json:" + (type.AssemblyQualifiedName ?? type.GetHashCode().ToString()); + var props = (InnerTypedJsonProperty[]?)_mappedTypeJsonProperties[skey]; + if (props == null) + { + props = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => + (p.CanRead && !p.GetCustomAttributes(inherit: true).Any()) && + (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) + ) + .Select(p => + { + string name = p.GetCustomAttribute(inherit: true)?.Name ?? p.Name; + return new InnerTypedJsonProperty + ( + propertyName: name, + propertyInfo: p, + isNArrayProperty: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass + ); + }) + .ToArray(); + _mappedTypeJsonProperties[skey] = props; + } + + return props; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/XmlUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/XmlUtility.cs new file mode 100644 index 00000000..1e072f1a --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/Internal/XmlUtility.cs @@ -0,0 +1,21 @@ +using System.Xml; +using Newtonsoft.Json; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities +{ + internal static class XmlUtility + { + public static string ConvertFromJson(string json) + { + XmlDocument xmlDocument = JsonConvert.DeserializeXmlNode(json); + return xmlDocument.InnerXml; + } + + public static string ConvertToJson(string xml) + { + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(xml); + return JsonConvert.SerializeXmlNode(xmlDocument); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/MD5Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/MD5Utility.cs new file mode 100644 index 00000000..05d33513 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/MD5Utility.cs @@ -0,0 +1,39 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities +{ + /// + /// MD5 算法工具类。 + /// + public static class MD5Utility + { + /// + /// 获取 MD5 信息摘要。 + /// + /// 信息字节数组。 + /// 信息摘要。 + public static string Hash(byte[] bytes) + { + if (bytes == null) throw new ArgumentNullException(nameof(bytes)); + + using MD5 md5 = MD5.Create(); + byte[] hashBytes = md5.ComputeHash(bytes); + return BitConverter.ToString(hashBytes).Replace("-", ""); + } + + /// + /// 获取 MD5 信息摘要。 + /// + /// 文本信息。 + /// 信息摘要。 + public static string Hash(string message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + byte[] bytes = Encoding.UTF8.GetBytes(message); + return Hash(bytes); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClient.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClient.cs new file mode 100644 index 00000000..1c464457 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClient.cs @@ -0,0 +1,153 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 一个微信支付 API HTTP 客户端。 + /// + public partial class WechatTenpayClient : CommonClientBase, ICommonClient + { + /// + /// 获取当前客户端使用的微信商户平台凭证。 + /// + public Settings.Credentials Credentials { get; } + + /// + /// 用指定的配置项初始化 类的新实例。 + /// + /// 配置项。 + public WechatTenpayClient(WechatTenpayClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + Credentials = new Settings.Credentials(options); + + FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT; + FlurlClient.WithTimeout(TimeSpan.FromMilliseconds(options.Timeout)); + FlurlClient.Configure((settings) => + settings.HttpClientFactory = new Settings.HttpClientFactory( + options.MerchantCertificateBytes, + options.MerchantCertificatePassword ?? options.MerchantId + ) + ); + } + + /// + /// 使用当前客户端生成一个新的 对象。 + /// + /// + /// + /// + /// + public IFlurlRequest CreateRequest(WechatTenpayRequest request, HttpMethod method, params object[] urlSegments) + { + IFlurlRequest flurlRequest = FlurlClient.Request(urlSegments).WithVerb(method); + + if (request.Timeout != null) + { + flurlRequest.WithTimeout(TimeSpan.FromMilliseconds(request.Timeout.Value)); + } + + if (request.MerchantId == null) + { + request.MerchantId = Credentials.MerchantId; + } + + if (request.AppId == null) + { + request.AppId = Credentials.AppId; + } + + if (request is WechatTenpaySignableRequest signableRequest) + { + if (signableRequest.NonceString == null) + { + signableRequest.NonceString = Guid.NewGuid().ToString("N"); + } + + if (signableRequest.Signature == null) + { + string signType = signableRequest.SignType ?? Constants.SignTypes.MD5; + + // TODO: 生成签名算法 + throw new NotImplementedException(); + } + } + + return flurlRequest; + } + + /// + /// 异步发起请求。 + /// + /// + /// + /// + /// + /// + public async Task SendRequestWithXmlAsync(IFlurlRequest flurlRequest, object? data = null, CancellationToken cancellationToken = default) + where T : WechatTenpayResponse, 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; + if (isSimpleRequest) + { + using IFlurlResponse flurlResponse = await base.SendRequestAsync(flurlRequest, null, cancellationToken).ConfigureAwait(false); + return await WrapResponseWithXmlAsync(flurlResponse, cancellationToken).ConfigureAwait(false); + } + else + { + string json = JsonSerializer.Serialize(data); + string xml = Utilities.XmlUtility.ConvertFromJson(json); + + using HttpContent httpContent = new StringContent(xml, Encoding.UTF8, "text/xml"); + using IFlurlResponse flurlResponse = await base.SendRequestAsync(flurlRequest, httpContent, cancellationToken).ConfigureAwait(false); + return await WrapResponseWithXmlAsync(flurlResponse, cancellationToken).ConfigureAwait(false); + } + } + catch (FlurlHttpException ex) + { + throw new WechatTenpayException(ex.Message, ex); + } + } + + private async Task WrapResponseWithXmlAsync(IFlurlResponse flurlResponse, CancellationToken cancellationToken = default) + where TResponse : WechatTenpayResponse, new() + { + TResponse tmp = await WrapResponseAsync(flurlResponse, cancellationToken); + byte tmpb1 = tmp.RawBytes.SkipWhile(b => b <= 32).FirstOrDefault(), + tmpb2 = tmp.RawBytes.Reverse().SkipWhile(b => b <= 32).FirstOrDefault(); + bool xmlable = tmp.RawStatus == 200 && (tmpb1 == 60 && tmpb2 == 62); // "<...>" + + TResponse result; + if (xmlable) + { + string xml = Encoding.UTF8.GetString(tmp.RawBytes); + string json = Utilities.XmlUtility.ConvertToJson(xml); + + result = JsonSerializer.Deserialize(json); + result.RawStatus = tmp.RawStatus; + result.RawHeaders = tmp.RawHeaders; + result.RawBytes = tmp.RawBytes; + } + else + { + result = tmp; + } + + return result; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClientOptions.cs new file mode 100644 index 00000000..c2e8762a --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayClientOptions.cs @@ -0,0 +1,46 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 一个用于构造 时使用的配置项。 + /// + public class WechatTenpayClientOptions + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。 + /// 默认值:30000 + /// + public int Timeout { get; set; } = 30 * 1000; + + /// + /// 获取或设置微信支付 API 域名。 + /// 默认值: + /// + public string Endpoints { get; set; } = WechatTenpayEndpoints.DEFAULT; + + /// + /// 获取或设置微信商户号。 + /// + public string MerchantId { get; set; } = default!; + + /// + /// 获取或设置微信商户 API 密钥(注意与 API v3 密钥相区分)。 + /// + public string MerchantSecret { get; set; } = default!; + + /// + /// 获取或设置微信商户 API 证书内容字节数组。仅部分敏感接口需要传入此参数。 + /// + public byte[]? MerchantCertificateBytes { get; set; } + + /// + /// 获取或设置微信商户 API 证书导入密码。仅部分敏感接口需要传入此参数。 + /// 默认值:与 参数值相同。 + /// + public string? MerchantCertificatePassword { get; set; } + + /// + /// 获取或设置微信 AppId。若一个商户号下关联多个 AppId 的,该参数可以置空,改为在请求时传入。 + /// + public string? AppId { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayEndpoints.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayEndpoints.cs new file mode 100644 index 00000000..eaf58479 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayEndpoints.cs @@ -0,0 +1,23 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 微信支付 API 接口域名。 + /// + public static class WechatTenpayEndpoints + { + /// + /// 主域名(默认)。 + /// + public const string DEFAULT = "https://api.mch.weixin.qq.com"; + + /// + /// 容灾备用域名。 + /// + public const string BACKUP = "https://api2.mch.weixin.qq.com"; + + /// + /// 沙箱域名。 + /// + public const string SANDBOX = "https://api.mch.weixin.qq.com/sandboxnew"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayException.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayException.cs new file mode 100644 index 00000000..2ae5d5bb --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayException.cs @@ -0,0 +1,27 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 当调用微信支付 API 出错时引发的异常。 + /// + public class WechatTenpayException : CommonExceptionBase + { + /// + public WechatTenpayException() + { + } + + /// + public WechatTenpayException(string message) + : base(message) + { + } + + /// + public WechatTenpayException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayRequest.cs new file mode 100644 index 00000000..748b706f --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayRequest.cs @@ -0,0 +1,56 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 表示微信支付 API 请求的基类。 + /// + public abstract class WechatTenpayRequest : ICommonRequest + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。如果不指定将使用构造 时的 参数,这在需要指定特定耗时请求(比如上传或下载文件)的超时时间时很有用。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual int? Timeout { get; set; } + + /// + /// 获取或设置微信商户号。如果不指定将使用构造 时的 参数。 + /// + [Newtonsoft.Json.JsonProperty("mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("mch_id")] + public string? MerchantId { get; set; } + + /// + /// 获取或设置微信 AppId。如果不指定将使用构造 时的 参数。 + /// + [Newtonsoft.Json.JsonProperty("appid")] + [System.Text.Json.Serialization.JsonPropertyName("appid")] + public string? AppId { get; set; } + } + + /// + /// 表示微信支付 API 请求的基类。 + /// + public abstract class WechatTenpaySignableRequest : WechatTenpayRequest + { + /// + /// 获取或设置随机字符串。如果不指定将由系统自动生成。 + /// + [Newtonsoft.Json.JsonProperty("nonce_str")] + [System.Text.Json.Serialization.JsonPropertyName("nonce_str")] + public virtual string? NonceString { get; set; } + + /// + /// 获取或设置签名方式。需注意部分接口不支持指定签名方式。 + /// + [Newtonsoft.Json.JsonProperty("sign_type")] + [System.Text.Json.Serialization.JsonPropertyName("sign_type")] + public virtual string? SignType { get; set; } + + /// + /// 获取或设置签名。如果不指定将由系统自动生成。 + /// + [Newtonsoft.Json.JsonProperty("sign")] + [System.Text.Json.Serialization.JsonPropertyName("sign")] + public virtual string? Signature { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayResponse.cs new file mode 100644 index 00000000..9359e6df --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/WechatTenpayResponse.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2 +{ + /// + /// 表示微信支付 API 响应的基类。 + /// + public abstract class WechatTenpayResponse : ICommonResponse + { + /// + /// + /// + int ICommonResponse.RawStatus { get; set; } + + /// + /// + /// + IDictionary ICommonResponse.RawHeaders { get; set; } = default!; + + /// + /// + /// + byte[] ICommonResponse.RawBytes { get; set; } = default!; + + /// + /// 获取原始的 HTTP 响应状态码。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public int RawStatus + { + get { return ((ICommonResponse)this).RawStatus; } + internal set { ((ICommonResponse)this).RawStatus = value; } + } + + /// + /// 获取原始的 HTTP 响应表头集合。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public IDictionary RawHeaders + { + get { return ((ICommonResponse)this).RawHeaders; } + internal set { ((ICommonResponse)this).RawHeaders = value; } + } + + /// + /// 获取原始的 HTTP 响应正文。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public byte[] RawBytes + { + get { return ((ICommonResponse)this).RawBytes; } + internal set { ((ICommonResponse)this).RawBytes = value; } + } + + /// + /// 获取微信支付 API 返回的状态码。 + /// + [Newtonsoft.Json.JsonProperty("return_code")] + [System.Text.Json.Serialization.JsonPropertyName("return_code")] + public virtual string? ReturnCode { get; set; } + + /// + /// 获取微信支付 API 返回的状态描述。 + /// + [Newtonsoft.Json.JsonProperty("return_msg")] + [System.Text.Json.Serialization.JsonPropertyName("return_msg")] + public virtual string? ReturnMessage { get; set; } + + /// + /// 获取微信支付 API 返回的错误码。 + /// + [Newtonsoft.Json.JsonProperty("err_code")] + [System.Text.Json.Serialization.JsonPropertyName("err_code")] + public virtual string? ErrorCode { get; set; } + + /// + /// 获取微信支付 API 返回的状态描述。 + /// + [Newtonsoft.Json.JsonProperty("err_code_des")] + [System.Text.Json.Serialization.JsonPropertyName("err_code_des")] + public virtual string? ErrorCodeDescription { get; set; } + + /// + /// 获取或设置业务结果。 + /// + [Newtonsoft.Json.JsonProperty("result_code")] + [System.Text.Json.Serialization.JsonPropertyName("result_code")] + public virtual string? ResultCode { get; set; } + + /// + /// 获取或设置微信商户号。 + /// + [Newtonsoft.Json.JsonProperty("mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("mch_id")] + public virtual string? MerchantId { get; set; } + + /// + /// 获取或设置微信 AppId。 + /// + [Newtonsoft.Json.JsonProperty("appid")] + [System.Text.Json.Serialization.JsonPropertyName("appid")] + public virtual string? AppId { get; set; } + + /// + /// 获取一个值,该值指示调用微信 API 是否成功(即 HTTP 状态码为 200、且 return_code 值 SUCCESS)。 + /// + /// + public virtual bool IsSuccessful() + { + bool ret = RawStatus == 200 && "SUCCESS".Equals(ReturnCode) && string.IsNullOrEmpty(ErrorCode); + if (ret) + { + return string.IsNullOrEmpty(ResultCode) || "SUCCESS".Equals(ResultCode); + } + + return false; + } + } + + /// + /// 表示微信支付 API 响应的基类。 + /// + public abstract class WechatTenpaySignableResponse : WechatTenpayResponse + { + /// + /// 获取或设置随机字符串。 + /// + [Newtonsoft.Json.JsonProperty("nonce_str")] + [System.Text.Json.Serialization.JsonPropertyName("nonce_str")] + public virtual string? NonceString { get; set; } + + /// + /// 获取或设置签名类型。 + /// + [Newtonsoft.Json.JsonProperty("sign_type")] + [System.Text.Json.Serialization.JsonPropertyName("sign_type")] + public virtual string? SignType { get; set; } + + /// + /// 获取或设置签名。 + /// + [Newtonsoft.Json.JsonProperty("sign")] + [System.Text.Json.Serialization.JsonPropertyName("sign")] + public virtual string? Signature { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/Newtonsoft.Json/List[string]/TextualStringIListWithCommaConverter.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/Newtonsoft.Json/List[string]/TextualStringIListWithCommaConverter.cs index 9a9701c7..baa8904d 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/Newtonsoft.Json/List[string]/TextualStringIListWithCommaConverter.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Converters/Newtonsoft.Json/List[string]/TextualStringIListWithCommaConverter.cs @@ -2,12 +2,23 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using System.Linq; namespace Newtonsoft.Json.Converters { - internal class TextualStringIListWithCommaConverter : JsonConverter?> + internal class TextualStringIListWithCommaConverter : JsonConverter { - private readonly JsonConverter?> _converter = new TextualStringListWithCommaConverter(); + public override bool CanConvert(Type objectType) + { + bool ret = objectType == typeof(IList) || objectType == typeof(List); + if (!ret) + { + ret = objectType.IsGenericType && + objectType.GetGenericTypeDefinition() == typeof(List<>) && + objectType.GetElementType() == typeof(string); + } + return ret; + } public override bool CanRead { @@ -19,26 +30,32 @@ namespace Newtonsoft.Json.Converters get { return true; } } - public override IList? ReadJson(JsonReader reader, Type objectType, IList? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - return _converter.ReadJson(reader, objectType, ConvertIListToList(existingValue), hasExistingValue, serializer); - } - - public override void WriteJson(JsonWriter writer, IList? value, JsonSerializer serializer) - { - _converter.WriteJson(writer, ConvertIListToList(value), serializer); - } - - private List? ConvertIListToList(IList? src) - { - if (src == null) + if (reader.TokenType == JsonToken.Null) + { return null; + } + else if (reader.TokenType == JsonToken.String) + { + string? value = serializer.Deserialize(reader); + if (value == null) + return null; + if (string.IsNullOrEmpty(value)) + return new List(); - List? dest = src as List; - if (dest != null) - return dest; + return value.Split(',').ToList(); + } - return new List(src); + throw new JsonReaderException(); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue(string.Join(",", value)); + else + writer.WriteNull(); } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/MarketingBusifavor/UsersCoupons/QueryMarketingBusifavorUserCouponsResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/MarketingBusifavor/UsersCoupons/QueryMarketingBusifavorUserCouponsResponse.cs index f0733e5c..edffee21 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/MarketingBusifavor/UsersCoupons/QueryMarketingBusifavorUserCouponsResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/MarketingBusifavor/UsersCoupons/QueryMarketingBusifavorUserCouponsResponse.cs @@ -74,7 +74,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models /// [Newtonsoft.Json.JsonProperty("transferable")] [System.Text.Json.Serialization.JsonPropertyName("transferable")] - [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.StringTypedNullableBooleanConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.TextualNullableBooleanConverter))] public bool? IsTransferable { get; set; } /// @@ -82,7 +82,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models /// [Newtonsoft.Json.JsonProperty("shareable")] [System.Text.Json.Serialization.JsonPropertyName("shareable")] - [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.StringTypedNullableBooleanConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.TextualNullableBooleanConverter))] public bool? IsShareable { get; set; } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/PayScoreServiceOrder/GetPayScoreServiceOrderByOutOrderNumberResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/PayScoreServiceOrder/GetPayScoreServiceOrderByOutOrderNumberResponse.cs index 125c8fa0..b75d2a0e 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/PayScoreServiceOrder/GetPayScoreServiceOrderByOutOrderNumberResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Models/PayScoreServiceOrder/GetPayScoreServiceOrderByOutOrderNumberResponse.cs @@ -350,7 +350,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Models /// [Newtonsoft.Json.JsonProperty("need_collection")] [System.Text.Json.Serialization.JsonPropertyName("need_collection")] - [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.StringTypedNullableBooleanConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.TextualNullableBooleanConverter))] public bool? RequireCollection { get; set; } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/AssemblyInfo.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Properties/AssemblyInfo.cs similarity index 100% rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/AssemblyInfo.cs rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Properties/AssemblyInfo.cs diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.json new file mode 100644 index 00000000..b2e0085c --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationRequest.json @@ -0,0 +1,14 @@ +{ + "appid": "wx2421b1c4370ec43b", + "customs": "ZHENGZHOU_BS", + "mch_customs_no": "D00411", + "mch_id": "1262544101", + "order_fee": "13110", + "out_trade_no": "15112496832609", + "product_fee": "13110", + "sign": "8FF6CEF879FB9555CD580222E671E9D4", + "transaction_id": "1006930610201511241751403478", + "transport_fee": "0", + "fee_type": "CNY", + "sub_order_no": "15112496832609001" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.json new file mode 100644 index 00000000..132a9acc --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/CreateMerchantCustomsCustomDeclarationResponse.json @@ -0,0 +1,20 @@ +{ + "return_code": "", + "return_msg": "", + "sign_type": "", + "sign": "", + "appid": "", + "mch_id": "", + "result_code": "", + "err_code": "", + "err_code_des": "", + "state": "", + "transaction_id": "", + "out_trade_no": "", + "sub_order_no": "", + "sub_order_id": "", + "modify_time": "20091227091010", + "cert_check_result": "", + "verify_department": "", + "verify_department_trade_id": "" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.json new file mode 100644 index 00000000..a5d112ed --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationRequest.json @@ -0,0 +1,11 @@ +{ + "sign_type": "", + "sign": "", + "appid": "", + "mch_id": "", + "out_trade_no": "", + "transaction_id": "", + "sub_order_no": "", + "sub_order_id": "", + "customs": "" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.json new file mode 100644 index 00000000..ae760565 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.json @@ -0,0 +1,38 @@ +{ + "return_code": "SUCCESS", + "return_msg": "OK", + "sign": "C380BEC2BFD727A4B6845133519F3AD6", + "appid": "wxd678efh567hg6787", + "mch_id": "1230000109", + "result_code": "SUCCESS", + "err_code": "SUCCESS", + "err_code_des": "ERRCODE", + "transaction_id": "ERRMSG", + "count": 1, + "sub_order_no_0": "20150806125346", + "sub_order_id_0": "20150806125346", + "mch_customs_no_0": "mch_customs_no_0", + "customs_0": "SHANGHAI", + "duty_0": 888, + "fee_type_0": "CNY", + "order_fee_0": 888, + "transport_fee_0": 888, + "product_fee_0": 888, + "state_0": "UNDECLARED", + "explanation_0": "支付单已存在并且为非退单状态", + "modify_time_0": "20091227091010", + "cert_check_result_0": "UNCHECKED", + "sub_order_no_1": "201508061253461", + "sub_order_id_1": "201508061253461", + "mch_customs_no_1": "mch_customs_no_1", + "customs_1": "SHANGHAI1", + "duty_1": 8881, + "fee_type_1": "CNY1", + "order_fee_1": 8881, + "transport_fee_1": 8881, + "product_fee_1": 8881, + "state_1": "UNDECLARED1", + "explanation_1": "支付单已存在并且为非退单状态1", + "modify_time_1": "20091227091011", + "cert_check_result_1": "UNCHECKED1" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.json new file mode 100644 index 00000000..df3536e6 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationRequest.json @@ -0,0 +1,8 @@ +{ + "appid": "wxab8acb865bb16371", + "customs": "SHENZHEN", + "mch_customs_no": "440316T004", + "mch_id": "1900006511", + "transaction_id": "4200000027201712197200279161", + "sign": "5D98596798203B0B1D61445707F71F87" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.json new file mode 100644 index 00000000..beff687a --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/MerchantCustoms/RedeclareMerchantCustomsCustomDeclarationResponse.json @@ -0,0 +1,18 @@ +{ + "return_code": "", + "return_msg": "", + "sign_type": "", + "sign": "", + "appid": "", + "mch_id": "", + "result_code": "", + "err_code": "", + "err_code_des": "", + "state": "", + "transaction_id": "", + "out_trade_no": "", + "sub_order_no": "", + "sub_order_id": "", + "modify_time": "20091227091010", + "explanation": "" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayRequest.json new file mode 100644 index 00000000..e9e0b8cc --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayRequest.json @@ -0,0 +1,17 @@ +{ + "appid": "wxdace645e0bc2c424", + "attach": "test", + "auth_code": "130050378319653252", + "body": "被扫测试", + "detail": "{\"cost_price\":1,\"receipt_id\":\"wx123\",\"goods_detail\":[{\"goods_id\":\"商品编码\",\"wxpay_goods_id\":\"1001\",\"goods_name\":\"iPhone6s 16G\",\"quantity\":1,\"price\":1},{\"goods_id\":\"商品编码\",\"wxpay_goods_id\":\"1002\",\"goods_name\":\"iPhone6s 32G\",\"quantity\":1,\"price\":1}]}", + "device_info": "TEST01", + "goods_tag": "MEETING", + "mch_id": "1900009001", + "nonce_str": "4b4f6f692547affd2c8fadb39fed603a", + "out_trade_no": "19000090011489146530", + "spbill_create_ip": "14.23.150.211", + "sub_mch_id": "11383918", + "total_fee": "503", + "version": "1.0", + "sign": "144FF79B7391FE1BD0708470B7D8A2E3" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayResponse.json new file mode 100644 index 00000000..9f668743 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Pay/CreatePayMicroPayResponse.json @@ -0,0 +1,21 @@ +{ + "return_code": "SUCCESS", + "return_msg": "OK", + "appid": "wx2421b1c4370ec43b", + "mch_id": "10000100", + "device_info": "1000", + "nonce_str": "GOp3TRyMXzbMlkun", + "sign": "D6C76CB785F07992CDE05494BB7DF7FD", + "result_code": "SUCCESS", + "openid": "oUpF8uN95-Ptaags6E_roPHg7AG0", + "is_subscribe": "Y", + "trade_type": "MICROPAY", + "bank_type": "CCB_DEBIT", + "total_fee": "1", + "coupon_fee": "0", + "fee_type": "CNY", + "transaction_id": "1008450740201411110005820873", + "out_trade_no": "1415757673", + "attach": "订单额外描述", + "time_end": "20141111170043" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportRequest.json new file mode 100644 index 00000000..44c530da --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportRequest.json @@ -0,0 +1,10 @@ +{ + "appid": "wx8888888888888888", + "mch_id": "1900000109", + "device_info": "013467007045764", + "nonce_str": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS", + "sign": "C380BEC2BFD727A4B6845133519F3AD6", + "interface_url": "https://api.mch.weixin.qq.com/pay/batchreport/micropay/total", + "user_ip": "8.8.8.8", + "trades": "[{\n\t\t\"out_trade_no\": \"out_trade_no_test_1\",\n\t\t\"begin_time\": \"20160602203256\",\n\t\t\"end_time\": \"20160602203257\",\n\t\t\"state\": \"OK\",\n\t\t\"err_msg\": \"\"\n\t},\n\t{\n\t\t\"out_trade_no\": \"out_trade_no_test_2\",\n\t\t\"begin_time\": \"20160602203258\",\n\t\t\"end_time\": \"20160602203259\",\n\t\t\"state\": \"FAIL\",\n\t\t\"err_msg\": \"SYSTEMERROR\"\n\t}\n]" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportResponse.json new file mode 100644 index 00000000..ff8382af --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/PayITIL/SubmitPayITILReportResponse.json @@ -0,0 +1,5 @@ +{ + "return_code": "SUCCESS", + "return_msg": "OK", + "result_code": "SUCCESS" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdRequest.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdRequest.json new file mode 100644 index 00000000..1f6175db --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdRequest.json @@ -0,0 +1,9 @@ +{ + "appid": "", + "sub_appid": "", + "mch_id": "", + "sub_mch_id": "", + "auth_code": "", + "nonce_str": "", + "sign": "" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdResponse.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdResponse.json new file mode 100644 index 00000000..7c3df3d9 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/ModelSamples/Tools/ToolsAuthCodeToOpenIdResponse.json @@ -0,0 +1,14 @@ +{ + "return_code": "", + "return_msg": "", + "appid": "", + "sub_appid": "", + "mch_id": "", + "sub_mch_id": "", + "nonce_str": "", + "sign": "", + "result_code": "", + "err_code": "", + "openid": "", + "sub_openid": "" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj new file mode 100644 index 00000000..13810525 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj @@ -0,0 +1,39 @@ + + + + net472; netcoreapp3.1; net6.0 + latest + enable + true + false + + + + + + + Never + Never + + + PreserveNewest + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_CodeReviewAnalyzer.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_CodeReviewAnalyzer.cs new file mode 100644 index 00000000..d4011efb --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_CodeReviewAnalyzer.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Reflection; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests +{ + public class TestCase_CodeReviewAnalyzer + { + private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV2"); + + [Fact(DisplayName = "代码评审:分析 API 模型命名")] + public void TestApiModelsNaming() + { + CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex); + + if (ex != null) + throw ex; + + Assert.Null(ex); + } + + [Fact(DisplayName = "代码评审:分析 API 模型定义")] + public void TestApiModelsDefinition() + { + string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples"); + CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex); + + if (ex != null) + throw ex; + + Assert.Null(ex); + } + + [Fact(DisplayName = "代码评审:分析 API 接口命名")] + public void TestApiExtensionsNaming() + { + CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex); + + if (ex != null) + throw ex; + + Assert.Null(ex); + } + + [Fact(DisplayName = "代码评审:分析代码规范")] + public void TestCodeStyle() + { + string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory); + CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex); + + if (ex != null) + throw ex; + + Assert.Null(ex); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterTest.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterTest.cs new file mode 100644 index 00000000..e424c7c8 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterTest.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests +{ + public class TestCase_JsonConverterTest + { + [Fact(DisplayName = "Զת֮ `FlattenNArrayObjectConverterBase`")] + public void TestFlattenNArrayObjectConverter() + { + var newtonsoftJsonSerializer = new FlurlNewtonsoftJsonSerializer(); + var systemTextJsonSerializer = new FlurlSystemTextJsonSerializer(); + + string rawJson = "{\"return_code\":\"RETURN_CODE\",\"return_msg\":\"RETURN_MSG\",\"sign\":\"SIGN\",\"appid\":\"APPID\",\"mch_id\":\"MCH_ID\",\"result_code\":\"RESULT_CODE\",\"err_code\":\"ERR_CODE\",\"err_code_des\":\"ERR_CODE_DESC\",\"transaction_id\":\"TRANSACTION_ID\",\"count\":2,\"sub_order_no_0\":\"SUB_ORDER_NO_0\",\"sub_order_id_0\":\"SUB_ORDER_ID_0\",\"mch_customs_no_0\":\"MCH_CUSTOMS_NO_0\",\"customs_0\":\"CUSTOMS_0\",\"duty_0\":10,\"fee_type_0\":\"FEE_TYPE_0\",\"order_fee_0\":10,\"transport_fee_0\":10,\"product_fee_0\":10,\"state_0\":\"STATE_0\",\"explanation_0\":\"EXPLANATION_0\",\"modify_time_0\":\"20000101112233\",\"cert_check_result_0\":\"UNCHECKED\",\"sub_order_no_1\":\"SUB_ORDER_NO_1\",\"sub_order_id_1\":\"SUB_ORDER_ID_1\",\"mch_customs_no_1\":\"MCH_CUSTOMS_NO_1\",\"customs_1\":\"CUSTOMS_1\",\"duty_1\":11,\"fee_type_1\":\"FEE_TYPE_1\",\"order_fee_1\":11,\"transport_fee_1\":11,\"product_fee_1\":11,\"state_1\":\"STATE_1\",\"explanation_1\":\"EXPLANATION_1\",\"modify_time_1\":\"20010101112233\",\"cert_check_result_1\":\"UNCHECKED1\"}"; + var parsedObjByNewtonsoftJson = newtonsoftJsonSerializer.Deserialize(rawJson); + var parsedObjBySystemTextJson = systemTextJsonSerializer.Deserialize(rawJson); + + Assert.Equal("RETURN_CODE", parsedObjByNewtonsoftJson.ReturnCode); + Assert.Equal("RETURN_CODE", parsedObjBySystemTextJson.ReturnCode); + Assert.Equal("RETURN_MSG", parsedObjByNewtonsoftJson.ReturnMessage); + Assert.Equal("RETURN_MSG", parsedObjBySystemTextJson.ReturnMessage); + Assert.Equal("SIGN", parsedObjByNewtonsoftJson.Signature); + Assert.Equal("SIGN", parsedObjBySystemTextJson.Signature); + Assert.Equal("APPID", parsedObjByNewtonsoftJson.AppId); + Assert.Equal("APPID", parsedObjBySystemTextJson.AppId); + Assert.Equal("MCH_ID", parsedObjByNewtonsoftJson.MerchantId); + Assert.Equal("MCH_ID", parsedObjBySystemTextJson.MerchantId); + Assert.Equal("RESULT_CODE", parsedObjByNewtonsoftJson.ResultCode); + Assert.Equal("RESULT_CODE", parsedObjBySystemTextJson.ResultCode); + Assert.Equal("ERR_CODE", parsedObjByNewtonsoftJson.ErrorCode); + Assert.Equal("ERR_CODE", parsedObjBySystemTextJson.ErrorCode); + Assert.Equal("ERR_CODE_DESC", parsedObjByNewtonsoftJson.ErrorCodeDescription); + Assert.Equal("ERR_CODE_DESC", parsedObjBySystemTextJson.ErrorCodeDescription); + Assert.Equal("TRANSACTION_ID", parsedObjByNewtonsoftJson.TransactionId); + Assert.Equal("TRANSACTION_ID", parsedObjBySystemTextJson.TransactionId); + Assert.Equal(2, parsedObjByNewtonsoftJson.RecordCount); + Assert.Equal(2, parsedObjBySystemTextJson.RecordCount); + Assert.Equal("SUB_ORDER_NO_0", parsedObjByNewtonsoftJson.RecordList[0].SubOrderNumber); + Assert.Equal("SUB_ORDER_NO_0", parsedObjBySystemTextJson.RecordList[0].SubOrderNumber); + Assert.Equal("SUB_ORDER_ID_0", parsedObjByNewtonsoftJson.RecordList[0].SubOrderId); + Assert.Equal("SUB_ORDER_ID_0", parsedObjBySystemTextJson.RecordList[0].SubOrderId); + Assert.Equal("MCH_CUSTOMS_NO_0", parsedObjByNewtonsoftJson.RecordList[0].MerchantCustomsNumber); + Assert.Equal("MCH_CUSTOMS_NO_0", parsedObjBySystemTextJson.RecordList[0].MerchantCustomsNumber); + Assert.Equal("CUSTOMS_0", parsedObjByNewtonsoftJson.RecordList[0].Customs); + Assert.Equal("CUSTOMS_0", parsedObjBySystemTextJson.RecordList[0].Customs); + Assert.Equal(10, parsedObjByNewtonsoftJson.RecordList[0].Duty); + Assert.Equal(10, parsedObjBySystemTextJson.RecordList[0].Duty); + Assert.Equal("FEE_TYPE_0", parsedObjByNewtonsoftJson.RecordList[0].FeeType); + Assert.Equal("FEE_TYPE_0", parsedObjBySystemTextJson.RecordList[0].FeeType); + Assert.Equal(DateTimeOffset.Parse("2000-01-01 11:22:33"), parsedObjByNewtonsoftJson.RecordList[0].ModifyTime); + Assert.Equal(DateTimeOffset.Parse("2000-01-01 11:22:33"), parsedObjBySystemTextJson.RecordList[0].ModifyTime); + Assert.Equal("SUB_ORDER_NO_1", parsedObjByNewtonsoftJson.RecordList[1].SubOrderNumber); + Assert.Equal("SUB_ORDER_NO_1", parsedObjBySystemTextJson.RecordList[1].SubOrderNumber); + Assert.Equal("SUB_ORDER_ID_1", parsedObjByNewtonsoftJson.RecordList[1].SubOrderId); + Assert.Equal("SUB_ORDER_ID_1", parsedObjBySystemTextJson.RecordList[1].SubOrderId); + Assert.Equal("MCH_CUSTOMS_NO_1", parsedObjByNewtonsoftJson.RecordList[1].MerchantCustomsNumber); + Assert.Equal("MCH_CUSTOMS_NO_1", parsedObjBySystemTextJson.RecordList[1].MerchantCustomsNumber); + Assert.Equal("CUSTOMS_1", parsedObjByNewtonsoftJson.RecordList[1].Customs); + Assert.Equal("CUSTOMS_1", parsedObjBySystemTextJson.RecordList[1].Customs); + Assert.Equal(11, parsedObjByNewtonsoftJson.RecordList[1].Duty); + Assert.Equal(11, parsedObjBySystemTextJson.RecordList[1].Duty); + Assert.Equal("FEE_TYPE_1", parsedObjByNewtonsoftJson.RecordList[1].FeeType); + Assert.Equal("FEE_TYPE_1", parsedObjBySystemTextJson.RecordList[1].FeeType); + Assert.Equal(DateTimeOffset.Parse("2001-01-01 11:22:33"), parsedObjByNewtonsoftJson.RecordList[1].ModifyTime); + Assert.Equal(DateTimeOffset.Parse("2001-01-01 11:22:33"), parsedObjBySystemTextJson.RecordList[1].ModifyTime); + + string unparsedJsonByNewtonsoftJson = newtonsoftJsonSerializer.Serialize(parsedObjByNewtonsoftJson); + string unparsedJsonBySystemTextJson = systemTextJsonSerializer.Serialize(parsedObjByNewtonsoftJson); + + Assert.Contains("return_code", unparsedJsonByNewtonsoftJson); + Assert.Contains("return_code", unparsedJsonBySystemTextJson); + Assert.Contains("return_msg", unparsedJsonByNewtonsoftJson); + Assert.Contains("return_msg", unparsedJsonBySystemTextJson); + Assert.Contains("sub_order_no_0", unparsedJsonByNewtonsoftJson); + Assert.Contains("sub_order_no_0", unparsedJsonBySystemTextJson); + Assert.Contains("sub_order_id_0", unparsedJsonByNewtonsoftJson); + Assert.Contains("sub_order_id_0", unparsedJsonBySystemTextJson); + Assert.DoesNotContain("#n", unparsedJsonByNewtonsoftJson); + Assert.DoesNotContain("#n", unparsedJsonBySystemTextJson); + Assert.DoesNotContain("$n", unparsedJsonByNewtonsoftJson); + Assert.DoesNotContain("$n", unparsedJsonBySystemTextJson); + } + } +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestClients.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestClients.cs new file mode 100644 index 00000000..1626181c --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestClients.cs @@ -0,0 +1,16 @@ +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests +{ + class TestClients + { + static TestClients() + { + Instance = new WechatTenpayClient(new WechatTenpayClientOptions() + { + MerchantId = TestConfigs.WechatMerchantId, + MerchantSecret = TestConfigs.WechatMerchantSecret + }); + } + + public static readonly WechatTenpayClient Instance; + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestConfigs.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestConfigs.cs new file mode 100644 index 00000000..79237051 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestConfigs.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests +{ + class TestConfigs + { + static TestConfigs() + { + // NOTICE: 请在项目根目录下按照 appsettings.json 的格式新建 appsettings.local.json 填入测试参数。 + // WARNING: 请在 DEBUG 模式下运行测试用例。 + // WARNING: 敏感信息请不要提交到 git! + + try + { + using var stream = File.OpenRead("appsettings.local.json"); + using var jdoc = JsonDocument.Parse(stream); + + var config = jdoc.RootElement.GetProperty("TestConfig"); + WechatAppId = config.GetProperty("AppId").GetString()!; + WechatMerchantId = config.GetProperty("MerchantId").GetString()!; + WechatMerchantSecret = config.GetProperty("MerchantSecret").GetString()!; + WechatOpenId = config.GetProperty("OpenId").GetString()!; + + ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!; + ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!; + } + catch (Exception ex) + { + throw new Exception("加载配置文件 appsettings.local.json 失败,请查看 `InnerException` 了解具体失败原因", ex); + } + } + + public static readonly string WechatAppId; + public static readonly string WechatMerchantId; + public static readonly string WechatMerchantSecret; + public static readonly string WechatOpenId; + + public static readonly string ProjectSourceDirectory; + public static readonly string ProjectTestDirectory; + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.json new file mode 100644 index 00000000..a36666fb --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.json @@ -0,0 +1,10 @@ +{ + "TestConfig": { + "AppId": "请在此填写用于测试的微信 AppId", + "MerchantId": "请在此填写用于测试的微信商户号", + "MerchantSecret": "请在此填写用于测试的微信商户 API 密钥", + "OpenId": "请在此填写用于测试的微信用户唯一标识" + }, + "ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV2\\", + "ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\\" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.local.json b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.local.json new file mode 100644 index 00000000..6d260d54 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/appsettings.local.json @@ -0,0 +1,10 @@ +{ + "TestConfig": { + "AppId": "wxd861802f8e303335", + "MerchantId": "1601103314", + "MerchantSecret": "f09b03a7a1902b5b4913856f1fd07ab1", + "OpenId": "owNIE0msADfoPjhpy2cz1qL4vImw" + }, + "ProjectSourceDirectory": "D:\\Projects\\_SKIT\\stack-dotnet\\DotNetCore.SKIT.FlurlHttpClient.Wechat\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV2", + "ProjectTestDirectory": "D:\\Projects\\_SKIT\\stack-dotnet\\DotNetCore.SKIT.FlurlHttpClient.Wechat\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests" +} \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/CodeStyleUtil.cs b/test/SKIT.FlurlHttpClient.Wechat.TestTools/CodeStyleUtil.cs index 41882683..632a4826 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TestTools/CodeStyleUtil.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/CodeStyleUtil.cs @@ -411,7 +411,7 @@ namespace SKIT.FlurlHttpClient.Wechat string extCodeFileName = Path.GetFileName(extCodeFilePath); string[] segments = File.ReadAllText(extCodeFilePath) - .Split("", StringSplitOptions.RemoveEmptyEntries) + .Split(new string[] { "" }, StringSplitOptions.RemoveEmptyEntries) .Where(e => e.Contains("Async") && !e.Contains("public static class")) .ToArray(); for (int i = 0; i < segments.Length; i++) @@ -440,9 +440,9 @@ namespace SKIT.FlurlHttpClient.Wechat string expectedMethod = regexApi.Groups[1].Value.Trim(); string expectedUrl = regexApi.Groups[2].Value.Split('?')[0].Trim(); string actualMethod = sourceCode.Contains(".CreateRequest(request, new HttpMethod(\"") ? - sourceCode.Split(".CreateRequest(request, new HttpMethod(\"")[1].Split("\"")[0] : + sourceCode.Split(new string[] { ".CreateRequest(request, new HttpMethod(\"" }, StringSplitOptions.None)[1].Split('\"')[0] : sourceCode.Contains(".CreateRequest(request, HttpMethod.") ? - sourceCode.Split(".CreateRequest(request, HttpMethod.")[1].Split(",")[0].Split(")")[0] : + sourceCode.Split(new string[] { ".CreateRequest(request, HttpMethod." }, StringSplitOptions.None)[1].Split(',')[0].Split(')')[0] : string.Empty; if (!string.Equals(expectedMethod, actualMethod, StringComparison.OrdinalIgnoreCase)) { @@ -452,17 +452,17 @@ namespace SKIT.FlurlHttpClient.Wechat // 比对请求路由 string actualUrl = sourceCode - .Split("CreateRequest(request,", StringSplitOptions.RemoveEmptyEntries)[1] - .Substring(sourceCode.Split("CreateRequest(request,", StringSplitOptions.RemoveEmptyEntries)[1].Split(",")[0].Length + 1) + .Split(new string[] { "CreateRequest(request," }, StringSplitOptions.RemoveEmptyEntries)[1] + .Substring(sourceCode.Split(new string[] { "CreateRequest(request," }, StringSplitOptions.RemoveEmptyEntries)[1].Split(',')[0].Length + 1) .Split('\n')[0] .Trim() .TrimEnd(')', ';') .Trim(); - string[] expectedUrlSegments = expectedUrl.Split('/', StringSplitOptions.RemoveEmptyEntries); - string[] actualUrlSegments = actualUrl.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(e => e.Trim()).ToArray(); + string[] expectedUrlSegments = expectedUrl.Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries); + string[] actualUrlSegments = actualUrl.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(e => e.Trim()).ToArray(); if (expectedUrlSegments.Length != actualUrlSegments.Length) { - lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致(段数不等)。")); + lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:节长不等(实际 {actualUrlSegments.Length},期望 {expectedUrlSegments.Length})。")); return false; } else @@ -475,7 +475,7 @@ namespace SKIT.FlurlHttpClient.Wechat { if (actualUrlSegment.StartsWith("\"")) { - lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致(预期为变量展位符,实际为常量字符串)。")); + lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:第 {urlSegmentIndex} 节值不同。")); break; } } @@ -484,7 +484,7 @@ namespace SKIT.FlurlHttpClient.Wechat actualUrlSegment = actualUrlSegment.Replace("\"", string.Empty).Trim('/'); if (!string.Equals(expectedUrlSegment, actualUrlSegment)) { - lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致(预期为常量展位符,实际为变量字符串)。")); + lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:第 {urlSegmentIndex} 节值不同。")); break; } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj b/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj index 0276f14b..9117e429 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj @@ -1,8 +1,9 @@  - netcoreapp3.1; net6.0 - 9.0 + net472; netcoreapp3.1; net6.0 + latest + false