diff --git a/LOGO.png b/LOGO.png new file mode 100644 index 00000000..ebcd2d3f Binary files /dev/null and b/LOGO.png differ 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