From 3c73b930513ed9f28ed0d1672e14018802f9a8ac Mon Sep 17 00:00:00 2001
From: Diego <2248356998@qq.com>
Date: Wed, 14 May 2025 18:52:19 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../AppHostApplicationBuilderExtensions.cs | 81 +++++
.../App/Extensions/ObjectExtensions.cs | 7 +-
.../App/Internal/InternalApp.cs | 28 ++
.../Encryptions/AESEncryption.cs | 160 ++++-----
.../Encryptions/GzipEncryption.cs | 92 +++++
.../Extensions/StringEncryptionExtensions.cs | 60 +++-
...ApiControllerApplicationModelConvention.cs | 8 +-
.../DynamicApiRuntimeChangeProvider.cs | 2 -
.../Builders/EventBusOptionsBuilder.cs | 2 +-
.../EventBus/Contexts/EventHandlerContext.cs | 28 ++
.../Contexts/EventHandlerExecutingContext.cs | 14 +
.../EventBus/Events/EventHandlerEventArgs.cs | 6 +
.../HostedServices/EventBusHostedService.cs | 5 +-
.../ThingsGateway.Furion/JWT/JWTEncryption.cs | 9 +-
.../Console/ConsoleFormatterExtend.cs | 3 +-
.../Console/ConsoleFormatterExtendOptions.cs | 8 +
.../Implantations/Database/DatabaseLogger.cs | 2 +-
.../Database/DatabaseLoggerOptions.cs | 8 +
.../Logging/Implantations/File/FileLogger.cs | 2 +-
.../Implantations/File/FileLoggerOptions.cs | 8 +
.../Logging/Implantations/LogMessage.cs | 4 +-
.../Monitors/LoggingMonitorAttribute.cs | 4 +-
.../Monitors/LoggingMonitorSettings.cs | 8 +
.../Logging/Internal/Penetrates.cs | 6 +-
.../Factories/ISchedulerFactory.Internal.cs | 10 +-
.../Factories/SchedulerFactory.Internal.cs | 37 +-
.../HostedServices/ScheduleHostedService.cs | 2 +-
.../Builders/SpecificationDocumentBuilder.cs | 6 +-
.../TimeCrontab/Crontab.Internal.cs | 26 +-
.../TimeCrontab/Parsers/RandomParser.cs | 2 +-
.../UnifyResult/UnifyContext.cs | 10 +-
...meConverterUsingDateTimeParseAsFallback.cs | 38 ++
...erterUsingDateTimeOffsetParseAsFallback.cs | 38 ++
.../Converters/Json/StringJsonConverter.cs | 37 ++
.../Extensions/LinqExpressionExtensions.cs | 47 +--
.../Core/Extensions/StringExtensions.cs | 15 +-
.../Extensions/Utf8JsonReaderExtensions.cs | 15 +
.../Core/Extensions/V5_ObjectExtensions.cs | 39 ++-
.../Builders/HttpContextForwardBuilder.cs | 17 +-
.../Builders/HttpMultipartFormDataBuilder.cs | 46 ++-
.../Builders/HttpRequestBuilder.Methods.cs | 312 ++++++++++++-----
.../Builders/HttpRequestBuilder.Properties.cs | 26 +-
.../HttpRequestBuilder.StaticMethods.cs | 115 ++++++
.../HttpRemote/Builders/HttpRequestBuilder.cs | 82 ++++-
.../HttpRemote/Constants/Constants.cs | 15 +-
.../Converters/ObjectContentConverter.cs | 53 ++-
.../Converters/VoidContentConverter.cs | 4 +-
.../Attributes/HttpVersionAttribute.cs | 30 ++
.../Attributes/RefererAttribute.cs | 30 ++
.../Attributes/SuppressExceptionsAttribute.cs | 45 +++
.../Builders/HttpDeclarativeBuilder.cs | 3 +
.../Extractors/HeaderDeclarativeExtractor.cs | 6 +-
.../HttpVersionDeclarativeExtractor.cs | 31 ++
.../Extractors/QueryDeclarativeExtractor.cs | 7 +-
.../Extractors/RefererDeclarativeExtractor.cs | 31 ++
.../SuppressExceptionsDeclarativeExtractor.cs | 31 ++
.../HttpDeclarativeExtractorContext.cs | 2 +-
.../Extensions/HttpContextExtensions.cs | 50 +--
.../HttpMultipartFormDataBuilderExtensions.cs | 88 +++++
.../Extensions/HttpRemoteExtensions.cs | 123 ++++++-
.../Factories/HttpContentConverterFactory.cs | 45 ++-
.../Factories/IHttpContentConverterFactory.cs | 8 +-
.../Managers/FileDownloadManager.cs | 23 +-
.../HttpRemote/Managers/FileUploadManager.cs | 8 +-
.../HttpRemote/Managers/LongPollingManager.cs | 18 +
.../Managers/ServerSentEventsManager.cs | 27 +-
.../Managers/StressTestHarnessManager.cs | 9 +
.../HttpRemote/Models/HttpRemoteAnalyzer.cs | 4 +-
.../HttpRemote/Models/HttpRemoteClient.cs | 186 ++++++++++
.../HttpRemote/Models/HttpRemoteResult.cs | 84 +++++
.../HttpRemote/Models/ServerSentEventsData.cs | 26 +-
.../HttpRemote/Options/HttpClientOptions.cs | 34 ++
.../HttpRemote/Options/HttpRemoteOptions.cs | 16 +-
.../Processors/StringContentProcessor.cs | 3 +-
.../Services/HttpRemoteService.Extensions.cs | 8 +-
.../Services/HttpRemoteService.HttpMethods.cs | 130 +++----
.../HttpRemote/Services/HttpRemoteService.cs | 126 +++++--
.../Services/IHttpRemoteService.Extensions.cs | 8 +-
.../IHttpRemoteService.HttpMethods.cs | 130 +++----
.../HttpRemote/Services/IHttpRemoteService.cs | 16 +-
.../HttpRemote/Utilities/HttpRemoteUtility.cs | 20 +-
.../WebSocket/WebSocketBinaryReceiveResult.cs | 2 +-
.../WebSocket/WebSocketTextReceiveResult.cs | 2 +-
.../RescuePolicy/Policies/FallbackPolicy.cs | 1 +
.../RescuePolicy/Policies/RetryPolicy.cs | 1 +
.../Shapeless/Attributes/ClayAttribute.cs | 19 +
.../Shapeless/Binders/ClayBinder.cs | 30 +-
.../Shapeless/Binders/ClayBinderProvider.cs | 14 +-
.../Shapeless/Clay/Clay.Enumerable.cs | 16 +
.../Shapeless/Clay/Clay.Exports.cs | 327 +++++++++++++++++-
.../Shapeless/Clay/Clay.Operator.cs | 147 ++++++++
.../Shapeless/Clay/Clay.Override.cs | 4 +
.../V5_Experience/Shapeless/Clay/Clay.cs | 59 +++-
.../Converters/ObjectToClayJsonConverter.cs | 40 +++
.../Extensions/ShapelessExtensions.cs | 77 +++++
.../ShapelessMvcBuilderExtensions.cs | 11 +
.../Shapeless/Models/ClayEventArgs.cs | 2 +-
.../Shapeless/Options/ClayOptions.cs | 28 +-
.../ThingsGateway.Razor.csproj | 2 +-
src/Directory.Build.props | 4 +-
.../CSharpScriptEngineExtension.cs | 2 -
.../ExpressionEvaluatorExtension.cs | 2 -
.../Services/Plugin/PluginService.cs | 2 -
src/Version.props | 2 +-
104 files changed, 3132 insertions(+), 615 deletions(-)
create mode 100644 src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs
create mode 100644 src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/HttpVersionAttribute.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/RefererAttribute.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Attributes/SuppressExceptionsAttribute.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/RefererDeclarativeExtractor.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Extensions/HttpMultipartFormDataBuilderExtensions.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Models/HttpRemoteClient.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Options/HttpClientOptions.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Attributes/ClayAttribute.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Clay/Clay.Operator.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Converters/ObjectToClayJsonConverter.cs
create mode 100644 src/Admin/ThingsGateway.Furion/V5_Experience/Shapeless/Extensions/ShapelessExtensions.cs
diff --git a/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs b/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs
new file mode 100644
index 000000000..c2472d75b
--- /dev/null
+++ b/src/Admin/ThingsGateway.Furion/App/Extensions/AppHostApplicationBuilderExtensions.cs
@@ -0,0 +1,81 @@
+// ------------------------------------------------------------------------
+// 版权信息
+// 版权归百小僧及百签科技(广东)有限公司所有。
+// 所有权利保留。
+// 官方网站:https://baiqian.com
+//
+// 许可证信息
+// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
+// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+using ThingsGateway;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// HostApplication 拓展
+///
+public static class AppHostApplicationBuilderExtensions
+{
+ ///
+ /// Host 应用注入
+ ///
+ /// Host 应用构建器
+ ///
+ /// HostApplicationBuilder
+ public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
+ {
+ // 初始化配置
+ InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService);
+
+ return hostApplicationBuilder;
+ }
+
+ ///
+ /// 注册依赖组件
+ ///
+ /// 派生自
+ /// Host 应用构建器
+ /// 组件参数
+ ///
+ public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, object options = default)
+ where TComponent : class, IServiceComponent, new()
+ {
+ hostApplicationBuilder.Services.AddComponent(options);
+
+ return hostApplicationBuilder;
+ }
+
+ ///
+ /// 注册依赖组件
+ ///
+ /// 派生自
+ /// 组件参数
+ /// Host 应用构建器
+ /// 组件参数
+ ///
+ public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default)
+ where TComponent : class, IServiceComponent, new()
+ {
+ hostApplicationBuilder.Services.AddComponent(options);
+
+ return hostApplicationBuilder;
+ }
+
+ ///
+ /// 注册依赖组件
+ ///
+ /// Host 应用构建器
+ /// 组件类型
+ /// 组件参数
+ ///
+ public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default)
+ {
+ hostApplicationBuilder.Services.AddComponent(componentType, options);
+
+ return hostApplicationBuilder;
+ }
+}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs b/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs
index 3855a3558..386fed9c7 100644
--- a/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/App/Extensions/ObjectExtensions.cs
@@ -467,18 +467,20 @@ public static class ObjectExtensions
return obj;
}
+
///
/// 查找方法指定特性,如果没找到则继续查找声明类
///
///
///
///
+ /// searchFromRuntimeType
///
- internal static TAttribute GetFoundAttribute(this MethodInfo method, bool inherit)
+ internal static TAttribute GetFoundAttribute(this MethodInfo method, bool inherit, bool searchFromReflectedType = false)
where TAttribute : Attribute
{
// 获取方法所在类型
- var declaringType = method.DeclaringType;
+ var declaringType = !searchFromReflectedType ? method.DeclaringType : method.ReflectedType; // 解决嵌套继承问题
var attributeType = typeof(TAttribute);
@@ -493,7 +495,6 @@ public static class ObjectExtensions
return foundAttribute;
}
-
///
/// 格式化字符串
///
diff --git a/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs b/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs
index 12269624d..22acf03cb 100644
--- a/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs
+++ b/src/Admin/ThingsGateway.Furion/App/Internal/InternalApp.cs
@@ -132,6 +132,34 @@ internal static class InternalApp
});
}
+ ///
+ /// 配置 Furion 框架(非 Web)
+ ///
+ ///
+ ///
+ internal static void ConfigureApplication(IHostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
+ {
+ // 存储环境对象
+ HostEnvironment = hostApplicationBuilder.Environment;
+
+ // 加载配置
+ AddJsonFiles(hostApplicationBuilder.Configuration, hostApplicationBuilder.Environment);
+
+ // 存储配置对象
+ Configuration = hostApplicationBuilder.Configuration;
+
+ // 存储服务提供器
+ InternalServices = hostApplicationBuilder.Services;
+
+ // 存储根服务
+ hostApplicationBuilder.Services.AddHostedService();
+
+ // 初始化应用服务
+ hostApplicationBuilder.Services.AddApp();
+
+ // 自动注册 BackgroundService
+ if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService();
+ }
///
/// 自动装载主机配置
///
diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs
index e5bcae81e..e727df64c 100644
--- a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs
+++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/AESEncryption.cs
@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
-using System.Runtime.CompilerServices;
+
using System.Security.Cryptography;
using System.Text;
@@ -29,34 +29,36 @@ public class AESEncryption
/// 偏移量
/// 模式
/// 填充
+ ///
///
- public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- var bKey = Encoding.UTF8.GetBytes(skey);
+ var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
+ if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
aesAlg.Mode = mode;
aesAlg.Padding = padding;
- // 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
- aesAlg.IV = iv ?? aesAlg.IV; // 如果未提供 IV,则使用随机生成的 IV
+ aesAlg.IV = iv ?? aesAlg.IV;
+ if (iv != null && iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
- using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
+ using var encryptor = aesAlg.CreateEncryptor();
using var msEncrypt = new MemoryStream();
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
- using (var swEncrypt = new StreamWriter(csEncrypt))
+ using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
{
swEncrypt.Write(text);
}
var encryptedContent = msEncrypt.ToArray();
- // 如果是 CBC 模式,将 IV 和密文拼接在一起
- if (mode != CipherMode.ECB)
+ // 仅在未提供 IV 时拼接 IV
+ if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
@@ -76,35 +78,43 @@ public class AESEncryption
/// 偏移量
/// 模式
/// 填充
+ ///
///
- public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
var fullCipher = Convert.FromBase64String(hash);
-
- var bKey = Encoding.UTF8.GetBytes(skey);
+ var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
+ if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
aesAlg.Mode = mode;
aesAlg.Padding = padding;
- // 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
- var bVector = new byte[16];
- var cipher = new byte[fullCipher.Length - bVector.Length];
+ if (iv == null)
+ {
+ if (fullCipher.Length < aesAlg.BlockSize / 8) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
- Unsafe.CopyBlock(ref bVector[0], ref fullCipher[0], (uint)bVector.Length);
- Unsafe.CopyBlock(ref cipher[0], ref fullCipher[bVector.Length], (uint)(fullCipher.Length - bVector.Length));
-
- aesAlg.IV = iv ?? bVector;
- fullCipher = cipher;
+ iv = new byte[aesAlg.BlockSize / 8];
+ var cipher = new byte[fullCipher.Length - iv.Length];
+ Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
+ Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
+ aesAlg.IV = iv;
+ fullCipher = cipher;
+ }
+ else
+ {
+ if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
+ aesAlg.IV = iv;
+ }
}
- using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
+ using var decryptor = aesAlg.CreateDecryptor();
using var msDecrypt = new MemoryStream(fullCipher);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
- using var srDecrypt = new StreamReader(csDecrypt);
+ using var srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8);
return srDecrypt.ReadToEnd();
}
@@ -117,19 +127,13 @@ public class AESEncryption
/// 偏移量
/// 模式
/// 填充
+ ///
/// 加密后的字节数组
- public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- // 确保密钥长度为 128 位、192 位或 256 位
- var bKey = new byte[32]; // 256 位密钥
- var keyBytes = Encoding.UTF8.GetBytes(skey);
- Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
-
- // 如果是 ECB 模式,不需要 IV
- if (mode != CipherMode.ECB)
- {
- iv ??= GenerateRandomIV(); // 生成随机 IV
- }
+ // 验证密钥长度
+ var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
+ if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
@@ -138,34 +142,29 @@ public class AESEncryption
if (mode != CipherMode.ECB)
{
- aesAlg.IV = iv;
+ aesAlg.IV = iv ?? GenerateRandomIV();
+ if (aesAlg.IV.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
using var memoryStream = new MemoryStream();
- using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Write);
-
- cryptoStream.Write(bytes, 0, bytes.Length);
- cryptoStream.FlushFinalBlock();
-
- // 如果是 CBC 模式,将 IV 和密文拼接在一起
- if (mode != CipherMode.ECB)
+ using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write))
{
- var result = new byte[aesAlg.IV.Length + memoryStream.ToArray().Length];
+ cryptoStream.Write(bytes, 0, bytes.Length);
+ cryptoStream.FlushFinalBlock();
+ }
+
+ var encryptedContent = memoryStream.ToArray();
+
+ // 仅在未提供 IV 时拼接 IV
+ if (mode != CipherMode.ECB && iv == null)
+ {
+ var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
- Buffer.BlockCopy(memoryStream.ToArray(), 0, result, aesAlg.IV.Length, memoryStream.ToArray().Length);
+ Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length);
return result;
}
- // 如果是 ECB 模式,直接返回密文
- return memoryStream.ToArray();
- }
-
- // 生成随机 IV
- private static byte[] GenerateRandomIV()
- {
- using var aes = Aes.Create();
- aes.GenerateIV();
- return aes.IV;
+ return encryptedContent;
}
///
@@ -176,25 +175,13 @@ public class AESEncryption
/// 偏移量
/// 模式
/// 填充
+ ///
///
- public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- // 确保密钥长度为 128 位、192 位或 256 位
- var bKey = new byte[32]; // 256 位密钥
- var keyBytes = Encoding.UTF8.GetBytes(skey);
- Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
-
- // 如果是 ECB 模式,不需要 IV
- if (mode != CipherMode.ECB)
- {
- if (iv == null)
- {
- // 从密文中提取 IV
- iv = new byte[16];
- Array.Copy(bytes, iv, iv.Length);
- bytes = bytes.Skip(iv.Length).ToArray();
- }
- }
+ // 验证密钥长度
+ var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
+ if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create();
aesAlg.Key = bKey;
@@ -203,21 +190,36 @@ public class AESEncryption
if (mode != CipherMode.ECB)
{
+ if (iv == null)
+ {
+ // 提取IV
+ if (bytes.Length < 16) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
+ iv = bytes.Take(16).ToArray();
+ bytes = bytes.Skip(16).ToArray();
+ }
+ else
+ {
+ if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
+ }
aesAlg.IV = iv;
}
using var memoryStream = new MemoryStream(bytes);
- using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Read);
+ using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read);
using var originalStream = new MemoryStream();
- var buffer = new byte[1024];
- var readBytes = 0;
-
- while ((readBytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
- {
- originalStream.Write(buffer, 0, readBytes);
- }
-
+ cryptoStream.CopyTo(originalStream);
return originalStream.ToArray();
}
+
+ ///
+ /// 生成随机 IV
+ ///
+ ///
+ private static byte[] GenerateRandomIV()
+ {
+ using var aes = Aes.Create();
+ aes.GenerateIV();
+ return aes.IV;
+ }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs
new file mode 100644
index 000000000..5e8aca415
--- /dev/null
+++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Encryptions/GzipEncryption.cs
@@ -0,0 +1,92 @@
+// ------------------------------------------------------------------------
+// 版权信息
+// 版权归百小僧及百签科技(广东)有限公司所有。
+// 所有权利保留。
+// 官方网站:https://baiqian.com
+//
+// 许可证信息
+// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
+// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
+// ------------------------------------------------------------------------
+
+using System.IO.Compression;
+using System.Text;
+
+namespace ThingsGateway.DataEncryption;
+
+///
+/// GZip 压缩解压
+///
+[SuppressSniffer]
+public static class GzipEncryption
+{
+ ///
+ /// 压缩字符串并返回字节数组
+ ///
+ ///
+ ///
+ public static byte[] Compress(string text)
+ {
+ var buffer = Encoding.UTF8.GetBytes(text);
+
+ using var ms = new MemoryStream();
+ using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
+ {
+ zip.Write(buffer, 0, buffer.Length);
+ }
+
+ return ms.ToArray();
+ }
+
+ ///
+ /// 从字节数组解压
+ ///
+ ///
+ ///
+ public static string Decompress(byte[] bytes)
+ {
+ using var ms = new MemoryStream(bytes);
+ using var zip = new GZipStream(ms, CompressionMode.Decompress);
+ using var outStream = new MemoryStream();
+
+ zip.CopyTo(outStream);
+
+ return Encoding.UTF8.GetString(outStream.ToArray());
+ }
+
+ ///
+ /// 压缩字符串并返回 Base64 字符串
+ ///
+ ///
+ ///
+ public static string CompressToBase64(string text)
+ {
+ var buffer = Encoding.UTF8.GetBytes(text);
+
+ using var ms = new MemoryStream();
+ using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
+ {
+ zip.Write(buffer, 0, buffer.Length);
+ }
+
+ return Convert.ToBase64String(ms.ToArray());
+ }
+
+ ///
+ /// 从 Base64 字符串解压
+ ///
+ ///
+ ///
+ public static string DecompressFromBase64(string base64String)
+ {
+ var compressedData = Convert.FromBase64String(base64String);
+
+ using var ms = new MemoryStream(compressedData);
+ using var zip = new GZipStream(ms, CompressionMode.Decompress);
+ using var outStream = new MemoryStream();
+
+ zip.CopyTo(outStream);
+
+ return Encoding.UTF8.GetString(outStream.ToArray());
+ }
+}
diff --git a/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs b/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs
index bb5251a1d..5b26b9056 100644
--- a/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/DataEncryption/Extensions/StringEncryptionExtensions.cs
@@ -77,10 +77,11 @@ public static class StringEncryptionExtensions
/// 偏移量
/// 模式
/// 填充
+ ///
/// string
- public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- return AESEncryption.Encrypt(text, skey, iv, mode, padding);
+ return AESEncryption.Encrypt(text, skey, iv, mode, padding, isBase64);
}
///
@@ -91,10 +92,11 @@ public static class StringEncryptionExtensions
/// 偏移量
/// 模式
/// 填充
+ ///
/// string
- public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- return AESEncryption.Decrypt(text, skey, iv, mode, padding);
+ return AESEncryption.Decrypt(text, skey, iv, mode, padding, isBase64);
}
///
@@ -105,10 +107,11 @@ public static class StringEncryptionExtensions
/// 偏移量
/// 模式
/// 填充
+ ///
/// string
- public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- return AESEncryption.Encrypt(bytes, skey, iv, mode, padding);
+ return AESEncryption.Encrypt(bytes, skey, iv, mode, padding, isBase64);
}
///
@@ -119,10 +122,11 @@ public static class StringEncryptionExtensions
/// 偏移量
/// 模式
/// 填充
+ ///
/// string
- public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
+ public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
- return AESEncryption.Decrypt(bytes, skey, iv, mode, padding);
+ return AESEncryption.Decrypt(bytes, skey, iv, mode, padding, isBase64);
}
///
@@ -243,4 +247,44 @@ public static class StringEncryptionExtensions
{
return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength);
}
+
+ ///
+ /// Gzip 压缩字符串并返回字节数组
+ ///
+ ///
+ ///
+ public static byte[] ToGzipCompress(this string text)
+ {
+ return GzipEncryption.Compress(text);
+ }
+
+ ///
+ /// Gzip 从字节数组解压
+ ///
+ ///
+ ///
+ public static string ToGzipDecompress(this byte[] bytes)
+ {
+ return GzipEncryption.Decompress(bytes);
+ }
+
+ ///
+ /// Gzip 压缩字符串并返回 Base64 字符串
+ ///
+ ///
+ ///
+ public static string ToGzipCompressToBase64(this string text)
+ {
+ return GzipEncryption.CompressToBase64(text);
+ }
+
+ ///
+ /// Gzip 从 Base64 字符串解压
+ ///
+ ///
+ ///
+ public static string ToGzipDecompressFromBase64(this string base64String)
+ {
+ return GzipEncryption.DecompressFromBase64(base64String);
+ }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs b/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs
index 9c4bbf70e..58ca1f379 100644
--- a/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs
+++ b/src/Admin/ThingsGateway.Furion/DynamicApiController/Conventions/DynamicApiControllerApplicationModelConvention.cs
@@ -565,10 +565,10 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase();
// 判断是否贴有任何 [FromXXX] 特性了
- var hasFormAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
+ var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
// 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性
- if (isQueryParametersAction && !hasFormAttribute)
+ if (isQueryParametersAction && !hasFromAttribute)
{
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue;
@@ -577,7 +577,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过
// 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过
if (!parameterAttributes.Any(u => u is FromRouteAttribute)
- && (!parameterType.IsRichPrimitive() || hasFormAttribute)) continue;
+ && (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue;
// 处理基元数组数组类型,还有全局配置参数问题
if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray)
@@ -588,7 +588,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 处理 [ApiController] 特性情况
// https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference
- if (!hasFormAttribute && hasApiControllerAttribute) continue;
+ if (!hasFromAttribute && hasApiControllerAttribute) continue;
// 处理默认基元参数绑定方式,若是 query([FromQuery])则跳过
if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query")
diff --git a/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs b/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs
index ad4ee6417..345b93033 100644
--- a/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs
+++ b/src/Admin/ThingsGateway.Furion/DynamicApiController/Runtimes/DynamicApiRuntimeChangeProvider.cs
@@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange
if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart);
}
- GC.Collect();
- GC.WaitForPendingFinalizers();
}
}
diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs b/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs
index f0103877e..dda9145e1 100644
--- a/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs
+++ b/src/Admin/ThingsGateway.Furion/EventBus/Builders/EventBusOptionsBuilder.cs
@@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder
///
/// 是否启用执行完成触发 GC 回收
///
- public bool GCCollect { get; set; } = true;
+ public bool GCCollect { get; set; } = false;
///
/// 是否启用日志记录
diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs
index 64566f767..79556a991 100644
--- a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs
+++ b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerContext.cs
@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------
using System.Reflection;
+using System.Text.Json;
namespace ThingsGateway.EventBus;
@@ -57,4 +58,31 @@ public abstract class EventHandlerContext
///
/// 如果是动态订阅,可能为 null
public EventSubscribeAttribute Attribute { get; }
+
+ private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default)
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ ///
+ /// 获取负载数据
+ ///
+ ///
+ ///
+ public T GetPayload()
+ {
+ var rawPayload = Source.Payload;
+
+ if (rawPayload is null)
+ {
+ return default;
+ }
+ else if (rawPayload is JsonElement jsonElement)
+ {
+ return JsonSerializer.Deserialize(jsonElement.GetRawText(), JsonSerializerOptions);
+ }
+ else
+ {
+ return (T)rawPayload;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs
index 38de54585..c32a4d0cd 100644
--- a/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs
+++ b/src/Admin/ThingsGateway.Furion/EventBus/Contexts/EventHandlerExecutingContext.cs
@@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext
/// 执行前时间
///
public DateTime ExecutingTime { get; internal set; }
+
+ ///
+ /// 执行结果
+ ///
+ internal object Result { get; private set; }
+
+ ///
+ /// 设置执行结果
+ ///
+ ///
+ public void SetResult(object result)
+ {
+ Result = result;
+ }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs b/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs
index 0920966b7..39a2ca792 100644
--- a/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs
+++ b/src/Admin/ThingsGateway.Furion/EventBus/Events/EventHandlerEventArgs.cs
@@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs
/// 异常信息
///
public Exception Exception { get; internal set; }
+
+
+ ///
+ /// 执行结果
+ ///
+ public object Result { get; internal set; }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs b/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs
index 315982978..a4cc8a5fc 100644
--- a/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs
+++ b/src/Admin/ThingsGateway.Furion/EventBus/HostedServices/EventBusHostedService.cs
@@ -304,7 +304,10 @@ internal sealed class EventBusHostedService : BackgroundService
}
// 触发事件处理程序事件
- _eventPublisher.InvokeEvents(new(eventSource, true));
+ _eventPublisher.InvokeEvents(new(eventSource, true)
+ {
+ Result = eventHandlerExecutingContext.Result
+ });
}
catch (Exception ex)
{
diff --git a/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs b/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs
index 7e1315d33..272261d4a 100644
--- a/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs
+++ b/src/Admin/ThingsGateway.Furion/JWT/JWTEncryption.cs
@@ -198,8 +198,9 @@ public class JWTEncryption
/// 新刷新 Token 有效期(分钟)
///
///
+ /// 当刷新时触发
///
- public static async Task AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5)
+ public static async Task AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action onRefreshing = null)
{
// 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated)
@@ -245,7 +246,11 @@ public class JWTEncryption
// 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token
- httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime);
+ var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ;
+ httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken;
+
+ // 调用刷新后回调函数
+ onRefreshing?.Invoke(accessToken, refreshAccessToken);
// 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs
index 801d21324..3b418beda 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtend.cs
@@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable
, true
, _disableColors
, _formatterOptions.WithTraceId
- , _formatterOptions.WithStackFrame);
+ , _formatterOptions.WithStackFrame
+ , _formatterOptions.FormatProvider);
}
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs
index aac123fd4..990ed1807 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Console/ConsoleFormatterExtendOptions.cs
@@ -12,6 +12,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
+using System.Globalization;
+
namespace ThingsGateway.Logging;
///
@@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions
/// 日志消息内容转换(如脱敏处理)
///
public Func MessageProcess { get; set; }
+
+ ///
+ /// 格式化提供器
+ ///
+ ///
+ public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs
index 6c6ab23a4..a19a53028 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLogger.cs
@@ -118,7 +118,7 @@ public sealed class DatabaseLogger : ILogger
// 设置日志消息模板
logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg)
- : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame);
+ : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查
if (logMsg.Message is null)
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs
index a3b4511aa..3bddb8cd0 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Database/DatabaseLoggerOptions.cs
@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
+using System.Globalization;
+
namespace ThingsGateway.Logging;
///
@@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions
/// 日志消息内容转换(如脱敏处理)
///
public Func MessageProcess { get; set; }
+
+ ///
+ /// 格式化提供器
+ ///
+ ///
+ public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs
index b50a9a455..5870a4cc7 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLogger.cs
@@ -116,7 +116,7 @@ public sealed class FileLogger : ILogger
// 设置日志消息模板
logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg)
- : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame);
+ : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查
if (logMsg.Message is null)
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs
index 5d9291e88..755e29471 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/File/FileLoggerOptions.cs
@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
+using System.Globalization;
+
namespace ThingsGateway.Logging;
///
@@ -104,4 +106,10 @@ public sealed class FileLoggerOptions
/// 日志消息内容转换(如脱敏处理)
///
public Func MessageProcess { get; set; }
+
+ ///
+ /// 格式化提供器
+ ///
+ ///
+ public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs
index 20da875fa..b66d27aa4 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/LogMessage.cs
@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
+using System.Globalization;
+
namespace ThingsGateway.Logging;
///
@@ -120,6 +122,6 @@ public struct LogMessage
///
public override readonly string ToString()
{
- return Penetrates.OutputStandardMessage(this);
+ return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture);
}
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs
index 8de7cd80d..4a80674d3 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorAttribute.cs
@@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
///
///
///
- private static List GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
+ private List GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
{
var templates = new List();
@@ -219,7 +219,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var succeed = long.TryParse(value, out var seconds);
if (succeed)
{
- value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime():yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd} L)";
+ value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd", Settings.FormatProvider)} L)";
}
}
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs
index d83770ca7..f4d37b0f2 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Implantations/Monitors/LoggingMonitorSettings.cs
@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
+using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -143,4 +144,11 @@ public sealed class LoggingMonitorSettings
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
SkipValidation = true
};
+
+
+ ///
+ /// 格式化提供器
+ ///
+ ///
+ public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs b/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs
index 5f5cdcb27..07a582e45 100644
--- a/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs
+++ b/src/Admin/ThingsGateway.Furion/Logging/Internal/Penetrates.cs
@@ -107,13 +107,15 @@ internal static class Penetrates
///
///
///
+ ///
///
internal static string OutputStandardMessage(LogMessage logMsg
, string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"
, bool isConsole = false
, bool disableColors = true
, bool withTraceId = false
- , bool withStackFrame = false)
+ , bool withStackFrame = false
+ , IFormatProvider? provider = null)
{
// 空检查
if (logMsg.Message is null) return null;
@@ -127,7 +129,7 @@ internal static class Penetrates
_ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors);
formatString.Append(": ");
- formatString.Append(logMsg.LogDateTime.ToString(dateFormat));
+ formatString.Append(logMsg.LogDateTime.ToString(dateFormat, provider));
formatString.Append(' ');
formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L");
formatString.Append(' ');
diff --git a/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs b/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs
index 0ad895701..44ffb67c5 100644
--- a/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs
+++ b/src/Admin/ThingsGateway.Furion/Schedule/Factories/ISchedulerFactory.Internal.cs
@@ -78,9 +78,9 @@ public partial interface ISchedulerFactory
///
IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context);
- ///
- /// GC 垃圾回收器回收处理
- ///
- /// 避免频繁 GC 回收
- void GCCollect();
+ /////
+ ///// GC 垃圾回收器回收处理
+ /////
+ ///// 避免频繁 GC 回收
+ //void GCCollect();
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs b/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs
index 7f3ccfab8..bca173509 100644
--- a/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs
+++ b/src/Admin/ThingsGateway.Furion/Schedule/Factories/SchedulerFactory.Internal.cs
@@ -183,9 +183,10 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
// 标记当前方法初始化完成
PreloadCompleted = true;
- // 释放引用内存并立即回收GC
+ // 释放引用内存
_schedulerBuilders.Clear();
- GCCollect();
+
+ //GCCollect();
// 输出作业调度器初始化日志
if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count);
@@ -393,22 +394,22 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
return jobHandler;
}
- ///
- /// GC 垃圾回收器回收处理
- ///
- /// 避免频繁 GC 回收
- public void GCCollect()
- {
- var nowTime = DateTime.UtcNow;
- if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
- {
- LastGCCollectTime = nowTime;
+ /////
+ ///// GC 垃圾回收器回收处理
+ /////
+ ///// 避免频繁 GC 回收
+ //public void GCCollect()
+ //{
+ // var nowTime = DateTime.UtcNow;
+ // if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
+ // {
+ // LastGCCollectTime = nowTime;
- // 通知 GC 垃圾回收器立即回收
- GC.Collect();
- GC.WaitForPendingFinalizers();
- }
- }
+ // // 通知 GC 垃圾回收器立即回收
+ // GC.Collect();
+ // GC.WaitForPendingFinalizers();
+ // }
+ //}
///
/// 释放非托管资源
@@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
//_logger.LogWarning("Schedule hosted service cancels hibernation.");
// 通知 GC 垃圾回收器立即回收
- GCCollect();
+ //GCCollect();
});
}
diff --git a/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs b/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs
index 5f98ffd7f..d40b4ef89 100644
--- a/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs
+++ b/src/Admin/ThingsGateway.Furion/Schedule/HostedServices/ScheduleHostedService.cs
@@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService
_jobCancellationToken.Cancel(jobId, triggerId, false);
// 通知 GC 垃圾回收器回收
- _schedulerFactory.GCCollect();
+ //_schedulerFactory.GCCollect();
}
}, stoppingToken);
});
diff --git a/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs b/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs
index 465766fde..df10d78cf 100644
--- a/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs
+++ b/src/Admin/ThingsGateway.Furion/SpecificationDocument/Builders/SpecificationDocumentBuilder.cs
@@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder
}
// 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口
- var apiExplorerSettings = method.GetFoundAttribute(true);
-
- var apiDescriptionSettings = method.GetFoundAttribute(true);
-
+ var apiExplorerSettings = method.GetFoundAttribute(true, true);
+ var apiDescriptionSettings = method.GetFoundAttribute(true, true);
if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false;
if (currentGroup == AllGroupsKey)
diff --git a/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs b/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs
index 40267f34e..9898ffddb 100644
--- a/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs
+++ b/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs
@@ -433,10 +433,15 @@ public partial class Crontab
{
newValue = newValue.AddSeconds(-newValue.Second);
}
-
+ // 初始化是否存在随机 R 标识符
+ var randomSecond = false;
+ var randomMinute = false;
+ var randomHour = false;
// 获取分钟、小时所有字符解析器
var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast().ToList();
+ randomMinute = minuteParsers.OfType().Any();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast().ToList();
+ randomHour = hourParsers.OfType().Any();
// 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取下一个发生值的输入参数
@@ -456,7 +461,7 @@ public partial class Crontab
{
// 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast().ToList();
-
+ randomSecond = secondParsers.OfType().Any();
// 获取秒解析器最小起始值
firstSecondValue = secondParsers.Select(x => x.First()).Min();
@@ -519,8 +524,8 @@ public partial class Crontab
// 设置起始时间为下一个小时时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
- overflow ? firstMinuteValue : newMinutes,
- overflow ? firstSecondValue : newSeconds);
+ overflow && !randomMinute ? firstMinuteValue : newMinutes,
+ overflow && !randomSecond ? firstSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
@@ -534,7 +539,7 @@ public partial class Crontab
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
- if (!overflow)
+ if (!randomHour && !overflow)
{
return MinDate(newValue, endTime);
}
@@ -788,8 +793,15 @@ public partial class Crontab
/// 默认值
/// 控制秒、分钟、小时到达59秒/分和23小时开关
///
- private static int Increment(IEnumerable parsers, int value, int defaultValue, out bool overflow)
+ private static int Increment(List parsers, int value, int defaultValue, out bool overflow)
{
+ // 检查是否是随机 R 字符解析器
+ if (parsers.Count == 1 && parsers.First() is RandomParser randomParser)
+ {
+ overflow = true;
+ return randomParser.Next(value).Value;
+ }
+
var nextValue = parsers.Select(x => x.Next(value))
.Where(x => x > value)
.Min()
@@ -808,7 +820,7 @@ public partial class Crontab
/// 默认值
/// 控制秒、分钟、小时到达59秒/分和23小时开关
///
- private static int Decrement(IEnumerable parsers, int value, int defaultValue, out bool overflow)
+ private static int Decrement(List parsers, int value, int defaultValue, out bool overflow)
{
var previousValue = parsers.Select(x => x.Previous(value))
.Where(x => x < value)
diff --git a/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs b/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs
index fec69198d..7c12501bb 100644
--- a/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs
+++ b/src/Admin/ThingsGateway.Furion/TimeCrontab/Parsers/RandomParser.cs
@@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser
///
public bool IsMatch(DateTime datetime)
{
- return true;
+ return Kind is not CrontabFieldKind.Hour;
}
///
diff --git a/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs b/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs
index ad0783e3c..7e1c30922 100644
--- a/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs
+++ b/src/Admin/ThingsGateway.Furion/UnifyResult/UnifyContext.cs
@@ -168,7 +168,7 @@ public static class UnifyContext
if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null;
// 获取序列化配置
- var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute(true);
+ var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute(true, true);
if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null;
// 解析全局配置
@@ -225,7 +225,8 @@ public static class UnifyContext
|| method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType)
|| method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
|| method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
- || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData");
+ || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
+ || method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
if (!isWebRequest)
{
@@ -255,7 +256,8 @@ public static class UnifyContext
!method.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
&& method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
)
- || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData");
+ || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
+ || method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider;
return unifyResult == null || isSkip;
@@ -398,7 +400,7 @@ public static class UnifyContext
{
if (method == default) return default;
- var unityProviderAttribute = method.GetFoundAttribute(true);
+ var unityProviderAttribute = method.GetFoundAttribute(true, true);
// 获取元数据
var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata);
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs
new file mode 100644
index 000000000..c04e1dd5e
--- /dev/null
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeConverterUsingDateTimeParseAsFallback.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------
+// 版权信息
+// 版权归百小僧及百签科技(广东)有限公司所有。
+// 所有权利保留。
+// 官方网站:https://baiqian.com
+//
+// 许可证信息
+// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
+// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
+// ------------------------------------------------------------------------
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ThingsGateway.Converters.Json;
+
+///
+/// JSON 序列化转换器
+///
+/// 在不符合 ISO 8601-1:2019 格式的 时间使用 DateTime.Parse 作为回退。
+public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter
+{
+ ///
+ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // 尝试获取 ISO 8601-1:2019 格式时间
+ if (!reader.TryGetDateTime(out var value))
+ {
+ value = DateTime.Parse(reader.GetString()!);
+ }
+
+ return value;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) =>
+ JsonSerializer.Serialize(writer, value);
+}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs
new file mode 100644
index 000000000..2a4c0599c
--- /dev/null
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback.cs
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------
+// 版权信息
+// 版权归百小僧及百签科技(广东)有限公司所有。
+// 所有权利保留。
+// 官方网站:https://baiqian.com
+//
+// 许可证信息
+// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
+// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
+// ------------------------------------------------------------------------
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ThingsGateway.Converters.Json;
+
+///
+/// JSON 序列化转换器
+///
+/// 在不符合 ISO 8601-1:2019 格式的 时间使用 DateTimeOffset.Parse 作为回退。
+public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter
+{
+ ///
+ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // 尝试获取 ISO 8601-1:2019 格式时间
+ if (!reader.TryGetDateTimeOffset(out var value))
+ {
+ value = DateTimeOffset.Parse(reader.GetString()!);
+ }
+
+ return value;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
+ JsonSerializer.Serialize(writer, value);
+}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs
new file mode 100644
index 000000000..3efcc9512
--- /dev/null
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Converters/Json/StringJsonConverter.cs
@@ -0,0 +1,37 @@
+// ------------------------------------------------------------------------
+// 版权信息
+// 版权归百小僧及百签科技(广东)有限公司所有。
+// 所有权利保留。
+// 官方网站:https://baiqian.com
+//
+// 许可证信息
+// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
+// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
+// ------------------------------------------------------------------------
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using ThingsGateway.Extensions;
+
+namespace ThingsGateway.Converters.Json;
+
+///
+/// JSON 序列化转换器
+///
+/// 解决 Number 类型和 Boolean 类型转 String 类型时异常。
+public sealed class StringJsonConverter : JsonConverter
+{
+ ///
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
+ reader.TokenType switch
+ {
+ JsonTokenType.True or JsonTokenType.False => reader.GetBoolean().ToString(),
+ JsonTokenType.Number => reader.ConvertRawValueToString(),
+ _ => reader.GetString()
+ };
+
+ ///
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
+ writer.WriteStringValue(value);
+}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs
index c704e5164..3d53182b0 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/LinqExpressionExtensions.cs
@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------
using System.Linq.Expressions;
+using System.Reflection;
namespace ThingsGateway.Extensions;
@@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions
}
///
- /// 解析表达式属性名称
+ /// 解析表达式并获取属性的 实例
///
/// 对象类型
/// 属性类型
@@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions
///
///
///
- ///
+ ///
///
///
- internal static string GetPropertyName(this Expression> propertySelector) =>
+ internal static PropertyInfo GetProperty(this Expression> propertySelector) =>
propertySelector.Body switch
{
// 检查 Lambda 表达式的主体是否是 MemberExpression 类型
- MemberExpression memberExpression => GetPropertyName(memberExpression),
-
+ MemberExpression memberExpression => GetProperty(memberExpression),
// 如果主体是 UnaryExpression 类型,则继续解析
- UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName(
+ UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty(
nestedMemberExpression),
-
- _ => throw new ArgumentException("Expression is not valid for property selection.")
+ _ => throw new ArgumentException("Expression must be a simple member access (e.g. x => x.Property).",
+ nameof(propertySelector))
};
///
- /// 解析表达式属性名称
+ /// 从成员表达式中提取 实例
///
- /// 对象类型
///
///
///
+ /// 对象类型
///
- ///
+ ///
///
///
- internal static string GetPropertyName(MemberExpression memberExpression)
+ internal static PropertyInfo GetProperty(MemberExpression memberExpression)
{
// 空检查
ArgumentNullException.ThrowIfNull(memberExpression);
- // 获取属性声明类型
- var propertyType = memberExpression.Member.DeclaringType;
-
- // 检查是否越界访问属性
- if (propertyType != typeof(T))
+ // 确保表达式根是 T 类型的参数
+ if (memberExpression.Expression is not ParameterExpression parameterExpression ||
+ parameterExpression.Type != typeof(T))
{
- throw new ArgumentException("Invalid property selection.");
+ throw new ArgumentException(
+ $"Expression '{memberExpression}' must refer to a member of type '{typeof(T)}'.",
+ nameof(memberExpression));
}
- // 返回属性名称
- return memberExpression.Member.Name;
+ // 确保成员是属性(非字段)
+ if (memberExpression.Member is not PropertyInfo propertyInfo)
+ {
+ throw new ArgumentException(
+ $"Expression '{memberExpression}' refers to a field. Only properties are supported.",
+ nameof(memberExpression));
+ }
+
+ return propertyInfo;
}
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs
index 528f332fc..0aba62fa3 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/StringExtensions.cs
@@ -11,6 +11,7 @@
using Microsoft.Extensions.Configuration;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
@@ -149,7 +150,7 @@ internal static partial class StringExtensions
var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators);
return (from pair in pairs
- select pair.Split('=')
+ select pair.Split('=', 2) // 限制只分割一次
into keyValue
where keyValue.Length == 2
select new KeyValuePair(keyValue[0].Trim(), keyValue[1])).ToList();
@@ -328,6 +329,18 @@ internal static partial class StringExtensions
});
}
+ ///
+ /// 转换输入字符串中的任何转义字符
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
+ string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
+
///
/// 占位符匹配正则表达式
///
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs
index 8c6180782..d2871015f 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/Utf8JsonReaderExtensions.cs
@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
+using System.Buffers;
+using System.Text;
using System.Text.Json;
namespace ThingsGateway.Extensions;
@@ -34,4 +36,17 @@ internal static class Utf8JsonReaderExtensions
return jsonDocument.RootElement.Clone().GetRawText();
}
+
+ ///
+ /// 从 中提取原始值,并将其转换为字符串
+ ///
+ /// 支持处理各种类型的原始值(例如数字、布尔值等)。
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static string ConvertRawValueToString(this Utf8JsonReader reader) =>
+ Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs
index 65a51e8ed..a3cff3a2e 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/Core/Extensions/V5_ObjectExtensions.cs
@@ -97,16 +97,45 @@ internal static class V5_ObjectExtensions
case ICollection collection:
count = collection.Count;
return true;
+ // 检查对象是否实现了 IEnumerable 接口
+ case IEnumerable enumerable:
+ // 获取集合枚举数
+ var enumerator = enumerable.GetEnumerator();
+
+ try
+ {
+ // 检查枚举数是否可以推进到下一个元素
+ if (!enumerator.MoveNext())
+ {
+ count = 0;
+ return true;
+ }
+
+ // 枚举数循环推进到下一个元素并叠加推进次数
+ var c = 1;
+ while (enumerator.MoveNext())
+ {
+ c++;
+ }
+
+ count = c;
+ return true;
+ }
+ finally
+ {
+ // 检查枚举数是否实现了 IDisposable 接口
+ if (enumerator is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
}
// 反射查找是否存在 Count 属性
- var runtimeProperty = obj.GetType()
- .GetRuntimeProperty("Count");
+ var runtimeProperty = obj.GetType().GetRuntimeProperty("Count");
// 反射获取 Count 属性值
- if (runtimeProperty is not null
- && runtimeProperty.CanRead
- && runtimeProperty.PropertyType == typeof(int))
+ if (runtimeProperty is not null && runtimeProperty.CanRead && runtimeProperty.PropertyType == typeof(int))
{
count = (int)runtimeProperty.GetValue(obj)!;
return true;
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs
index 6bd6d7a22..049946203 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpContextForwardBuilder.cs
@@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder
///
/// 忽略在转发时需要跳过的请求标头列表
///
- internal static HashSet _ignoreRequestHeaders =
+ internal static readonly HashSet _ignoreRequestHeaders =
[
Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding",
"Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges"
@@ -356,8 +356,7 @@ public sealed class HttpContextForwardBuilder
if (multipartSection.AsFileSection() is not null)
{
// 复制多部分表单内容文件节内容
- await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, httpRequestBuilder,
- cancellationToken).ConfigureAwait(false);
+ await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -410,15 +409,11 @@ public sealed class HttpContextForwardBuilder
///
///
///
- ///
- ///
- ///
///
///
///
internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection,
- HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, HttpRequestBuilder httpRequestBuilder,
- CancellationToken cancellationToken)
+ HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, CancellationToken cancellationToken)
{
// 初始化 MemoryStream 实例
var memoryStream = new MemoryStream();
@@ -433,10 +428,8 @@ public sealed class HttpContextForwardBuilder
var fileMultipartSection = multipartSection.AsFileSection()!;
// 添加文件流
- httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName);
-
- // 添加文件流到请求结束时需要释放的集合中
- httpRequestBuilder.AddDisposable(memoryStream);
+ httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName,
+ disposeStreamOnRequestCompletion: true);
}
///
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs
index 28af3b58a..f119cf20b 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpMultipartFormDataBuilder.cs
@@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder
///
///
///
- public HttpMultipartFormDataBuilder AddJson(object rawJson, string? name = null, Encoding? contentEncoding = null,
+ public HttpMultipartFormDataBuilder AddJson(object? rawJson, string? name = null, Encoding? contentEncoding = null,
string? contentType = null)
{
- // 空检查
- ArgumentNullException.ThrowIfNull(rawJson);
-
// 检查是否配置表单名或不是字符串类型
if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString)
{
@@ -292,10 +289,8 @@ public sealed class HttpMultipartFormDataBuilder
// 从互联网 URL 地址中加载流
var fileStream = Helpers.GetStreamFromRemote(url);
- // 添加文件流到请求结束时需要释放的集合中
- _httpRequestBuilder.AddDisposable(fileStream);
-
- return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
+ return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
+ true);
}
///
@@ -365,10 +360,8 @@ public sealed class HttpMultipartFormDataBuilder
// 读取文件流(没有 using)
var fileStream = File.OpenRead(filePath);
- // 添加文件流到请求结束时需要释放的集合中
- _httpRequestBuilder.AddDisposable(fileStream);
-
- return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
+ return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
+ true);
}
///
@@ -407,10 +400,8 @@ public sealed class HttpMultipartFormDataBuilder
// 初始化带读写进度的文件流
var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName);
- // 添加文件流到请求结束时需要释放的集合中
- _httpRequestBuilder.AddDisposable(progressFileStream);
-
- return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding);
+ return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding,
+ true);
}
///
@@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder
/// 文件的名称
/// 内容类型
/// 内容编码
+ /// 是否在请求结束后自动释放流。默认值为:false
///
///
///
public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null,
- string? contentType = null, Encoding? contentEncoding = null)
+ string? contentType = null, Encoding? contentEncoding = null, bool disposeStreamOnRequestCompletion = false)
{
// 空检查
ArgumentNullException.ThrowIfNull(stream);
@@ -529,6 +521,12 @@ public sealed class HttpMultipartFormDataBuilder
FileName = fileName
});
+ // 是否在请求结束后自动释放流
+ if (disposeStreamOnRequestCompletion)
+ {
+ _httpRequestBuilder.AddDisposable(stream);
+ }
+
return this;
}
@@ -697,6 +695,20 @@ public sealed class HttpMultipartFormDataBuilder
return this;
}
+ ///
+ /// 设置是否移除默认的多部分内容的 Content-Type
+ ///
+ /// 如果为 true 则移除,默认为 false
+ ///
+ ///
+ ///
+ public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
+ {
+ OmitContentType = omit;
+
+ return this;
+ }
+
///
/// 构建 实例
///
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs
index c557acab0..bb13838e9 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Methods.cs
@@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder
/// 键
/// 值
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的请求标头。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的请求标头。默认值为 false。
///
///
///
- public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null,
- IEqualityComparer? comparer = null, bool replace = false)
+ public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, bool replace = false,
+ CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
- return WithHeaders(new Dictionary { { key, value } }, escape, culture, comparer, replace);
+ return WithHeaders(new Dictionary { { key, value } }, escape, replace, culture);
}
///
@@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder
/// 支持多次调用。
/// 请求标头集合
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的请求标头。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的请求标头。默认值为 false。
///
///
///
public HttpRequestBuilder WithHeaders(IDictionary headers, bool escape = false,
- CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false)
+ bool replace = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(headers);
// 初始化请求标头
- Headers ??= new Dictionary>(comparer);
- var objectHeaders = new Dictionary>(comparer);
+ Headers ??= new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ var objectHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加
objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -380,7 +374,7 @@ public sealed partial class HttpRequestBuilder
Headers = objectHeaders.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
- comparer);
+ StringComparer.OrdinalIgnoreCase);
return this;
}
@@ -391,26 +385,23 @@ public sealed partial class HttpRequestBuilder
/// 支持多次调用。
/// 请求标头源对象
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的请求标头。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的请求标头。默认值为 false。
///
///
///
- public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null,
- IEqualityComparer? comparer = null, bool replace = false)
+ public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, bool replace = false,
+ CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(headerSource);
return WithHeaders(
headerSource.ObjectToDictionary()!.ToDictionary(
- u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture,
- comparer, replace);
+ u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, replace,
+ culture);
}
///
@@ -474,6 +465,7 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder SetTimeout(TimeSpan timeout)
{
Timeout = timeout;
+ TimeoutAction = null;
return this;
}
@@ -494,6 +486,43 @@ public sealed partial class HttpRequestBuilder
}
Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
+ TimeoutAction = null;
+
+ return this;
+ }
+
+ ///
+ /// 设置超时时间
+ ///
+ /// 超时时间
+ /// 超时发生时要执行的操作
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout)
+ {
+ // 空检查
+ ArgumentNullException.ThrowIfNull(onTimeout);
+
+ SetTimeout(timeout).TimeoutAction = onTimeout;
+
+ return this;
+ }
+
+ ///
+ /// 设置超时时间
+ ///
+ /// 超时时间(毫秒)
+ /// 超时发生时要执行的操作
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetTimeout(double timeoutMilliseconds, Action onTimeout)
+ {
+ // 空检查
+ ArgumentNullException.ThrowIfNull(onTimeout);
+
+ SetTimeout(timeoutMilliseconds).TimeoutAction = onTimeout;
return this;
}
@@ -570,26 +599,22 @@ public sealed partial class HttpRequestBuilder
/// 键
/// 值
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的查询参数。默认值为 false
+ /// 是否忽略空值。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的查询参数。默认值为 false。
- /// 是否忽略空值。默认值为 false。
///
///
///
- public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false,
- CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false,
- bool ignoreNullValues = false)
+ public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, bool replace = false,
+ bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
- return WithQueryParameters(new Dictionary { { key, value } }, escape, culture, comparer,
- replace, ignoreNullValues);
+ return WithQueryParameters(new Dictionary { { key, value } }, escape, replace,
+ ignoreNullValues, culture);
}
///
@@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder
/// 支持多次调用。
/// 查询参数集合
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的查询参数。默认值为 false
+ /// 是否忽略空值。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的查询参数。默认值为 false。
- /// 是否忽略空值。默认值为 false。
///
///
///
public HttpRequestBuilder WithQueryParameters(IDictionary parameters, bool escape = false,
- CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false,
- bool ignoreNullValues = false)
+ bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameters);
// 初始化查询参数
- QueryParameters ??= new Dictionary>(comparer);
- var objectQueryParameters = new Dictionary>(comparer);
+ QueryParameters ??= new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ var objectQueryParameters = new Dictionary>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加
objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -629,7 +650,7 @@ public sealed partial class HttpRequestBuilder
QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
- comparer);
+ StringComparer.OrdinalIgnoreCase);
return this;
}
@@ -641,20 +662,16 @@ public sealed partial class HttpRequestBuilder
/// 查询参数集合
/// 参数前缀。对于对象类型可生成如 prefix.Name=furion 与 prefix.Age=30 参数格式。
/// 是否转义字符串,默认 false
+ /// 是否替换已存在的查询参数。默认值为 false
+ /// 是否忽略空值。默认值为 false
///
///
///
- ///
- ///
- ///
- /// 是否替换已存在的查询参数。默认值为 false。
- /// 是否忽略空值。默认值为 false。
///
///
///
public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false,
- CultureInfo? culture = null, IEqualityComparer? comparer = null, bool replace = false,
- bool ignoreNullValues = false)
+ bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameterSource);
@@ -663,7 +680,7 @@ public sealed partial class HttpRequestBuilder
parameterSource.ObjectToDictionary()!.ToDictionary(
u =>
$"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}",
- u => u.Value), escape, culture, comparer, replace, ignoreNullValues);
+ u => u.Value), escape, replace, ignoreNullValues, culture);
}
///
@@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false,
- CultureInfo? culture = null, IEqualityComparer? comparer = null)
+ CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
- return WithPathParameters(new Dictionary { { key, value } }, escape, culture, comparer);
+ return WithPathParameters(new Dictionary { { key, value } }, escape, culture);
}
///
@@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
- public HttpRequestBuilder WithPathParameters(IDictionary parameters,
- bool escape = false,
- CultureInfo? culture = null,
- IEqualityComparer? comparer = null)
+ public HttpRequestBuilder WithPathParameters(IDictionary parameters, bool escape = false,
+ CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(parameters);
- PathParameters ??= new Dictionary(comparer);
+ PathParameters ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加
PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
- comparer));
+ StringComparer.OrdinalIgnoreCase));
return this;
}
@@ -767,15 +776,11 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false,
- CultureInfo? culture = null,
- IEqualityComparer? comparer = null)
+ CultureInfo? culture = null)
{
// 检查是否设置了模板字符串前缀
if (string.IsNullOrWhiteSpace(prefix))
@@ -786,7 +791,7 @@ public sealed partial class HttpRequestBuilder
return WithPathParameters(
parameterSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape,
- culture, comparer);
+ culture);
}
ObjectPathParameters ??= new Dictionary();
@@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
- public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null,
- IEqualityComparer? comparer = null)
+ public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key);
- return WithCookies(new Dictionary { { key, value } }, escape, culture, comparer);
+ return WithCookies(new Dictionary { { key, value } }, escape, culture);
}
///
@@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
- public HttpRequestBuilder WithCookies(IDictionary cookies,
- bool escape = false,
- CultureInfo? culture = null,
- IEqualityComparer? comparer = null)
+ public HttpRequestBuilder WithCookies(IDictionary cookies, bool escape = false,
+ CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(cookies);
- Cookies ??= new Dictionary(comparer);
+ Cookies ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加
Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
- comparer));
+ StringComparer.OrdinalIgnoreCase));
return this;
}
@@ -880,15 +876,10 @@ public sealed partial class HttpRequestBuilder
///
///
///
- ///
- ///
- ///
///
///
///
- public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false,
- CultureInfo? culture = null,
- IEqualityComparer? comparer = null)
+ public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, CultureInfo? culture = null)
{
// 空检查
ArgumentNullException.ThrowIfNull(cookieSource);
@@ -896,8 +887,7 @@ public sealed partial class HttpRequestBuilder
// 存在则更新否则添加
return WithCookies(
cookieSource.ObjectToDictionary()!.ToDictionary(
- u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture,
- comparer);
+ u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture);
}
///
@@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder
return this;
}
+ ///
+ /// 设置身份验证凭据请求授权标头
+ ///
+ /// 身份验证的方案
+ /// 身份验证的凭证
+ ///
+ ///
+ ///
+ public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) =>
+ AddAuthentication(new AuthenticationHeaderValue(scheme, parameter));
+
///
/// 设置身份验证凭据请求授权标头
///
@@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder
ReleaseDisposables();
}
+ ///
+ /// 设置请求来源地址
+ ///
+ /// 设置此配置后,将在单次请求标头中添加 Referer 标头。
+ /// 请求来源地址,当设置为 "{BASE_ADDRESS}" 时将替换为基地址
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetReferer(string? referer) =>
+ WithHeader(HeaderNames.Referer, referer, replace: true);
+
///
/// 设置模拟浏览器环境
///
@@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder WithAnyStatusCodeHandler(Func handler) =>
WithStatusCodeHandler(["*"], handler);
+ ///
+ /// 添加请求成功(200-299)状态码处理程序
+ ///
+ /// 自定义处理程序
+ ///
+ ///
+ ///
+ public HttpRequestBuilder
+ WithSuccessStatusCodeHandler(Func handler) =>
+ WithStatusCodeHandler("200-299", handler);
+
///
/// 添加状态码处理程序
///
@@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder
? null
: new Uri(baseAddress, UriKind.RelativeOrAbsolute));
+ ///
+ /// 设置 HTTP 版本
+ ///
+ /// 版本号
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetVersion(string? version) =>
+ SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version));
+
+ ///
+ /// 设置 HTTP 版本
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetVersion(Version? version)
+ {
+ Version = version;
+
+ return this;
+ }
+
+ ///
+ /// 设置异常抑制
+ ///
+ /// 抑制所有异常。重复调用仅最后一次调用生效。
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true);
+
+ ///
+ /// 设置异常抑制
+ ///
+ /// 重复调用仅最后一次调用生效。
+ /// 是否启用异常抑制。当设置为 false 时,将禁用异常抑制机制。
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []);
+
+ ///
+ /// 设置是否移除默认的内容的 Content-Type
+ ///
+ /// 如果为 true 则移除,默认为 false
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SetOmitContentType(bool omit)
+ {
+ OmitContentType = omit;
+
+ return this;
+ }
+
+ ///
+ /// 设置异常抑制
+ ///
+ /// 重复调用仅最后一次调用生效。
+ /// 异常抑制类型集合
+ ///
+ ///
+ ///
+ ///
+ public HttpRequestBuilder SuppressExceptions(Type[] exceptionTypes)
+ {
+ // 空检查
+ ArgumentNullException.ThrowIfNull(exceptionTypes);
+
+ // 检查是否包含 null 或者不是 Exception 类型的元素
+ if (exceptionTypes.Any(u => (Type?)u is null || !typeof(Exception).IsAssignableFrom(u)))
+ {
+ throw new ArgumentException(
+ "All elements in exceptionTypes must be non-null and assignable to System.Exception.");
+ }
+
+ // 释放引用(无关紧要)
+ SuppressExceptionTypes = null;
+
+ // 空检查
+ if (exceptionTypes.Length == 0)
+ {
+ return this;
+ }
+
+ // 确保每次都能覆盖
+ SuppressExceptionTypes = [];
+
+ // 遍历异常抑制类型集合逐条追加
+ foreach (var exceptionType in exceptionTypes)
+ {
+ SuppressExceptionTypes.Add(exceptionType);
+ }
+
+ return this;
+ }
+
///
/// 释放可释放的对象集合
///
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs
index 20cab95c7..8a2919ff1 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.Properties.cs
@@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder
///
public Uri? BaseAddress { get; private set; }
+ ///
+ /// HTTP 版本
+ ///
+ public Version? Version { get; private set; }
+
///
/// 实例提供器
///
@@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder
///
/// 用于处理在设置 的请求消息的内容时的操作
///
- public Action? OnPreSetContent { get; private set; }
+ public Action? OnPreSetContent { get; private set; }
///
/// 用于处理在发送 HTTP 请求之前的操作
@@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder
///
///
///
- internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
+ public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
+
+ ///
+ /// 是否移除默认的内容的 Content-Type
+ ///
+ /// 默认值为:false。
+ public bool OmitContentType { get; private set; }
///
/// 如果 HTTP 响应的 IsSuccessStatusCode 属性是 false,则引发异常。
@@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder
get;
private set;
}
+
+ ///
+ /// 异常抑制类型集合
+ ///
+ /// 当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。
+ internal HashSet? SuppressExceptionTypes { get; private set; }
+
+ ///
+ /// 超时发生时要执行的操作
+ ///
+ internal Action? TimeoutAction { get; private set; }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs
index 4f4dd1d0d..8f34b034e 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.StaticMethods.cs
@@ -10,6 +10,9 @@
// ------------------------------------------------------------------------
using System.Reflection;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
namespace ThingsGateway.HttpRemote;
@@ -614,4 +617,116 @@ public sealed partial class HttpRequestBuilder
///
public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) =>
new(method, args);
+
+ ///
+ /// 从 JSON 中创建 实例
+ ///
+ /// JSON 字符串
+ /// 自定义配置委托
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static HttpRequestBuilder FromJson(string json, Action? configure = null)
+ {
+ // 空检查
+ ArgumentException.ThrowIfNullOrWhiteSpace(json);
+
+ /*
+ * 手动解析 JSON 字符串
+ *
+ * 不采用 JSON 反序列化的原因如下:
+ * 1. HttpRequestBuilder 的属性设计为只读,无法直接通过反序列化赋值。
+ * 2. 避免引入 [JsonInclude] 特性对 System.Text.Json 的强耦合,保持依赖解耦。
+ * 3. 简化 JSON 字符串的结构定义,无需严格遵循 HttpRequestBuilder 的属性定义,从而省略 [JsonPropertyName] 等自定义映射。
+ * 4. 精确控制需要解析的键,减少不必要的自定义 JsonConverter 操作,提升性能与可维护性。
+ */
+ var jsonObject = JsonNode.Parse(json, new JsonNodeOptions { PropertyNameCaseInsensitive = true },
+ new JsonDocumentOptions { AllowTrailingCommas = true })?.AsObject();
+
+ // 空检查
+ ArgumentNullException.ThrowIfNull(jsonObject);
+
+ // 验证必填字段
+ if (!jsonObject.TryGetPropertyValue("method", out var methodNode) || methodNode is not JsonValue methodValue)
+ {
+ throw new ArgumentException("Missing required `method` in JSON.");
+ }
+
+ // 允许 "url" 为 null,但必须定义
+ if (!jsonObject.ContainsKey("url"))
+ {
+ throw new ArgumentException("Missing required `url` in JSON.");
+ }
+
+ // 初始化 HttpRequestBuilder 实例
+ var httpRequestBuilder = Create(methodValue.ToString(), jsonObject["url"]?.GetValue());
+
+ // 处理可选字段
+ HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue()));
+ HandleJsonNode(jsonObject, "headers", node => httpRequestBuilder.WithHeaders(node));
+ HandleJsonNode(jsonObject, "queries", node => httpRequestBuilder.WithQueryParameters(node));
+ HandleJsonNode(jsonObject, "cookies", node => httpRequestBuilder.WithCookies(node));
+ HandleJsonNode(jsonObject, "timeout", node => httpRequestBuilder.SetTimeout(node.GetValue()));
+ HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue()));
+ HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue()));
+
+ // 处理请求内容
+ if (jsonObject.TryGetPropertyValue("data", out var dataNode))
+ {
+ // "data" 和 "contentType" 必须同时存在或同时不存在
+ if (!jsonObject.TryGetPropertyValue("contentType", out var contentTypeNode) ||
+ contentTypeNode is not JsonValue contentTypeValue)
+ {
+ throw new InvalidOperationException("The `contentType` key is required when `data` is present.");
+ }
+
+ // 设置请求内容
+ httpRequestBuilder
+ .SetContent(
+ dataNode?.ToJsonString(new JsonSerializerOptions
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ }), contentTypeValue.ToString()).AddStringContentForFormUrlEncodedContentProcessor();
+
+ // 设置内容编码
+ HandleJsonNode(jsonObject, "encoding",
+ node => httpRequestBuilder.SetContentEncoding(node.GetValue()));
+ }
+
+ // 处理多部分表单
+ if (jsonObject.TryGetPropertyValue("multipart", out var multipartNode))
+ {
+ // 设置多部分表单内容
+ httpRequestBuilder.SetMultipartContent(multipart => multipart.AddJson(multipartNode?.AsObject()
+ .ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })));
+ }
+
+ // 调用自定义配置委托
+ configure?.Invoke(httpRequestBuilder);
+
+ return httpRequestBuilder;
+ }
+
+ ///
+ /// 处理
+ ///
+ ///
+ ///
+ ///
+ /// 属性名
+ /// 自定义操作
+ internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action action)
+ {
+ // 空检查
+ ArgumentNullException.ThrowIfNull(jsonObject);
+ ArgumentNullException.ThrowIfNull(propertyName);
+ ArgumentNullException.ThrowIfNull(action);
+
+ if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null)
+ {
+ action(node);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs
index e89ec2807..43137c58c 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Builders/HttpRequestBuilder.cs
@@ -76,6 +76,12 @@ public sealed partial class HttpRequestBuilder
// 初始化 HttpRequestMessage 实例
var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri);
+ // 设置 HTTP 版本
+ if (Version is not null)
+ {
+ httpRequestMessage.Version = Version;
+ }
+
// 启用性能优化
EnablePerformanceOptimization(httpRequestMessage);
@@ -160,18 +166,44 @@ public sealed partial class HttpRequestBuilder
///
internal void AppendPathSegments(UriBuilder uriBuilder)
{
+ // 空检查
+ if ((PathSegments == null || PathSegments.Count == 0) &&
+ (PathSegmentsToRemove == null || PathSegmentsToRemove.Count == 0))
+ {
+ return;
+ }
+
+ // 记录原路径是否以斜杠结尾(修复核心逻辑)
+ var originalPath = uriBuilder.Uri.AbsolutePath;
+ var endsWithSlash = originalPath.Length > 1 && originalPath.EndsWith('/');
+
// 解析 URL 中的路径片段列表
- var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Concat([]);
+ var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
- // 追加路径片段
- pathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([]).Where(u => !string.IsNullOrWhiteSpace(u))
- .Select(u => u.TrimStart('/').TrimEnd('/')));
+ // 追加并处理新路径片段
+ var newPathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([])
+ .Where(u => !string.IsNullOrWhiteSpace(u)).Select(u => u.TrimStart('/').TrimEnd('/')));
- // 构建路径片段赋值给 UriBuilder 的 Path 属性
- uriBuilder.Path = '/' + string.Join('/',
- // 过滤已标记为移除的路径片段
- pathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
- u => PathSegmentsToRemove?.TryGetValue(u, out _) == false));
+ // 过滤需要移除的路径片段
+ var filteredSegments = newPathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
+ u => PathSegmentsToRemove?.Contains(u) == false).ToArray();
+
+ // 构建最终路径
+ if (filteredSegments.Length != 0)
+ {
+ uriBuilder.Path = $"/{string.Join('/', filteredSegments)}";
+
+ // 恢复原路径的结尾斜杠(当存在路径片段时)
+ if (endsWithSlash)
+ {
+ uriBuilder.Path += "/";
+ }
+ }
+ // 没有路径片段时设置为根路径
+ else
+ {
+ uriBuilder.Path = "/";
+ }
}
///
@@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder
///
internal void AppendQueryParameters(UriBuilder uriBuilder)
{
+ // 空检查
+ if ((QueryParameters is null || QueryParameters.Count == 0) &&
+ (QueryParametersToRemove is null || QueryParametersToRemove.Count == 0))
+ {
+ return;
+ }
+
// 解析 URL 中的查询字符串为键值对列表
var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?');
@@ -300,6 +339,16 @@ public sealed partial class HttpRequestBuilder
// 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中
foreach (var (key, values) in Headers)
{
+ // 替换 Referer 标头的 "{BASE_ADDRESS}" 模板字符串
+ if (key.IsIn([HeaderNames.Referer], StringComparer.OrdinalIgnoreCase) &&
+ values.FirstOrDefault() == Constants.REFERER_HEADER_BASE_ADDRESS_TEMPLATE)
+ {
+ httpRequestMessage.Headers.Referrer = new Uri(
+ $"{httpRequestMessage.RequestUri?.Scheme}://{httpRequestMessage.RequestUri?.Host}{(httpRequestMessage.RequestUri?.IsDefaultPort != true ? $":{httpRequestMessage.RequestUri?.Port}" : string.Empty)}",
+ UriKind.RelativeOrAbsolute);
+ continue;
+ }
+
httpRequestMessage.Headers.TryAddWithoutValidation(key, values);
}
}
@@ -486,6 +535,18 @@ public sealed partial class HttpRequestBuilder
// 构建 HttpContent 实例
var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors);
+ // 空检查
+ if (httpContent is null)
+ {
+ return;
+ }
+
+ // 检查是否移除默认的内容的 Content-Type,解决对接 Java 程序时可能出现失败问题
+ if (OmitContentType)
+ {
+ httpContent.Headers.ContentType = null;
+ }
+
// 调用用于处理在设置请求消息的内容时的操作
OnPreSetContent?.Invoke(httpContent);
@@ -513,6 +574,9 @@ public sealed partial class HttpRequestBuilder
{
httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE");
}
+
+ // 添加 HttpClient 实例的配置名称
+ httpRequestMessage.Options.AddOrUpdate(Constants.HTTP_CLIENT_NAME, HttpClientName ?? string.Empty);
}
///
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs
index c0adc6def..30317cc35 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Constants/Constants.cs
@@ -88,15 +88,26 @@ internal static class Constants
/// 被用于从 的 Options 属性中读取。
internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__";
+ ///
+ /// HTTP 请求 实例的配置名称键
+ ///
+ /// 被用于从 的 Options 属性中读取。
+ internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__";
+
///
/// 浏览器的 User-Agent 标头值
///
internal const string USER_AGENT_OF_BROWSER =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0";
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0";
///
/// 移动端浏览器的 User-Agent 标头值
///
internal const string USER_AGENT_OF_MOBILE_BROWSER =
- "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36 Edg/133.0.0.0";
+ "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 Edg/135.0.0.0";
+
+ ///
+ /// Referer 标头请求基地址模板
+ ///
+ internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}";
}
\ No newline at end of file
diff --git a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs
index 3984238a5..0c1871d1d 100644
--- a/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs
+++ b/src/Admin/ThingsGateway.Furion/V5_Experience/HttpRemote/Converters/ObjectContentConverter.cs
@@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Net.Http.Json;
+using System.Text.Json;
namespace ThingsGateway.HttpRemote;
@@ -27,16 +28,45 @@ public class ObjectContentConverter : IHttpContentConverter
///
public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) =>
- httpResponseMessage.Content.ReadFromJsonAsync(resultType,
- ServiceProvider?.GetRequiredService>().Value.JsonSerializerOptions ??
- HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult();
+ httpResponseMessage.Content
+ .ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken)
+ .GetAwaiter().GetResult();
///
public virtual async Task