Compare commits
	
		
			50 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					45a8c91a5a | ||
| 
						 | 
					8e938f18be | ||
| 
						 | 
					ab1b364c54 | ||
| 
						 | 
					5ec65b2fb0 | ||
| 
						 | 
					926eced724 | ||
| 
						 | 
					f7f8802272 | ||
| 
						 | 
					c6910dff02 | ||
| 
						 | 
					ad299d0dbb | ||
| 
						 | 
					8b124d1050 | ||
| 
						 | 
					ff41080dbd | ||
| 
						 | 
					0e28606e3d | ||
| 
						 | 
					6a025ceee5 | ||
| 
						 | 
					6b2e53d6dc | ||
| 
						 | 
					b989aa5561 | ||
| 
						 | 
					f5b0b7ebd2 | ||
| 
						 | 
					16881ae076 | ||
| 
						 | 
					af04112656 | ||
| 
						 | 
					a2863112dc | ||
| 
						 | 
					f531e4dfc5 | ||
| 
						 | 
					8db9b32ba7 | ||
| 
						 | 
					dd5691cbef | ||
| 
						 | 
					de48b32af3 | ||
| 
						 | 
					600b5042a1 | ||
| 
						 | 
					aac77029da | ||
| 
						 | 
					e50205f557 | ||
| 
						 | 
					e227411d1f | ||
| 
						 | 
					2de0ed793f | ||
| 
						 | 
					cb0276f273 | ||
| 
						 | 
					562b3f17c9 | ||
| 
						 | 
					0f78f81c1c | ||
| 
						 | 
					6594937d0a | ||
| 
						 | 
					64bc6084be | ||
| 
						 | 
					20434d5bd2 | ||
| 
						 | 
					9ecc9380e6 | ||
| 
						 | 
					a57c55080b | ||
| 
						 | 
					b8ca06c6be | ||
| 
						 | 
					5aff6461a1 | ||
| 
						 | 
					6cf53fefec | ||
| 
						 | 
					45132f3503 | ||
| 
						 | 
					f1e78a0e8a | ||
| 
						 | 
					0bf28ec275 | ||
| 
						 | 
					41f8412c97 | ||
| 
						 | 
					c535974362 | ||
| 
						 | 
					1860c5f215 | ||
| 
						 | 
					6d778b2d39 | ||
| 
						 | 
					f48b99c259 | ||
| 
						 | 
					3c73b93051 | ||
| 
						 | 
					98f3f2d519 | ||
| 
						 | 
					b76b4e8d68 | ||
| 
						 | 
					07285a7c61 | 
@@ -64,24 +64,31 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
 | 
			
		||||
    public override void OnException(MethodContext context)
 | 
			
		||||
    {
 | 
			
		||||
        //插入异常日志
 | 
			
		||||
        SysOperateLog log = GetOperLog(LocalizerType, context);
 | 
			
		||||
        if (App.HttpContext.Request.Path.StartsWithSegments("/_blazor"))
 | 
			
		||||
        {
 | 
			
		||||
            //插入异常日志
 | 
			
		||||
            SysOperateLog log = GetOperLog(LocalizerType, context);
 | 
			
		||||
 | 
			
		||||
        log.Category = LogCateGoryEnum.Exception;//操作类型为异常
 | 
			
		||||
        log.ExeStatus = false;//操作状态为失败
 | 
			
		||||
        if (context.Exception is AppFriendlyException exception)
 | 
			
		||||
            log.ExeMessage = exception?.Message;
 | 
			
		||||
        else
 | 
			
		||||
            log.ExeMessage = context.Exception?.ToString();
 | 
			
		||||
            log.Category = LogCateGoryEnum.Exception;//操作类型为异常
 | 
			
		||||
            log.ExeStatus = false;//操作状态为失败
 | 
			
		||||
            if (context.Exception is AppFriendlyException exception)
 | 
			
		||||
                log.ExeMessage = exception?.Message;
 | 
			
		||||
            else
 | 
			
		||||
                log.ExeMessage = context.Exception?.ToString();
 | 
			
		||||
 | 
			
		||||
        OperDescAttribute.WriteToQueue(log);
 | 
			
		||||
            OperDescAttribute.WriteToQueue(log);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void OnSuccess(MethodContext context)
 | 
			
		||||
    {
 | 
			
		||||
        //插入操作日志
 | 
			
		||||
        SysOperateLog log = GetOperLog(LocalizerType, context);
 | 
			
		||||
        OperDescAttribute.WriteToQueue(log);
 | 
			
		||||
        if (App.HttpContext.Request.Path.StartsWithSegments("/_blazor"))
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            //插入操作日志
 | 
			
		||||
            SysOperateLog log = GetOperLog(LocalizerType, context);
 | 
			
		||||
            OperDescAttribute.WriteToQueue(log);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -115,7 +122,7 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
    private SysOperateLog GetOperLog(Type? localizerType, MethodContext context)
 | 
			
		||||
    {
 | 
			
		||||
        var methodBase = context.Method;
 | 
			
		||||
        var clientInfo = AppService.ClientInfo;
 | 
			
		||||
        var userAgent = AppService.UserAgent;
 | 
			
		||||
        string? paramJson = null;
 | 
			
		||||
        if (IsRecordPar)
 | 
			
		||||
        {
 | 
			
		||||
@@ -127,10 +134,10 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
            {
 | 
			
		||||
                parametersDict[parametersInfo[i].Name!] = args[i];
 | 
			
		||||
            }
 | 
			
		||||
            paramJson = parametersDict.ToJsonNetString();
 | 
			
		||||
            paramJson = parametersDict.ToSystemTextJsonString();
 | 
			
		||||
        }
 | 
			
		||||
        var result = context.ReturnValue;
 | 
			
		||||
        var resultJson = IsRecordPar ? result?.ToJsonNetString() : null;
 | 
			
		||||
        var resultJson = IsRecordPar ? result?.ToSystemTextJsonString() : null;
 | 
			
		||||
        //操作日志表实体
 | 
			
		||||
        var log = new SysOperateLog
 | 
			
		||||
        {
 | 
			
		||||
@@ -138,8 +145,8 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
            Category = LogCateGoryEnum.Operate,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = AppService?.RemoteIpAddress ?? string.Empty,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = DateTime.Now,
 | 
			
		||||
            OpAccount = UserManager.UserAccount,
 | 
			
		||||
            ReqUrl = null,
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
[ApiDescriptionSettings(false)]
 | 
			
		||||
[Route("api/auth")]
 | 
			
		||||
[LoggingMonitor]
 | 
			
		||||
[RequestAudit]
 | 
			
		||||
public class AuthController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private readonly IAuthService _authService;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
[Description("登录")]
 | 
			
		||||
[Route("openapi/auth")]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
[LoggingMonitor]
 | 
			
		||||
[RequestAudit]
 | 
			
		||||
[ApiController]
 | 
			
		||||
public class OpenApiController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace System.Logging;
 | 
			
		||||
 | 
			
		||||
public class RequestAudit
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
// 版权信息
 | 
			
		||||
// 版权归百小僧及百签科技(广东)有限公司所有。
 | 
			
		||||
// 所有权利保留。
 | 
			
		||||
// 官方网站:https://baiqian.com
 | 
			
		||||
//
 | 
			
		||||
// 许可证信息
 | 
			
		||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
 | 
			
		||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
namespace System;
 | 
			
		||||
 | 
			
		||||
[SuppressSniffer, AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
 | 
			
		||||
public sealed class RequestAuditAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class RequestAuditData
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分类
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string CateGory { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 客户端信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public UserAgent Client { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 请求方法:POST/GET
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Method { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 操作名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Operation { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 请求地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 方法名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ActionName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 认证信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<AuthorizationClaims> AuthorizationClaims { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 控制器名
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ControllerName { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 异常信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public LogException Exception { get; set; }
 | 
			
		||||
 | 
			
		||||
    public long TimeOperationElapsedMilliseconds { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 服务端
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string LocalIPv4 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 日志时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTimeOffset LogDateTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 参数列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<Parameters> Parameters { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 客户端IPV4地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string RemoteIPv4 { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 请求地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string RequestUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 返回信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public object ReturnInformation { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 验证错误信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Validation Validation { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,301 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.Controllers;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.Filters;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.FriendlyException;
 | 
			
		||||
using ThingsGateway.Logging;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
using ThingsGateway.UnifyResult;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class RequestAuditFilter : IAsyncActionFilter, IOrderedFilter
 | 
			
		||||
{
 | 
			
		||||
    private const int FilterOrder = -3000;
 | 
			
		||||
    public int Order => FilterOrder;
 | 
			
		||||
 | 
			
		||||
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
 | 
			
		||||
    {
 | 
			
		||||
        var timeOperation = Stopwatch.StartNew();
 | 
			
		||||
        var resultContext = await next().ConfigureAwait(false);
 | 
			
		||||
        // 计算接口执行时间
 | 
			
		||||
        timeOperation.Stop();
 | 
			
		||||
 | 
			
		||||
        var controllerActionDescriptor = (context.ActionDescriptor as ControllerActionDescriptor);
 | 
			
		||||
        // 获取动作方法描述器
 | 
			
		||||
        var actionMethod = controllerActionDescriptor?.MethodInfo;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 处理 Blazor Server
 | 
			
		||||
        if (actionMethod == null)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 排除 WebSocket 请求处理
 | 
			
		||||
        if (context.HttpContext.IsWebSocketRequest())
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果贴了 [SuppressMonitor] 特性则跳过
 | 
			
		||||
        if (actionMethod.IsDefined(typeof(SuppressRequestAuditAttribute), true)
 | 
			
		||||
            || actionMethod.DeclaringType.IsDefined(typeof(SuppressRequestAuditAttribute), true))
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 只有方法贴有特性才进行审计
 | 
			
		||||
        if (
 | 
			
		||||
            !actionMethod.DeclaringType.IsDefined(typeof(RequestAuditAttribute), true)
 | 
			
		||||
            &&
 | 
			
		||||
            !actionMethod.IsDefined(typeof(RequestAuditAttribute), true))
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var logData = new RequestAuditData();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        logData.TimeOperationElapsedMilliseconds = timeOperation.ElapsedMilliseconds;
 | 
			
		||||
 | 
			
		||||
        var resultHttpContext = (resultContext as FilterContext).HttpContext;
 | 
			
		||||
 | 
			
		||||
        // 获取 HttpContext 和 HttpRequest 对象
 | 
			
		||||
        var httpContext = context.HttpContext;
 | 
			
		||||
        var httpRequest = httpContext.Request;
 | 
			
		||||
 | 
			
		||||
        // 获取客户端 Ipv4 地址
 | 
			
		||||
        var remoteIPv4 = httpContext.GetRemoteIpAddressToIPv4();
 | 
			
		||||
        logData.RemoteIPv4 = remoteIPv4;
 | 
			
		||||
        var requestUrl = Uri.UnescapeDataString(httpRequest.GetRequestUrlAddress());
 | 
			
		||||
        logData.RequestUrl = requestUrl;
 | 
			
		||||
 | 
			
		||||
        object returnValue = null;
 | 
			
		||||
        Type finalReturnType;
 | 
			
		||||
        var result = resultContext.Result as IActionResult;
 | 
			
		||||
        // 解析返回值
 | 
			
		||||
        if (UnifyContext.CheckVaildResult(result, out var data))
 | 
			
		||||
        {
 | 
			
		||||
            returnValue = data;
 | 
			
		||||
            finalReturnType = data?.GetType();
 | 
			
		||||
        }
 | 
			
		||||
        // 处理文件类型
 | 
			
		||||
        else if (result is FileResult fresult)
 | 
			
		||||
        {
 | 
			
		||||
            returnValue = new
 | 
			
		||||
            {
 | 
			
		||||
                FileName = fresult.FileDownloadName,
 | 
			
		||||
                fresult.ContentType,
 | 
			
		||||
                Length = fresult is FileContentResult cresult ? (object)cresult.FileContents.Length : null
 | 
			
		||||
            };
 | 
			
		||||
            finalReturnType = fresult?.GetType();
 | 
			
		||||
        }
 | 
			
		||||
        else finalReturnType = result?.GetType();
 | 
			
		||||
 | 
			
		||||
        logData.ReturnInformation = returnValue;
 | 
			
		||||
 | 
			
		||||
        //获取客户端信息
 | 
			
		||||
        var client = App.GetService<IAppService>().UserAgent;
 | 
			
		||||
        //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
 | 
			
		||||
        var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
 | 
			
		||||
 | 
			
		||||
        var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[actionMethod.Name];
 | 
			
		||||
        //获取特性
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        logData.CateGory = desc.Value;//传操作名称
 | 
			
		||||
        logData.Operation = desc.Value;//传操作名称
 | 
			
		||||
        logData.Client = client;
 | 
			
		||||
        logData.Path = httpContext.Request.Path.Value;//请求地址
 | 
			
		||||
        logData.Method = httpContext.Request.Method;//请求方法
 | 
			
		||||
 | 
			
		||||
        logData.ControllerName = controllerActionDescriptor.ControllerName;
 | 
			
		||||
        logData.ActionName = controllerActionDescriptor.ActionName;
 | 
			
		||||
 | 
			
		||||
        logData.AuthorizationClaims = new();
 | 
			
		||||
        // 获取授权用户
 | 
			
		||||
        var user = httpContext.User;
 | 
			
		||||
        foreach (var claim in user.Claims)
 | 
			
		||||
        {
 | 
			
		||||
            logData.AuthorizationClaims.Add(new AuthorizationClaims
 | 
			
		||||
            {
 | 
			
		||||
                Type = claim.Type,
 | 
			
		||||
                Value = claim.Value,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        logData.LocalIPv4 = httpContext.GetLocalIpAddressToIPv4();
 | 
			
		||||
        logData.LogDateTime = DateTimeOffset.Now;
 | 
			
		||||
        var parameterValues = context.ActionArguments;
 | 
			
		||||
 | 
			
		||||
        logData.Parameters = new();
 | 
			
		||||
        var parameters = actionMethod.GetParameters();
 | 
			
		||||
 | 
			
		||||
        foreach (var parameter in parameters)
 | 
			
		||||
        {
 | 
			
		||||
            // 判断是否禁用记录特定参数
 | 
			
		||||
            if (parameter.IsDefined(typeof(SuppressRequestAuditAttribute), false)) continue;
 | 
			
		||||
 | 
			
		||||
            // 排除标记 [FromServices] 的解析
 | 
			
		||||
            if (parameter.IsDefined(typeof(FromServicesAttribute), false)) continue;
 | 
			
		||||
 | 
			
		||||
            var name = parameter.Name;
 | 
			
		||||
            var parameterType = parameter.ParameterType;
 | 
			
		||||
 | 
			
		||||
            _ = parameterValues.TryGetValue(name, out var value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var par = new Parameters()
 | 
			
		||||
            {
 | 
			
		||||
                Name = name,
 | 
			
		||||
            };
 | 
			
		||||
            logData.Parameters.Add(par);
 | 
			
		||||
 | 
			
		||||
            object rawValue = default;
 | 
			
		||||
 | 
			
		||||
            // 文件类型参数
 | 
			
		||||
            if (value is IFormFile || value is List<IFormFile>)
 | 
			
		||||
            {
 | 
			
		||||
                // 单文件
 | 
			
		||||
                if (value is IFormFile formFile)
 | 
			
		||||
                {
 | 
			
		||||
                    var fileSize = Math.Round(formFile.Length / 1024D);
 | 
			
		||||
                    rawValue = new
 | 
			
		||||
                    {
 | 
			
		||||
                        name = formFile.Name,
 | 
			
		||||
                        fileName = formFile.FileName,
 | 
			
		||||
                        length = formFile.Length,
 | 
			
		||||
                        contentType = formFile.ContentType
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                // 多文件
 | 
			
		||||
                else if (value is List<IFormFile> formFiles)
 | 
			
		||||
                {
 | 
			
		||||
                    var rawValues1 = new List<object>();
 | 
			
		||||
                    for (var i = 0; i < formFiles.Count; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var file = formFiles[i];
 | 
			
		||||
                        var size = Math.Round(file.Length / 1024D);
 | 
			
		||||
                        var rawValue1 = new
 | 
			
		||||
                        {
 | 
			
		||||
                            name = file.Name,
 | 
			
		||||
                            fileName = file.FileName,
 | 
			
		||||
                            length = file.Length,
 | 
			
		||||
                            contentType = file.ContentType
 | 
			
		||||
                        };
 | 
			
		||||
                        rawValues1.Add(rawValue1);
 | 
			
		||||
                    }
 | 
			
		||||
                    rawValue = rawValues1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // 处理 byte[] 参数类型
 | 
			
		||||
            else if (value is byte[] byteArray)
 | 
			
		||||
            {
 | 
			
		||||
                rawValue = new
 | 
			
		||||
                {
 | 
			
		||||
                    length = byteArray.Length,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            // 处理基元类型,字符串类型和空值
 | 
			
		||||
            else if (parameterType.IsPrimitive || value is string || value == null)
 | 
			
		||||
            {
 | 
			
		||||
                rawValue = value;
 | 
			
		||||
            }
 | 
			
		||||
            // 其他类型统一进行序列化
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                rawValue = value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            par.Value = rawValue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 获取异常对象情况
 | 
			
		||||
        Exception exception = resultContext.Exception;
 | 
			
		||||
        if (exception is AppFriendlyException friendlyException)
 | 
			
		||||
        {
 | 
			
		||||
            logData.Validation = new();
 | 
			
		||||
            logData.Validation.Message = friendlyException.Message;
 | 
			
		||||
        }
 | 
			
		||||
        else if (exception != null)
 | 
			
		||||
        {
 | 
			
		||||
            logData.Exception = new();
 | 
			
		||||
            logData.Exception.Message = exception.Message;
 | 
			
		||||
            logData.Exception.StackTrace = exception.StackTrace;
 | 
			
		||||
            logData.Exception.Type = HandleGenericType(exception.GetType());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 创建日志记录器
 | 
			
		||||
        var logger = httpContext.RequestServices.GetRequiredService<ILogger<RequestAudit>>();
 | 
			
		||||
 | 
			
		||||
        var logContext = new LogContext();
 | 
			
		||||
 | 
			
		||||
        logContext.Set(nameof(RequestAuditData), logData);
 | 
			
		||||
 | 
			
		||||
        // 设置日志上下文
 | 
			
		||||
        using var scope = logger.ScopeContext(logContext);
 | 
			
		||||
 | 
			
		||||
        if (exception == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Log(LogLevel.Information, $"{logData.Method}:{logData.Path}-{logData.Operation}");
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception.ToSystemTextJsonString()}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 处理泛型类型转字符串打印问题
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="type"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private static string HandleGenericType(Type type)
 | 
			
		||||
    {
 | 
			
		||||
        if (type == null) return string.Empty;
 | 
			
		||||
 | 
			
		||||
        var typeName = type.FullName ?? (!string.IsNullOrEmpty(type.Namespace) ? type.Namespace + "." : string.Empty) + type.Name;
 | 
			
		||||
 | 
			
		||||
        // 处理泛型类型问题
 | 
			
		||||
        if (type.IsConstructedGenericType)
 | 
			
		||||
        {
 | 
			
		||||
            var prefix = type.GetGenericArguments()
 | 
			
		||||
                .Select(genericArg => HandleGenericType(genericArg))
 | 
			
		||||
                .Aggregate((previous, current) => previous + ", " + current);
 | 
			
		||||
 | 
			
		||||
            typeName = typeName.Split('`').First() + "<" + prefix + ">";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return typeName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
// 版权信息
 | 
			
		||||
// 版权归百小僧及百签科技(广东)有限公司所有。
 | 
			
		||||
// 所有权利保留。
 | 
			
		||||
// 官方网站:https://baiqian.com
 | 
			
		||||
//
 | 
			
		||||
// 许可证信息
 | 
			
		||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
 | 
			
		||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
namespace System;
 | 
			
		||||
 | 
			
		||||
[SuppressSniffer, AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
 | 
			
		||||
public sealed class SuppressRequestAuditAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -51,7 +51,7 @@ public class HardwareInfo
 | 
			
		||||
    /// 进程占用内存
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public string WorkingSet { get; set; }
 | 
			
		||||
    public int WorkingSet { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 更新时间
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
using ThingsGateway.NewLife.Threading;
 | 
			
		||||
using ThingsGateway.Schedule;
 | 
			
		||||
 | 
			
		||||
@@ -51,11 +52,20 @@ public class HardwareJob : IJob, IHardwareJob
 | 
			
		||||
 | 
			
		||||
    #endregion 属性
 | 
			
		||||
 | 
			
		||||
    private MemoryCache MemoryCache = new() { };
 | 
			
		||||
    private const string CacheKey = "HistoryHardwareInfo";
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
 | 
			
		||||
    {
 | 
			
		||||
        using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
 | 
			
		||||
        return await db.Queryable<HistoryHardwareInfo>().ToListAsync().ConfigureAwait(false);
 | 
			
		||||
        var historyHardwareInfos = MemoryCache.Get<List<HistoryHardwareInfo>>(CacheKey);
 | 
			
		||||
        if (historyHardwareInfos == null)
 | 
			
		||||
        {
 | 
			
		||||
            using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
 | 
			
		||||
            historyHardwareInfos = await db.Queryable<HistoryHardwareInfo>().Where(a => a.Date > DateTime.Now.AddDays(-3)).ToListAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            MemoryCache.Set(CacheKey, historyHardwareInfos);
 | 
			
		||||
        }
 | 
			
		||||
        return historyHardwareInfos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private bool error = false;
 | 
			
		||||
@@ -94,7 +104,7 @@ public class HardwareJob : IJob, IHardwareJob
 | 
			
		||||
            {
 | 
			
		||||
                HardwareInfo.MachineInfo.Refresh();
 | 
			
		||||
                HardwareInfo.UpdateTime = TimerX.Now.ToDefaultDateTimeFormat();
 | 
			
		||||
                HardwareInfo.WorkingSet = (Environment.WorkingSet / 1024.0 / 1024.0).ToString("F2");
 | 
			
		||||
                HardwareInfo.WorkingSet = (Environment.WorkingSet / 1024.0 / 1024.0).ToInt();
 | 
			
		||||
                error = false;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
@@ -116,17 +126,22 @@ public class HardwareJob : IJob, IHardwareJob
 | 
			
		||||
                            var his = new HistoryHardwareInfo()
 | 
			
		||||
                            {
 | 
			
		||||
                                Date = TimerX.Now,
 | 
			
		||||
                                DriveUsage = (100 - (HardwareInfo.DriveInfo.TotalFreeSpace * 100.00 / HardwareInfo.DriveInfo.TotalSize)).ToString("F2"),
 | 
			
		||||
                                Battery = (HardwareInfo.MachineInfo.Battery * 100).ToString("F2"),
 | 
			
		||||
                                DriveUsage = (100 - (HardwareInfo.DriveInfo.TotalFreeSpace * 100.00 / HardwareInfo.DriveInfo.TotalSize)).ToInt(),
 | 
			
		||||
                                Battery = (HardwareInfo.MachineInfo.Battery * 100).ToInt(),
 | 
			
		||||
                                MemoryUsage = (HardwareInfo.WorkingSet),
 | 
			
		||||
                                CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToString("F2"),
 | 
			
		||||
                                Temperature = (HardwareInfo.MachineInfo.Temperature).ToString("F2"),
 | 
			
		||||
                                CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToInt(),
 | 
			
		||||
                                Temperature = (HardwareInfo.MachineInfo.Temperature).ToInt(),
 | 
			
		||||
                            };
 | 
			
		||||
                            await db.Insertable(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                            MemoryCache.Remove(CacheKey);
 | 
			
		||||
                        }
 | 
			
		||||
                        var sevenDaysAgo = TimerX.Now.AddDays(-HardwareInfoOptions.DaysAgo);
 | 
			
		||||
                        //删除特定信息
 | 
			
		||||
                        await db.Deleteable<HistoryHardwareInfo>(a => a.Date <= sevenDaysAgo).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                        var result = await db.Deleteable<HistoryHardwareInfo>(a => a.Date <= sevenDaysAgo).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                        if (result > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            MemoryCache.Remove(CacheKey);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                error = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,23 +19,23 @@ public class HistoryHardwareInfo
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "磁盘使用率")]
 | 
			
		||||
    public string DriveUsage { get; set; }
 | 
			
		||||
    public int DriveUsage { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "内存")]
 | 
			
		||||
    public string MemoryUsage { get; set; }
 | 
			
		||||
    public int MemoryUsage { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "CPU使用率")]
 | 
			
		||||
    public string CpuUsage { get; set; }
 | 
			
		||||
    public int CpuUsage { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "温度")]
 | 
			
		||||
    public string Temperature { get; set; }
 | 
			
		||||
    public int Temperature { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "电池")]
 | 
			
		||||
    public string Battery { get; set; }
 | 
			
		||||
    public int Battery { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "时间")]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "User expired, please login again"
 | 
			
		||||
  },
 | 
			
		||||
@@ -24,9 +35,6 @@
 | 
			
		||||
    "LatestLoginTime": "LatestLoginTime",
 | 
			
		||||
    "LatestLoginDevice": "LatestLoginDevice",
 | 
			
		||||
    "LatestLoginAddress": "LatestLoginAddress",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "OrgNames": "OrgNames",
 | 
			
		||||
    "PositionName": "PositionName",
 | 
			
		||||
    "OrgId": "Org",
 | 
			
		||||
@@ -60,9 +68,6 @@
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Name.Required": "{0} is required",
 | 
			
		||||
    "Category": "Category",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "OrgId": "Org",
 | 
			
		||||
    "Global": "Global",
 | 
			
		||||
    "Status": "Status",
 | 
			
		||||
@@ -105,9 +110,6 @@
 | 
			
		||||
    "Category": "Category",
 | 
			
		||||
    "Target": "Target",
 | 
			
		||||
    "NavLinkMatch": "NavLinkMatch",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "ParentId": "Parent",
 | 
			
		||||
    "ResourceDup": "Duplicate name {0} exists",
 | 
			
		||||
    "ResourceParentChoiceSelf": "Parent cannot choose itself",
 | 
			
		||||
@@ -134,9 +136,6 @@
 | 
			
		||||
    "Status": "Status",
 | 
			
		||||
    "OrgId": "Organization",
 | 
			
		||||
    "Remark": "Remarks",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "Dup": "Duplicate position exists with Category {0} and Name {1}",
 | 
			
		||||
    "CodeDup": "Duplicate code {0} exists",
 | 
			
		||||
    "NameDup": "Duplicate name {0} exists",
 | 
			
		||||
@@ -159,9 +158,6 @@
 | 
			
		||||
    "Names": "Names",
 | 
			
		||||
    "Remark": "Remarks",
 | 
			
		||||
    "DirectorId": "Director",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "Dup": "Duplicate organization exists with Category {0} and Name {1}",
 | 
			
		||||
    "CodeDup": "Duplicate code {0} exists",
 | 
			
		||||
    "NameDup": "Duplicate name {0} exists",
 | 
			
		||||
@@ -358,9 +354,6 @@
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Code": "Code",
 | 
			
		||||
    "Remark": "Remark",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "DemoCanotUpdateWebsitePolicy": "DEMO environment does not allow modifying website settings",
 | 
			
		||||
    "DictDup": "Duplicate configuration exists, category {0}, name {1}"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,469 +1,462 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
      "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
    },
 | 
			
		||||
    "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysUser": {
 | 
			
		||||
      "Disable": "禁用",
 | 
			
		||||
      "Enable": "启用",
 | 
			
		||||
      "GrantRole": "分配角色",
 | 
			
		||||
      "ExitVerificat": "您已被强制下线",
 | 
			
		||||
      "PasswordEdited": "密码被修改,已退出登录",
 | 
			
		||||
      "Avatar": "头像",
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "Account.Required": " {0} 是必填项",
 | 
			
		||||
      "Password": "密码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "Phone": "手机",
 | 
			
		||||
      "Email": "邮箱",
 | 
			
		||||
      "LastLoginIp": "上次登录ip",
 | 
			
		||||
      "LastLoginDevice": "上次登录设备",
 | 
			
		||||
      "LastLoginTime": "上次登录时间",
 | 
			
		||||
      "LastLoginAddress": "上次登录地点",
 | 
			
		||||
      "LatestLoginIp": "最新登录ip",
 | 
			
		||||
      "LatestLoginTime": "最新登录时间",
 | 
			
		||||
      "LatestLoginDevice": "最新登录设备",
 | 
			
		||||
      "LatestLoginAddress": "最新登录地点",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "OrgNames": "机构名称",
 | 
			
		||||
      "PositionName": "职位名称",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "PositionId": "职位",
 | 
			
		||||
      "DirectorId": "主管",
 | 
			
		||||
      "CheckSelf": "禁止 {0} 自己",
 | 
			
		||||
      "CanotDeleteAdminUser": "不可删除系统内置超管用户",
 | 
			
		||||
      "CanotEditAdminUser": "不可编辑超管用户",
 | 
			
		||||
      "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
      "EmailDup": "存在重复的邮箱 {0}",
 | 
			
		||||
      "AccountDup": "存在重复的账号 {0}",
 | 
			
		||||
      "CanotDeleteSelf": "不可删除自己",
 | 
			
		||||
      "EmailError": "邮箱 {0} 格式错误",
 | 
			
		||||
      "PhoneError": "手机号码 {0} 格式错误",
 | 
			
		||||
      "NoOrg": "组织机构不存在",
 | 
			
		||||
      "DirectorSelf": "不能设置自己为主管",
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysUser": {
 | 
			
		||||
    "Disable": "禁用",
 | 
			
		||||
    "Enable": "启用",
 | 
			
		||||
    "GrantRole": "分配角色",
 | 
			
		||||
    "ExitVerificat": "您已被强制下线",
 | 
			
		||||
    "PasswordEdited": "密码被修改,已退出登录",
 | 
			
		||||
    "Avatar": "头像",
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "Account.Required": " {0} 是必填项",
 | 
			
		||||
    "Password": "密码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "Phone": "手机",
 | 
			
		||||
    "Email": "邮箱",
 | 
			
		||||
    "LastLoginIp": "上次登录ip",
 | 
			
		||||
    "LastLoginDevice": "上次登录设备",
 | 
			
		||||
    "LastLoginTime": "上次登录时间",
 | 
			
		||||
    "LastLoginAddress": "上次登录地点",
 | 
			
		||||
    "LatestLoginIp": "最新登录ip",
 | 
			
		||||
    "LatestLoginTime": "最新登录时间",
 | 
			
		||||
    "LatestLoginDevice": "最新登录设备",
 | 
			
		||||
    "LatestLoginAddress": "最新登录地点",
 | 
			
		||||
    "OrgNames": "机构名称",
 | 
			
		||||
    "PositionName": "职位名称",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
    "PositionId": "职位",
 | 
			
		||||
    "DirectorId": "主管",
 | 
			
		||||
    "CheckSelf": "禁止 {0} 自己",
 | 
			
		||||
    "CanotDeleteAdminUser": "不可删除系统内置超管用户",
 | 
			
		||||
    "CanotEditAdminUser": "不可编辑超管用户",
 | 
			
		||||
    "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
    "EmailDup": "存在重复的邮箱 {0}",
 | 
			
		||||
    "AccountDup": "存在重复的账号 {0}",
 | 
			
		||||
    "CanotDeleteSelf": "不可删除自己",
 | 
			
		||||
    "EmailError": "邮箱 {0} 格式错误",
 | 
			
		||||
    "PhoneError": "手机号码 {0} 格式错误",
 | 
			
		||||
    "NoOrg": "组织机构不存在",
 | 
			
		||||
    "DirectorSelf": "不能设置自己为主管",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      "DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
 | 
			
		||||
      "OldPasswordError": "原密码错误",
 | 
			
		||||
      "ConfirmPasswordDiff": "两次输入的密码不一致",
 | 
			
		||||
      "PasswordLengthLess": "密码长度不能小于 {0} ",
 | 
			
		||||
      "PasswordMustNum ": "密码必须包含数字",
 | 
			
		||||
      "PasswordMustLow": "密码必须包含小写字母",
 | 
			
		||||
      "PasswordMustUpp": "密码必须包含大写字母",
 | 
			
		||||
      "PasswordMustSpecial": "密码必须包含特殊字符"
 | 
			
		||||
    },
 | 
			
		||||
    "DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
 | 
			
		||||
    "OldPasswordError": "原密码错误",
 | 
			
		||||
    "ConfirmPasswordDiff": "两次输入的密码不一致",
 | 
			
		||||
    "PasswordLengthLess": "密码长度不能小于 {0} ",
 | 
			
		||||
    "PasswordMustNum ": "密码必须包含数字",
 | 
			
		||||
    "PasswordMustLow": "密码必须包含小写字母",
 | 
			
		||||
    "PasswordMustUpp": "密码必须包含大写字母",
 | 
			
		||||
    "PasswordMustSpecial": "密码必须包含特殊字符"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysRole": {
 | 
			
		||||
      "Code": "编码",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Name.Required": " {0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "Global": "全局",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysRole": {
 | 
			
		||||
    "Code": "编码",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Global": "全局",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
 | 
			
		||||
      "CanotDeleteAdmin": "不可删除系统内置超管角色",
 | 
			
		||||
      "CanotEditAdmin": "不可编辑超管角色",
 | 
			
		||||
      "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
    "CanotDeleteAdmin": "不可删除系统内置超管角色",
 | 
			
		||||
    "CanotEditAdmin": "不可编辑超管角色",
 | 
			
		||||
    "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
 | 
			
		||||
      "NameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
      "OrgNotNull": "机构不能为空",
 | 
			
		||||
      "SameOrgNameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
      "CannotRoleScopeAll": "机构角色不能选择全局数据范围",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.RoleCategoryEnum": {
 | 
			
		||||
      "Global": "全局",
 | 
			
		||||
      "Org": "机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DataScopeEnum": {
 | 
			
		||||
      "SCOPE_SELF": "仅自己",
 | 
			
		||||
      "SCOPE_ALL": "全部",
 | 
			
		||||
      "SCOPE_ORG": "仅所属组织",
 | 
			
		||||
      "SCOPE_ORG_CHILD": "所属组织及以下",
 | 
			
		||||
      "SCOPE_ORG_DEFINE": "自定义"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DefaultDataScope": {
 | 
			
		||||
      "ScopeCategory": "数据范围",
 | 
			
		||||
      "ScopeDefineOrgIdList": "自定义列表"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysResource": {
 | 
			
		||||
      "Title": "标题",
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Title.Required": "{0} 是必填项",
 | 
			
		||||
      "Href.Required": "{0} 是必填项",
 | 
			
		||||
      "Icon": "图标",
 | 
			
		||||
      "Href": "路径",
 | 
			
		||||
      "Code": "编码",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Target": "跳转类型",
 | 
			
		||||
      "NavLinkMatch": "匹配类型",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "ParentId": "上级菜单",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "ResourceDup": "存在重复的名称 {0}",
 | 
			
		||||
      "ResourceParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ResourceParentNull": "父级不存在 {0}",
 | 
			
		||||
      "NotFoundResource": "系统异常,没找到该菜单",
 | 
			
		||||
      "ModuleIdDiff": "模块与上级菜单不一致",
 | 
			
		||||
      "CanotDeleteSystemResource": "不可删除系统资源 {0}",
 | 
			
		||||
      "ResourceMenuHrefNotNull": "菜单的路径不能为空"
 | 
			
		||||
    },
 | 
			
		||||
    "NameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
    "OrgNotNull": "机构不能为空",
 | 
			
		||||
    "SameOrgNameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
    "CannotRoleScopeAll": "机构角色不能选择全局数据范围",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.RoleCategoryEnum": {
 | 
			
		||||
    "Global": "全局",
 | 
			
		||||
    "Org": "机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DataScopeEnum": {
 | 
			
		||||
    "SCOPE_SELF": "仅自己",
 | 
			
		||||
    "SCOPE_ALL": "全部",
 | 
			
		||||
    "SCOPE_ORG": "仅所属组织",
 | 
			
		||||
    "SCOPE_ORG_CHILD": "所属组织及以下",
 | 
			
		||||
    "SCOPE_ORG_DEFINE": "自定义"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DefaultDataScope": {
 | 
			
		||||
    "ScopeCategory": "数据范围",
 | 
			
		||||
    "ScopeDefineOrgIdList": "自定义列表"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysResource": {
 | 
			
		||||
    "Title": "标题",
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Title.Required": "{0} 是必填项",
 | 
			
		||||
    "Href.Required": "{0} 是必填项",
 | 
			
		||||
    "Icon": "图标",
 | 
			
		||||
    "Href": "路径",
 | 
			
		||||
    "Code": "编码",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Target": "跳转类型",
 | 
			
		||||
    "NavLinkMatch": "匹配类型",
 | 
			
		||||
    "ParentId": "上级菜单",
 | 
			
		||||
    "ResourceDup": "存在重复的名称 {0}",
 | 
			
		||||
    "ResourceParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ResourceParentNull": "父级不存在 {0}",
 | 
			
		||||
    "NotFoundResource": "系统异常,没找到该菜单",
 | 
			
		||||
    "ModuleIdDiff": "模块与上级菜单不一致",
 | 
			
		||||
    "CanotDeleteSystemResource": "不可删除系统资源 {0}",
 | 
			
		||||
    "ResourceMenuHrefNotNull": "菜单的路径不能为空"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOrgCopyInput": {
 | 
			
		||||
      "TargetId": "目标机构",
 | 
			
		||||
      "ContainsChild": "包含下级",
 | 
			
		||||
      "ContainsPosition": "包含职位"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysPosition": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "OrgId.MinValue": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
      "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
      "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
      "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
      "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ParentNull": "父级不存在 {0}",
 | 
			
		||||
      "DeleteUserFirst": "请先删除职位下的用户"
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOrgCopyInput": {
 | 
			
		||||
    "TargetId": "目标机构",
 | 
			
		||||
    "ContainsChild": "包含下级",
 | 
			
		||||
    "ContainsPosition": "包含职位"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysPosition": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "OrgId.MinValue": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
    "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
    "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
    "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
    "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ParentNull": "父级不存在 {0}",
 | 
			
		||||
    "DeleteUserFirst": "请先删除职位下的用户"
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOrg": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "ParentId": "上级机构",
 | 
			
		||||
      "Names": "机构全称",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "DirectorId": "主管",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "Dup": "存在重复的机构 分类 {0} 名称 {1}",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
      "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
      "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
      "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
      "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ParentNull": "父级不存在 {0}",
 | 
			
		||||
      "DeleteUserFirst": "请先删除机构下的用户",
 | 
			
		||||
      "DeleteRoleFirst": "请先删除机构下的角色",
 | 
			
		||||
      "DeletePositionFirst": "请先删除机构下的职位",
 | 
			
		||||
      "RootOrg": "无法创建顶层机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OrgEnum": {
 | 
			
		||||
      "COMPANY": "公司",
 | 
			
		||||
      "DEPT": "部门"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PositionCategoryEnum": {
 | 
			
		||||
      "HIGH": "高层",
 | 
			
		||||
      "MIDDLE": "中层",
 | 
			
		||||
      "LOW": "低层"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOrg": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "ParentId": "上级机构",
 | 
			
		||||
    "Names": "机构全称",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "DirectorId": "主管",
 | 
			
		||||
    "Dup": "存在重复的机构 分类 {0} 名称 {1}",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
    "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
    "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
    "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
    "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ParentNull": "父级不存在 {0}",
 | 
			
		||||
    "DeleteUserFirst": "请先删除机构下的用户",
 | 
			
		||||
    "DeleteRoleFirst": "请先删除机构下的角色",
 | 
			
		||||
    "DeletePositionFirst": "请先删除机构下的职位",
 | 
			
		||||
    "RootOrg": "无法创建顶层机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OrgEnum": {
 | 
			
		||||
    "COMPANY": "公司",
 | 
			
		||||
    "DEPT": "部门"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PositionCategoryEnum": {
 | 
			
		||||
    "HIGH": "高层",
 | 
			
		||||
    "MIDDLE": "中层",
 | 
			
		||||
    "LOW": "低层"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //controller
 | 
			
		||||
    "ThingsGateway.Admin.Application.AuthController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "AuthController": "登录API",
 | 
			
		||||
      "LoginAsync": "登录",
 | 
			
		||||
      "LogoutAsync": "注销"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.TestController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "TestController": "测试API",
 | 
			
		||||
      "Test": "测试"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OpenApiAuthController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "OpenApiAuthController": "登录API",
 | 
			
		||||
      "LoginAsync": "登录",
 | 
			
		||||
      "LogoutAsync": "注销"
 | 
			
		||||
    },
 | 
			
		||||
  //controller
 | 
			
		||||
  "ThingsGateway.Admin.Application.AuthController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "AuthController": "登录API",
 | 
			
		||||
    "LoginAsync": "登录",
 | 
			
		||||
    "LogoutAsync": "注销"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.TestController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "TestController": "测试API",
 | 
			
		||||
    "Test": "测试"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OpenApiAuthController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "OpenApiAuthController": "登录API",
 | 
			
		||||
    "LoginAsync": "登录",
 | 
			
		||||
    "LogoutAsync": "注销"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.FileService": {
 | 
			
		||||
      "FileNullError": "文件不能为空",
 | 
			
		||||
      "FileLengthError": "文件大小不允许超过 {0} M",
 | 
			
		||||
      "FileTypeError": "不支持 {0} 格式"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.FileService": {
 | 
			
		||||
    "FileNullError": "文件不能为空",
 | 
			
		||||
    "FileLengthError": "文件大小不允许超过 {0} M",
 | 
			
		||||
    "FileTypeError": "不支持 {0} 格式"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.UnifyResultProvider": {
 | 
			
		||||
      "TokenOver": "登录已过期,请重新登录",
 | 
			
		||||
      "NoPermission": "禁止访问,没有权限"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.UnifyResultProvider": {
 | 
			
		||||
    "TokenOver": "登录已过期,请重新登录",
 | 
			
		||||
    "NoPermission": "禁止访问,没有权限"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.AuthService": {
 | 
			
		||||
      "TenantNull": "租户不存在",
 | 
			
		||||
      "OrgDisable": "所属公司/部门已停用,请联系管理员",
 | 
			
		||||
      "SingleLoginWarn": "您的账号已在别处登录",
 | 
			
		||||
      "UserNull": "用户 {0} 不存在",
 | 
			
		||||
      "PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
 | 
			
		||||
      "AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
 | 
			
		||||
      "UserDisable": "账号 {0} 已停用",
 | 
			
		||||
      "MustDesc": "密码需要DESC加密后传入",
 | 
			
		||||
      "UserNoModule": "该账号未分配模块,请联系管理员"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.AuthService": {
 | 
			
		||||
    "TenantNull": "租户不存在",
 | 
			
		||||
    "OrgDisable": "所属公司/部门已停用,请联系管理员",
 | 
			
		||||
    "SingleLoginWarn": "您的账号已在别处登录",
 | 
			
		||||
    "UserNull": "用户 {0} 不存在",
 | 
			
		||||
    "PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
 | 
			
		||||
    "AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
 | 
			
		||||
    "UserDisable": "账号 {0} 已停用",
 | 
			
		||||
    "MustDesc": "密码需要DESC加密后传入",
 | 
			
		||||
    "UserNoModule": "该账号未分配模块,请联系管理员"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.HardwareInfo": {
 | 
			
		||||
      "Environment": "主机环境",
 | 
			
		||||
      "FrameworkDescription": "NET框架",
 | 
			
		||||
      "OsArchitecture": "系统架构",
 | 
			
		||||
      "UUID": "唯一编码",
 | 
			
		||||
      "UpdateTime": "更新时间"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.HistoryHardwareInfo": {
 | 
			
		||||
      "DriveUsage": "磁盘使用率",
 | 
			
		||||
      "MemoryUsage": "内存",
 | 
			
		||||
      "CpuUsage": "CPU使用率",
 | 
			
		||||
      "Temperature": "温度",
 | 
			
		||||
      "Battery": "电池"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HardwareInfo": {
 | 
			
		||||
    "Environment": "主机环境",
 | 
			
		||||
    "FrameworkDescription": "NET框架",
 | 
			
		||||
    "OsArchitecture": "系统架构",
 | 
			
		||||
    "UUID": "唯一编码",
 | 
			
		||||
    "UpdateTime": "更新时间"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HistoryHardwareInfo": {
 | 
			
		||||
    "DriveUsage": "磁盘使用率",
 | 
			
		||||
    "MemoryUsage": "内存",
 | 
			
		||||
    "CpuUsage": "CPU使用率",
 | 
			
		||||
    "Temperature": "温度",
 | 
			
		||||
    "Battery": "电池"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //oper
 | 
			
		||||
    "ThingsGateway.Admin.Application.OperDescAttribute": {
 | 
			
		||||
      //dict
 | 
			
		||||
      "SaveDict": "修改字典",
 | 
			
		||||
      "DeleteDict": "删除字典",
 | 
			
		||||
      "EditLoginPolicy": "修改登录策略",
 | 
			
		||||
      "EditPasswordPolicy": "修改密码策略",
 | 
			
		||||
      "EditPagePolicy": "修改页面策略",
 | 
			
		||||
      "EditWebsitePolicy": "修改网站设置",
 | 
			
		||||
      //operlog
 | 
			
		||||
      "DeleteOperLog": "删除操作日志",
 | 
			
		||||
      "ExportOperLog": "导出操作日志",
 | 
			
		||||
  //oper
 | 
			
		||||
  "ThingsGateway.Admin.Application.OperDescAttribute": {
 | 
			
		||||
    //dict
 | 
			
		||||
    "SaveDict": "修改字典",
 | 
			
		||||
    "DeleteDict": "删除字典",
 | 
			
		||||
    "EditLoginPolicy": "修改登录策略",
 | 
			
		||||
    "EditPasswordPolicy": "修改密码策略",
 | 
			
		||||
    "EditPagePolicy": "修改页面策略",
 | 
			
		||||
    "EditWebsitePolicy": "修改网站设置",
 | 
			
		||||
    //operlog
 | 
			
		||||
    "DeleteOperLog": "删除操作日志",
 | 
			
		||||
    "ExportOperLog": "导出操作日志",
 | 
			
		||||
 | 
			
		||||
      //resource
 | 
			
		||||
      "SaveResource": "修改资源",
 | 
			
		||||
      "DeleteResource": "删除资源",
 | 
			
		||||
    //resource
 | 
			
		||||
    "SaveResource": "修改资源",
 | 
			
		||||
    "DeleteResource": "删除资源",
 | 
			
		||||
 | 
			
		||||
      //role
 | 
			
		||||
      "SaveRole": "修改角色",
 | 
			
		||||
      "DeleteRole": "删除角色",
 | 
			
		||||
      "RoleGrantResource": "角色授权资源",
 | 
			
		||||
      "RoleGrantUser": "角色授权用户",
 | 
			
		||||
      "RoleGrantApiPermission": "角色授权OpenApi",
 | 
			
		||||
      "GrantApi": "API",
 | 
			
		||||
      "GrantUser": "用户",
 | 
			
		||||
      "GrantRole": "角色",
 | 
			
		||||
      "GrantResource": "资源",
 | 
			
		||||
      //user
 | 
			
		||||
      "SaveUser": "修改用户",
 | 
			
		||||
      "DeleteuSER": "删除用户",
 | 
			
		||||
      "ResetPassword": "重置密码",
 | 
			
		||||
      "UserGrantRole": "用户授权角色",
 | 
			
		||||
      "UserGrantResource": "用户授权资源",
 | 
			
		||||
      "UserGrantApiPermission": "用户授权OpenApi",
 | 
			
		||||
    //role
 | 
			
		||||
    "SaveRole": "修改角色",
 | 
			
		||||
    "DeleteRole": "删除角色",
 | 
			
		||||
    "RoleGrantResource": "角色授权资源",
 | 
			
		||||
    "RoleGrantUser": "角色授权用户",
 | 
			
		||||
    "RoleGrantApiPermission": "角色授权OpenApi",
 | 
			
		||||
    "GrantApi": "API",
 | 
			
		||||
    "GrantUser": "用户",
 | 
			
		||||
    "GrantRole": "角色",
 | 
			
		||||
    "GrantResource": "资源",
 | 
			
		||||
    //user
 | 
			
		||||
    "SaveUser": "修改用户",
 | 
			
		||||
    "DeleteuSER": "删除用户",
 | 
			
		||||
    "ResetPassword": "重置密码",
 | 
			
		||||
    "UserGrantRole": "用户授权角色",
 | 
			
		||||
    "UserGrantResource": "用户授权资源",
 | 
			
		||||
    "UserGrantApiPermission": "用户授权OpenApi",
 | 
			
		||||
 | 
			
		||||
      //usercenter
 | 
			
		||||
      "UpdateUserInfo": "更新个人信息",
 | 
			
		||||
      "WorkbenchInfo": "更新个人工作台",
 | 
			
		||||
      "UpdatePassword": "更新个人密码",
 | 
			
		||||
    //usercenter
 | 
			
		||||
    "UpdateUserInfo": "更新个人信息",
 | 
			
		||||
    "WorkbenchInfo": "更新个人工作台",
 | 
			
		||||
    "UpdatePassword": "更新个人密码",
 | 
			
		||||
 | 
			
		||||
      //session
 | 
			
		||||
      "ExitVerificat": "强退令牌",
 | 
			
		||||
      "ExitSession": "强退会话",
 | 
			
		||||
    //session
 | 
			
		||||
    "ExitVerificat": "强退令牌",
 | 
			
		||||
    "ExitSession": "强退会话",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      "CopyOrg": "复制机构",
 | 
			
		||||
      "DeleteOrg": "删除机构",
 | 
			
		||||
      "SaveOrg": "保存机构",
 | 
			
		||||
    "CopyOrg": "复制机构",
 | 
			
		||||
    "DeleteOrg": "删除机构",
 | 
			
		||||
    "SaveOrg": "保存机构",
 | 
			
		||||
 | 
			
		||||
      "DeletePosition": "删除岗位",
 | 
			
		||||
      "SavePosition": "保存岗位",
 | 
			
		||||
    "DeletePosition": "删除岗位",
 | 
			
		||||
    "SavePosition": "保存岗位",
 | 
			
		||||
 | 
			
		||||
      "NoPermission": "无权限操作",
 | 
			
		||||
    "NoPermission": "无权限操作",
 | 
			
		||||
 | 
			
		||||
      "CopyResource": "复制资源",
 | 
			
		||||
      "ChangeParentResource": "更改父节点"
 | 
			
		||||
    },
 | 
			
		||||
    "CopyResource": "复制资源",
 | 
			
		||||
    "ChangeParentResource": "更改父节点"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //service
 | 
			
		||||
  //service
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.HardwareJob": {
 | 
			
		||||
      "GetHardwareInfoFail": "获取硬件信息出错"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HardwareJob": {
 | 
			
		||||
    "GetHardwareInfoFail": "获取硬件信息出错"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //dto
 | 
			
		||||
    "ThingsGateway.Admin.Application.UserSelectorOutput": {
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "OrgId": "机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.ResourceTableSearchModel": {
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Href": "路径",
 | 
			
		||||
      "Title": "标题"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.WorkbenchInfo": {
 | 
			
		||||
      "Razor": "主页",
 | 
			
		||||
      "Shortcuts": "快捷方式"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.UpdatePasswordInput": {
 | 
			
		||||
      "Password": "密码",
 | 
			
		||||
      "NewPassword": "新密码",
 | 
			
		||||
      "ConfirmPassword": "确认密码",
 | 
			
		||||
      "Password.Required": " {0} 是必填项",
 | 
			
		||||
      "NewPassword.Required": " {0} 是必填项",
 | 
			
		||||
      "ConfirmPassword.Required": " {0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
  //dto
 | 
			
		||||
  "ThingsGateway.Admin.Application.UserSelectorOutput": {
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "OrgId": "机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.ResourceTableSearchModel": {
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Href": "路径",
 | 
			
		||||
    "Title": "标题"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.WorkbenchInfo": {
 | 
			
		||||
    "Razor": "主页",
 | 
			
		||||
    "Shortcuts": "快捷方式"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.UpdatePasswordInput": {
 | 
			
		||||
    "Password": "密码",
 | 
			
		||||
    "NewPassword": "新密码",
 | 
			
		||||
    "ConfirmPassword": "确认密码",
 | 
			
		||||
    "Password.Required": " {0} 是必填项",
 | 
			
		||||
    "NewPassword.Required": " {0} 是必填项",
 | 
			
		||||
    "ConfirmPassword.Required": " {0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.VerificatInfo": {
 | 
			
		||||
      "Expire": "过期时间(分)",
 | 
			
		||||
      "Online": "在线状态",
 | 
			
		||||
      "VerificatTimeout": "超时时间",
 | 
			
		||||
      "Device": "登录设备",
 | 
			
		||||
      "LoginIp": "登录IP",
 | 
			
		||||
      "LoginTime": "登录时间"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.VerificatInfo": {
 | 
			
		||||
    "Expire": "过期时间(分)",
 | 
			
		||||
    "Online": "在线状态",
 | 
			
		||||
    "VerificatTimeout": "超时时间",
 | 
			
		||||
    "Device": "登录设备",
 | 
			
		||||
    "LoginIp": "登录IP",
 | 
			
		||||
    "LoginTime": "登录时间"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SessionOutput": {
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "Online": "在线状态",
 | 
			
		||||
      "LatestLoginIp": "最新登录ip",
 | 
			
		||||
      "LatestLoginTime": "最新登录时间",
 | 
			
		||||
      "VerificatCount": "令牌数量"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SessionOutput": {
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "Online": "在线状态",
 | 
			
		||||
    "LatestLoginIp": "最新登录ip",
 | 
			
		||||
    "LatestLoginTime": "最新登录时间",
 | 
			
		||||
    "VerificatCount": "令牌数量"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysDict": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
 | 
			
		||||
      "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysDict": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
 | 
			
		||||
    "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOperateLog": {
 | 
			
		||||
      "ClassName": "类名",
 | 
			
		||||
      "ExeMessage": "具体消息",
 | 
			
		||||
      "MethodName": "方法名称",
 | 
			
		||||
      "ParamJson": "请求参数",
 | 
			
		||||
      "ReqMethod": "请求方式",
 | 
			
		||||
      "ReqUrl": "请求地址",
 | 
			
		||||
      "ResultJson": "返回结果",
 | 
			
		||||
      "Category": "日志分类",
 | 
			
		||||
      "ExeStatus": "执行状态",
 | 
			
		||||
      "Name": "日志名称",
 | 
			
		||||
      "OpAccount": "账号",
 | 
			
		||||
      "OpBrowser": "浏览器",
 | 
			
		||||
      "OpIp": "ip",
 | 
			
		||||
      "OpOs": "系统",
 | 
			
		||||
      "OpTime": "操作时间",
 | 
			
		||||
      "VerificatId": "验证Id"
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOperateLog": {
 | 
			
		||||
    "ClassName": "类名",
 | 
			
		||||
    "ExeMessage": "具体消息",
 | 
			
		||||
    "MethodName": "方法名称",
 | 
			
		||||
    "ParamJson": "请求参数",
 | 
			
		||||
    "ReqMethod": "请求方式",
 | 
			
		||||
    "ReqUrl": "请求地址",
 | 
			
		||||
    "ResultJson": "返回结果",
 | 
			
		||||
    "Category": "日志分类",
 | 
			
		||||
    "ExeStatus": "执行状态",
 | 
			
		||||
    "Name": "日志名称",
 | 
			
		||||
    "OpAccount": "账号",
 | 
			
		||||
    "OpBrowser": "浏览器",
 | 
			
		||||
    "OpIp": "ip",
 | 
			
		||||
    "OpOs": "系统",
 | 
			
		||||
    "OpTime": "操作时间",
 | 
			
		||||
    "VerificatId": "验证Id"
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OperateLogPageInput": {
 | 
			
		||||
      "SearchDate": "时间范围",
 | 
			
		||||
      "Account": "操作账号",
 | 
			
		||||
      "Category": "分类"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LoginInput": {
 | 
			
		||||
      "Account": "登录账号",
 | 
			
		||||
      "Password": "登录密码",
 | 
			
		||||
      "Account.Required": "{0} 是必填项",
 | 
			
		||||
      "Password.Required": "{0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogoutInput": {
 | 
			
		||||
      "VerificatId.Required": "{0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.AppConfig": {
 | 
			
		||||
      "LoginPolicy": "登录策略",
 | 
			
		||||
      "PasswordPolicy": "密码策略",
 | 
			
		||||
      "PagePolicy": "页面设置",
 | 
			
		||||
      "WebsitePolicy": "网站设置"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LoginPolicy": {
 | 
			
		||||
      "SingleOpen": "单用户登录开关",
 | 
			
		||||
      "ErrorLockTime": "登录错误锁定时长(分)",
 | 
			
		||||
      "ErrorResetTime": "登录错误次数过期时长(分)",
 | 
			
		||||
      "ErrorCount": "登录错误次数锁定阈值",
 | 
			
		||||
      "VerificatExpireTime": "登录过期时间(分)",
 | 
			
		||||
      "ErrorLockTime.MinValue": " {0} 值太小",
 | 
			
		||||
      "ErrorResetTime.MinValue": " {0} 值太小",
 | 
			
		||||
      "ErrorCount.MinValue": " {0} 值太小",
 | 
			
		||||
      "VerificatExpireTime.MinValue": " {0} 值太小"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PagePolicy": {
 | 
			
		||||
      "Shortcuts": "默认快捷方式",
 | 
			
		||||
      "Razor": "默认主页"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PasswordPolicy": {
 | 
			
		||||
      "DefaultPassword": "默认用户密码",
 | 
			
		||||
      "DefaultPassword.Required": " {0} 是必填项",
 | 
			
		||||
      "PasswordMinLen": "密码最小长度",
 | 
			
		||||
      "PasswordMinLen.MinValue": " {0} 值太小",
 | 
			
		||||
      "PasswordContainNum": "包含数字",
 | 
			
		||||
      "PasswordContainLower": "包含小写字母",
 | 
			
		||||
      "PasswordContainUpper": "包含大写字母",
 | 
			
		||||
      "PasswordContainChar": "包含特殊字符"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.WebsitePolicy": {
 | 
			
		||||
      "WebStatus": "是否开放",
 | 
			
		||||
      "CloseTip": "关闭提示",
 | 
			
		||||
      "CloseTip.Required": " {0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OperateLogPageInput": {
 | 
			
		||||
    "SearchDate": "时间范围",
 | 
			
		||||
    "Account": "操作账号",
 | 
			
		||||
    "Category": "分类"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LoginInput": {
 | 
			
		||||
    "Account": "登录账号",
 | 
			
		||||
    "Password": "登录密码",
 | 
			
		||||
    "Account.Required": "{0} 是必填项",
 | 
			
		||||
    "Password.Required": "{0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogoutInput": {
 | 
			
		||||
    "VerificatId.Required": "{0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.AppConfig": {
 | 
			
		||||
    "LoginPolicy": "登录策略",
 | 
			
		||||
    "PasswordPolicy": "密码策略",
 | 
			
		||||
    "PagePolicy": "页面设置",
 | 
			
		||||
    "WebsitePolicy": "网站设置"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LoginPolicy": {
 | 
			
		||||
    "SingleOpen": "单用户登录开关",
 | 
			
		||||
    "ErrorLockTime": "登录错误锁定时长(分)",
 | 
			
		||||
    "ErrorResetTime": "登录错误次数过期时长(分)",
 | 
			
		||||
    "ErrorCount": "登录错误次数锁定阈值",
 | 
			
		||||
    "VerificatExpireTime": "登录过期时间(分)",
 | 
			
		||||
    "ErrorLockTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "ErrorResetTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "ErrorCount.MinValue": " {0} 值太小",
 | 
			
		||||
    "VerificatExpireTime.MinValue": " {0} 值太小"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PagePolicy": {
 | 
			
		||||
    "Shortcuts": "默认快捷方式",
 | 
			
		||||
    "Razor": "默认主页"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PasswordPolicy": {
 | 
			
		||||
    "DefaultPassword": "默认用户密码",
 | 
			
		||||
    "DefaultPassword.Required": " {0} 是必填项",
 | 
			
		||||
    "PasswordMinLen": "密码最小长度",
 | 
			
		||||
    "PasswordMinLen.MinValue": " {0} 值太小",
 | 
			
		||||
    "PasswordContainNum": "包含数字",
 | 
			
		||||
    "PasswordContainLower": "包含小写字母",
 | 
			
		||||
    "PasswordContainUpper": "包含大写字母",
 | 
			
		||||
    "PasswordContainChar": "包含特殊字符"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.WebsitePolicy": {
 | 
			
		||||
    "WebStatus": "是否开放",
 | 
			
		||||
    "CloseTip": "关闭提示",
 | 
			
		||||
    "CloseTip.Required": " {0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //enum
 | 
			
		||||
    "ThingsGateway.Admin.Application.ResourceCategoryEnum": {
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Menu": "菜单",
 | 
			
		||||
      "Button": "按钮"
 | 
			
		||||
    },
 | 
			
		||||
  //enum
 | 
			
		||||
  "ThingsGateway.Admin.Application.ResourceCategoryEnum": {
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Menu": "菜单",
 | 
			
		||||
    "Button": "按钮"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.TargetEnum": {
 | 
			
		||||
      "_self": "本窗口",
 | 
			
		||||
      "_blank": "新窗口",
 | 
			
		||||
      "_parent": "父级窗口",
 | 
			
		||||
      "_top": "顶级窗口"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DictTypeEnum": {
 | 
			
		||||
      "System": "系统配置",
 | 
			
		||||
      "Define": "业务配置"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.TargetEnum": {
 | 
			
		||||
    "_self": "本窗口",
 | 
			
		||||
    "_blank": "新窗口",
 | 
			
		||||
    "_parent": "父级窗口",
 | 
			
		||||
    "_top": "顶级窗口"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DictTypeEnum": {
 | 
			
		||||
    "System": "系统配置",
 | 
			
		||||
    "Define": "业务配置"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogCateGoryEnum": {
 | 
			
		||||
      "Login": "登录",
 | 
			
		||||
      "Logout": "注销",
 | 
			
		||||
      "Operate": "操作",
 | 
			
		||||
      "Exception": "异常"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogCateGoryEnum": {
 | 
			
		||||
    "Login": "登录",
 | 
			
		||||
    "Logout": "注销",
 | 
			
		||||
    "Operate": "操作",
 | 
			
		||||
    "Exception": "异常"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogEnum": {
 | 
			
		||||
      "SUCCESS": "成功",
 | 
			
		||||
      "FAIL": "失败"
 | 
			
		||||
    }
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogEnum": {
 | 
			
		||||
    "SUCCESS": "成功",
 | 
			
		||||
    "FAIL": "失败"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,6 @@ using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.FriendlyException;
 | 
			
		||||
using ThingsGateway.Logging;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
using ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
@@ -41,33 +38,31 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
    /// <param name="flush"></param>
 | 
			
		||||
    public async Task WriteAsync(LogMessage logMsg, bool flush)
 | 
			
		||||
    {
 | 
			
		||||
        //获取请求json字符串
 | 
			
		||||
        var jsonString = logMsg.Context.Get("loggingMonitor").ToString();
 | 
			
		||||
        //转成实体
 | 
			
		||||
        var loggingMonitor = jsonString.FromJsonNetString<LoggingMonitorJson>();
 | 
			
		||||
        var requestAuditData = logMsg.Context.Get(nameof(RequestAuditData)) as RequestAuditData;
 | 
			
		||||
        //日志时间赋值
 | 
			
		||||
        loggingMonitor.LogDateTime = logMsg.LogDateTime;
 | 
			
		||||
        // loggingMonitor.ReturnInformation.Value
 | 
			
		||||
        requestAuditData.LogDateTime = logMsg.LogDateTime;
 | 
			
		||||
        // requestAuditData.ReturnInformation.Value
 | 
			
		||||
        //验证失败不记录日志
 | 
			
		||||
        bool save = false;
 | 
			
		||||
        if (loggingMonitor.Validation == null)
 | 
			
		||||
        if (requestAuditData.Validation == null)
 | 
			
		||||
        {
 | 
			
		||||
            var operation = logMsg.Context.Get(LoggingConst.Operation).ToString();//获取操作名称
 | 
			
		||||
            var client = (ClientInfo)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息
 | 
			
		||||
            var path = logMsg.Context.Get(LoggingConst.Path).ToString();//获取操作名称
 | 
			
		||||
            var method = logMsg.Context.Get(LoggingConst.Method).ToString();//获取方法
 | 
			
		||||
            var operation = requestAuditData.Operation;//获取操作名称
 | 
			
		||||
            var client = requestAuditData.Client;//获取客户端信息
 | 
			
		||||
            var path = requestAuditData.Path;//获取操作名称
 | 
			
		||||
            var method = requestAuditData.Method;//获取方法
 | 
			
		||||
            //表示访问日志
 | 
			
		||||
            if (path == "/api/auth/login" || path == "/api/auth/logout")
 | 
			
		||||
            {
 | 
			
		||||
                //如果没有异常信息
 | 
			
		||||
                if (loggingMonitor.Exception == null)
 | 
			
		||||
                if (requestAuditData.Exception == null)
 | 
			
		||||
                {
 | 
			
		||||
                    save = await CreateVisitLog(operation, path, loggingMonitor, client, flush).ConfigureAwait(false);//添加到访问日志
 | 
			
		||||
                    save = await CreateVisitLog(operation, path, requestAuditData, client, flush).ConfigureAwait(false);//添加到访问日志
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    //添加到异常日志
 | 
			
		||||
                    save = await CreateOperationLog(operation, path, loggingMonitor, client, flush).ConfigureAwait(false);
 | 
			
		||||
                    save = await CreateOperationLog(operation, path, requestAuditData, client, flush).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
@@ -76,7 +71,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
                if (!operation.IsNullOrWhiteSpace() && method == "POST")
 | 
			
		||||
                {
 | 
			
		||||
                    //添加到操作日志
 | 
			
		||||
                    save = await CreateOperationLog(operation, path, loggingMonitor, client, flush).ConfigureAwait(false);
 | 
			
		||||
                    save = await CreateOperationLog(operation, path, requestAuditData, client, flush).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -91,27 +86,21 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="operation">操作名称</param>
 | 
			
		||||
    /// <param name="path">请求地址</param>
 | 
			
		||||
    /// <param name="loggingMonitor">loggingMonitor</param>
 | 
			
		||||
    /// <param name="clientInfo">客户端信息</param>
 | 
			
		||||
    /// <param name="requestAuditData">requestAuditData</param>
 | 
			
		||||
    /// <param name="userAgent">客户端信息</param>
 | 
			
		||||
    /// <param name="flush"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private async Task<bool> CreateOperationLog(string operation, string path, LoggingMonitorJson loggingMonitor, ClientInfo clientInfo, bool flush)
 | 
			
		||||
    private async Task<bool> CreateOperationLog(string operation, string path, RequestAuditData requestAuditData, UserAgent userAgent, bool flush)
 | 
			
		||||
    {
 | 
			
		||||
        //账号
 | 
			
		||||
        var opAccount = loggingMonitor.AuthorizationClaims?.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault();
 | 
			
		||||
        var opAccount = requestAuditData.AuthorizationClaims?.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
        //获取参数json字符串,
 | 
			
		||||
        var paramJson = loggingMonitor.Parameters == null || loggingMonitor.Parameters.Count == 0 ? null : loggingMonitor.Parameters[0].Value.ToJsonNetString();
 | 
			
		||||
        var paramJson = requestAuditData.Parameters == null || requestAuditData.Parameters.Count == 0 ? null : requestAuditData.Parameters.ToSystemTextJsonString();
 | 
			
		||||
 | 
			
		||||
        //获取结果json字符串
 | 
			
		||||
        var resultJson = string.Empty;
 | 
			
		||||
        if (loggingMonitor.ReturnInformation != null)//如果有返回值
 | 
			
		||||
        {
 | 
			
		||||
            if (loggingMonitor.ReturnInformation.Value != null)//如果返回值不为空
 | 
			
		||||
            {
 | 
			
		||||
                resultJson = loggingMonitor.ReturnInformation.Value.ToJsonNetString();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        var resultJson = requestAuditData.ReturnInformation?.ToSystemTextJsonString();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //操作日志表实体
 | 
			
		||||
        var sysLogOperate = new SysOperateLog
 | 
			
		||||
@@ -119,29 +108,29 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
            Name = operation,
 | 
			
		||||
            Category = LogCateGoryEnum.Operate,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = loggingMonitor.RemoteIPv4,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpTime = loggingMonitor.LogDateTime.LocalDateTime,
 | 
			
		||||
            OpIp = requestAuditData.RemoteIPv4,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = requestAuditData.LogDateTime.LocalDateTime,
 | 
			
		||||
            OpAccount = opAccount,
 | 
			
		||||
            ReqMethod = loggingMonitor.HttpMethod,
 | 
			
		||||
            ReqMethod = requestAuditData.Method,
 | 
			
		||||
            ReqUrl = path,
 | 
			
		||||
            ResultJson = resultJson,
 | 
			
		||||
            ClassName = loggingMonitor.DisplayName,
 | 
			
		||||
            MethodName = loggingMonitor.ActionName,
 | 
			
		||||
            ClassName = requestAuditData.ControllerName,
 | 
			
		||||
            MethodName = requestAuditData.ActionName,
 | 
			
		||||
            ParamJson = paramJson,
 | 
			
		||||
            VerificatId = UserManager.VerificatId,
 | 
			
		||||
        };
 | 
			
		||||
        //如果异常不为空
 | 
			
		||||
        if (loggingMonitor.Exception != null)
 | 
			
		||||
        if (requestAuditData.Exception != null)
 | 
			
		||||
        {
 | 
			
		||||
            sysLogOperate.Category = LogCateGoryEnum.Exception;//操作类型为异常
 | 
			
		||||
            sysLogOperate.ExeStatus = false;//操作状态为失败
 | 
			
		||||
 | 
			
		||||
            if (loggingMonitor.Exception.Type == typeof(AppFriendlyException).ToString())
 | 
			
		||||
                sysLogOperate.ExeMessage = loggingMonitor?.Exception.Message;
 | 
			
		||||
            if (requestAuditData.Exception.Type == typeof(AppFriendlyException).ToString())
 | 
			
		||||
                sysLogOperate.ExeMessage = requestAuditData?.Exception.Message;
 | 
			
		||||
            else
 | 
			
		||||
                sysLogOperate.ExeMessage = $"{loggingMonitor.Exception.Type}:{loggingMonitor.Exception.Message}{Environment.NewLine}{loggingMonitor.Exception.StackTrace}";
 | 
			
		||||
                sysLogOperate.ExeMessage = $"{requestAuditData.Exception.Type}:{requestAuditData.Exception.Message}{Environment.NewLine}{requestAuditData.Exception.StackTrace}";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _operateLogMessageQueue.Enqueue(sysLogOperate);
 | 
			
		||||
@@ -160,26 +149,25 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="operation">访问类型</param>
 | 
			
		||||
    /// <param name="path"></param>
 | 
			
		||||
    /// <param name="loggingMonitor">loggingMonitor</param>
 | 
			
		||||
    /// <param name="clientInfo">客户端信息</param>
 | 
			
		||||
    /// <param name="requestAuditData">requestAuditData</param>
 | 
			
		||||
    /// <param name="userAgent">客户端信息</param>
 | 
			
		||||
    /// <param name="flush"></param>
 | 
			
		||||
    private async Task<bool> CreateVisitLog(string operation, string path, LoggingMonitorJson loggingMonitor, ClientInfo clientInfo, bool flush)
 | 
			
		||||
    private async Task<bool> CreateVisitLog(string operation, string path, RequestAuditData requestAuditData, UserAgent userAgent, bool flush)
 | 
			
		||||
    {
 | 
			
		||||
        long verificatId = 0;//验证Id
 | 
			
		||||
        var opAccount = "";//用户账号
 | 
			
		||||
        if (path == "/api/auth/login")
 | 
			
		||||
        {
 | 
			
		||||
            //如果是登录,用户信息就从返回值里拿
 | 
			
		||||
            var result = loggingMonitor.ReturnInformation?.Value?.ToJsonNetString();//返回值转json
 | 
			
		||||
            var userInfo = result.FromJsonNetString<UnifyResult<LoginOutput>>();//格式化成user表
 | 
			
		||||
            dynamic userInfo = requestAuditData.ReturnInformation;
 | 
			
		||||
            opAccount = userInfo.Data.Account;//赋值账号
 | 
			
		||||
            verificatId = userInfo.Data.VerificatId;
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            //如果是登录出,用户信息就从AuthorizationClaims里拿
 | 
			
		||||
            opAccount = loggingMonitor.AuthorizationClaims.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault();
 | 
			
		||||
            verificatId = loggingMonitor.AuthorizationClaims.Where(it => it.Type == ClaimConst.VerificatId).Select(it => it.Value).FirstOrDefault().ToLong();
 | 
			
		||||
            opAccount = requestAuditData.AuthorizationClaims.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault();
 | 
			
		||||
            verificatId = requestAuditData.AuthorizationClaims.Where(it => it.Type == ClaimConst.VerificatId).Select(it => it.Value).FirstOrDefault().ToLong();
 | 
			
		||||
        }
 | 
			
		||||
        //日志表实体
 | 
			
		||||
        var sysLogVisit = new SysOperateLog
 | 
			
		||||
@@ -187,19 +175,19 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
 | 
			
		||||
            Name = operation,
 | 
			
		||||
            Category = path == "/api/auth/login" ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = loggingMonitor.RemoteIPv4,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpTime = loggingMonitor.LogDateTime.LocalDateTime,
 | 
			
		||||
            OpIp = requestAuditData.RemoteIPv4,
 | 
			
		||||
            OpBrowser = userAgent?.Browser,
 | 
			
		||||
            OpOs = userAgent?.Platform,
 | 
			
		||||
            OpTime = requestAuditData.LogDateTime.LocalDateTime,
 | 
			
		||||
            VerificatId = verificatId,
 | 
			
		||||
            OpAccount = opAccount,
 | 
			
		||||
 | 
			
		||||
            ReqMethod = loggingMonitor.HttpMethod,
 | 
			
		||||
            ReqMethod = requestAuditData.Method,
 | 
			
		||||
            ReqUrl = path,
 | 
			
		||||
            ResultJson = loggingMonitor.ReturnInformation?.Value?.ToJsonNetString(),
 | 
			
		||||
            ClassName = loggingMonitor.DisplayName,
 | 
			
		||||
            MethodName = loggingMonitor.ActionName,
 | 
			
		||||
            ParamJson = loggingMonitor.Parameters?.ToJsonNetString(),
 | 
			
		||||
            ResultJson = requestAuditData.ReturnInformation?.ToSystemTextJsonString(),
 | 
			
		||||
            ClassName = requestAuditData.ControllerName,
 | 
			
		||||
            MethodName = requestAuditData.ActionName,
 | 
			
		||||
            ParamJson = requestAuditData.Parameters?.ToSystemTextJsonString(),
 | 
			
		||||
        };
 | 
			
		||||
        _operateLogMessageQueue.Enqueue(sysLogVisit);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,12 +15,15 @@ using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class AppService : IAppService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IUserAgentService UserAgentService;
 | 
			
		||||
    public AppService(IUserAgentService userAgentService)
 | 
			
		||||
    {
 | 
			
		||||
        UserAgentService = userAgentService;
 | 
			
		||||
    }
 | 
			
		||||
    public string GetReturnUrl(string returnUrl)
 | 
			
		||||
    {
 | 
			
		||||
        var url = QueryHelpers.AddQueryString(CookieAuthenticationDefaults.LoginPath, new Dictionary<string, string?>
 | 
			
		||||
@@ -41,18 +44,16 @@ public class AppService : IAppService
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public Parser Parser = Parser.GetDefault();
 | 
			
		||||
    public ClientInfo? ClientInfo
 | 
			
		||||
    public UserAgent? UserAgent
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            var str = App.HttpContext?.Request?.Headers?.UserAgent;
 | 
			
		||||
            ClientInfo? clientInfo = null;
 | 
			
		||||
            if (!string.IsNullOrEmpty(str))
 | 
			
		||||
            {
 | 
			
		||||
                clientInfo = Parser.Parse(str);
 | 
			
		||||
                return UserAgentService.Parse(str);
 | 
			
		||||
            }
 | 
			
		||||
            return clientInfo;
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,19 +13,17 @@ using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class HybridAppService : IAppService
 | 
			
		||||
{
 | 
			
		||||
    public HybridAppService()
 | 
			
		||||
    public HybridAppService(IUserAgentService userAgentService)
 | 
			
		||||
    {
 | 
			
		||||
        var str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0";
 | 
			
		||||
        ClientInfo = Parser.GetDefault().Parse(str);
 | 
			
		||||
        UserAgent = userAgentService.Parse(str);
 | 
			
		||||
        RemoteIpAddress = "127.0.0.1";
 | 
			
		||||
    }
 | 
			
		||||
    public ClientInfo? ClientInfo { get; }
 | 
			
		||||
    public UserAgent? UserAgent { get; }
 | 
			
		||||
 | 
			
		||||
    private static BlazorHybridAuthenticationStateProvider _authenticationStateProvider;
 | 
			
		||||
    private static BlazorHybridAuthenticationStateProvider AuthenticationStateProvider
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public interface IAppService
 | 
			
		||||
@@ -20,7 +18,7 @@ public interface IAppService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// ClientInfo
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public ClientInfo? ClientInfo { get; }
 | 
			
		||||
    public UserAgent? UserAgent { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// ClaimsPrincipal
 | 
			
		||||
 
 | 
			
		||||
@@ -96,9 +96,9 @@ public class AuthService : IAuthService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task LoginOutAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (UserManager.UserId == 0)
 | 
			
		||||
        if (UserManager.VerificatId == 0)
 | 
			
		||||
            return;
 | 
			
		||||
        var verificatId = UserManager.UserId;
 | 
			
		||||
        var verificatId = UserManager.VerificatId;
 | 
			
		||||
        //获取用户信息
 | 
			
		||||
        var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false);
 | 
			
		||||
        if (userinfo != null)
 | 
			
		||||
@@ -237,7 +237,7 @@ public class AuthService : IAuthService
 | 
			
		||||
        var logingEvent = new LoginEvent
 | 
			
		||||
        {
 | 
			
		||||
            Ip = _appService.RemoteIpAddress,
 | 
			
		||||
            Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(),
 | 
			
		||||
            Device = App.GetService<IAppService>().UserAgent?.Platform,
 | 
			
		||||
            Expire = expire,
 | 
			
		||||
            SysUser = sysUser,
 | 
			
		||||
            VerificatId = verificatId
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ internal sealed class SysDictService : BaseService<SysDict>, ISysDictService
 | 
			
		||||
        //更新数据
 | 
			
		||||
        List<SysDict> dicts = new List<SysDict>()
 | 
			
		||||
        {
 | 
			
		||||
            new SysDict() { DictType = DictTypeEnum.System, Category = nameof(PagePolicy), Name = nameof(PagePolicy.Shortcuts), Code = input.Shortcuts.ToJsonNetString() },
 | 
			
		||||
            new SysDict() { DictType = DictTypeEnum.System, Category = nameof(PagePolicy), Name = nameof(PagePolicy.Shortcuts), Code = input.Shortcuts.ToSystemTextJsonString() },
 | 
			
		||||
    };
 | 
			
		||||
        var storageable = await db.Storageable(dicts).WhereColumns(it => new { it.DictType, it.Category, it.Name }).ToStorageAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,9 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
/// 内存推送事件服务
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <typeparam name="TEntry"></typeparam>
 | 
			
		||||
public class EventService<TEntry> : IEventService<TEntry>
 | 
			
		||||
public class EventService<TEntry> : IEventService<TEntry>, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private ConcurrentDictionary<string, Func<TEntry, Task>> Cache { get; } = new();
 | 
			
		||||
    private ConcurrentDictionary<string, Func<TEntry, Task>> Cache = new();
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@
 | 
			
		||||
using Microsoft.AspNetCore.Http.Connections.Features;
 | 
			
		||||
using Microsoft.AspNetCore.SignalR;
 | 
			
		||||
 | 
			
		||||
using Yitter.IdGenerator;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -28,7 +26,7 @@ public class UserIdProvider : IUserIdProvider
 | 
			
		||||
 | 
			
		||||
        if (UserId > 0)
 | 
			
		||||
        {
 | 
			
		||||
            return $"{UserId}{SysHub.Separate}{YitIdHelper.NextId()}";//返回用户ID
 | 
			
		||||
            return $"{UserId}{SysHub.Separate}{CommonUtils.GetSingleId()}";//返回用户ID
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return connection.ConnectionId;
 | 
			
		||||
 
 | 
			
		||||
@@ -277,7 +277,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
        if (isSuperAdmin)
 | 
			
		||||
            throw Oops.Bah(Localizer["CanotGrantAdmin"]);
 | 
			
		||||
        var menuIds = input.GrantInfoList.Select(it => it.MenuId).ToList();//菜单ID
 | 
			
		||||
        var extJsons = input.GrantInfoList.Select(it => it.ToJsonNetString()).ToList();//拓展信息
 | 
			
		||||
        var extJsons = input.GrantInfoList.Select(it => it.ToSystemTextJsonString()).ToList();//拓展信息
 | 
			
		||||
        var relationRoles = new List<SysRelation>();//要添加的角色资源和授权关系表
 | 
			
		||||
        var sysRole = (await GetAllAsync().ConfigureAwait(false)).FirstOrDefault(it => it.Id == input.Id);//获取角色
 | 
			
		||||
 | 
			
		||||
@@ -338,7 +338,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
                    ExtJson = new RelationPermission
 | 
			
		||||
                    {
 | 
			
		||||
                        ApiUrl = it.ApiRoute,
 | 
			
		||||
                    }.ToJsonNetString()
 | 
			
		||||
                    }.ToSystemTextJsonString()
 | 
			
		||||
                });
 | 
			
		||||
                relationRoles.AddRange(relationRolePer);//合并列表
 | 
			
		||||
            }
 | 
			
		||||
@@ -410,7 +410,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
        if (sysRole != null)
 | 
			
		||||
        {
 | 
			
		||||
            await _relationService.SaveRelationBatchAsync(RelationCategoryEnum.RoleHasOpenApiPermission, input.Id,
 | 
			
		||||
                 input.GrantInfoList.Select(a => (a.ApiUrl, a.ToJsonNetString()))
 | 
			
		||||
                 input.GrantInfoList.Select(a => (a.ApiUrl, a.ToSystemTextJsonString()))
 | 
			
		||||
                , true).ConfigureAwait(false);//添加到数据库
 | 
			
		||||
            await ClearTokenUtil.DeleteUserCacheByRoleIds(new List<long> { input.Id }).ConfigureAwait(false);//清除角色下用户缓存
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -117,6 +117,25 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
        db.Aop.DataExecuted = (value, entity) =>
 | 
			
		||||
        {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        db.Aop.OnLogExecuted = (sql, pars) =>
 | 
			
		||||
        {
 | 
			
		||||
            //执行时间超过1秒
 | 
			
		||||
            if (db.Ado.SqlExecutionTime.TotalSeconds > 1)
 | 
			
		||||
            {
 | 
			
		||||
                //代码CS文件名
 | 
			
		||||
                var fileName = db.Ado.SqlStackTrace.FirstFileName;
 | 
			
		||||
                //代码行数
 | 
			
		||||
                var fileLine = db.Ado.SqlStackTrace.FirstLine;
 | 
			
		||||
                //方法名
 | 
			
		||||
                var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName;
 | 
			
		||||
 | 
			
		||||
                DbContext.WriteLog($"{fileName}-{FirstMethodName}-{fileLine} 执行时间超过1秒");
 | 
			
		||||
                DbContext.WriteLogWithSql(UtilMethods.GetNativeSql(sql, pars));
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -435,7 +435,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
        if (sysUser != null)
 | 
			
		||||
        {
 | 
			
		||||
            await _relationService.SaveRelationBatchAsync(RelationCategoryEnum.UserHasOpenApiPermission, input.Id,
 | 
			
		||||
                 input.GrantInfoList.Select(a => (a.ApiUrl, a.ToJsonNetString())),
 | 
			
		||||
                 input.GrantInfoList.Select(a => (a.ApiUrl, a.ToSystemTextJsonString())),
 | 
			
		||||
                true).ConfigureAwait(false);//添加到数据库
 | 
			
		||||
            DeleteUserFromCache(input.Id);
 | 
			
		||||
        }
 | 
			
		||||
@@ -557,7 +557,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
    public async Task GrantResourceAsync(GrantResourceData input)
 | 
			
		||||
    {
 | 
			
		||||
        var menuIds = input.GrantInfoList.Select(it => it.MenuId).ToList();//菜单ID
 | 
			
		||||
        var extJsons = input.GrantInfoList.Select(it => it.ToJsonNetString()).ToList();//拓展信息
 | 
			
		||||
        var extJsons = input.GrantInfoList.Select(it => it.ToSystemTextJsonString()).ToList();//拓展信息
 | 
			
		||||
        var relationUsers = new List<SysRelation>();//要添加的用户资源和授权关系表
 | 
			
		||||
        var sysUser = await GetUserByIdAsync(input.Id).ConfigureAwait(false);//获取用户
 | 
			
		||||
        await CheckApiDataScopeAsync(sysUser.OrgId, sysUser.CreateUserId).ConfigureAwait(false);
 | 
			
		||||
@@ -613,7 +613,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
                    TargetId = it.ApiRoute,
 | 
			
		||||
                    Category = RelationCategoryEnum.UserHasPermission,
 | 
			
		||||
                    ExtJson = new RelationPermission { ApiUrl = it.ApiRoute }
 | 
			
		||||
                            .ToJsonNetString()
 | 
			
		||||
                            .ToSystemTextJsonString()
 | 
			
		||||
                });
 | 
			
		||||
                relationUsers.AddRange(relationUserPer);//合并列表
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -203,7 +203,7 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
 | 
			
		||||
    public async Task UpdateWorkbenchInfoAsync(WorkbenchInfo input)
 | 
			
		||||
    {
 | 
			
		||||
        //关系表保存个人工作台
 | 
			
		||||
        await _relationService.SaveRelationAsync(RelationCategoryEnum.UserWorkbenchData, input.Id, null, input.Shortcuts.ToJsonNetString(),
 | 
			
		||||
        await _relationService.SaveRelationAsync(RelationCategoryEnum.UserWorkbenchData, input.Id, null, input.Shortcuts.ToSystemTextJsonString(),
 | 
			
		||||
            true).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,6 @@
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.List;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -169,7 +166,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
 | 
			
		||||
    public void RemoveAllClientId()
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        db.Updateable<VerificatInfo>().SetColumns("ClientIds", new ConcurrentList<long>().ToJsonNetString()).Where(a => a.Id >= 0).ExecuteCommand();
 | 
			
		||||
        db.Updateable<VerificatInfo>().SetColumns(a => a.ClientIds == null).Where(a => a.Id > 0).ExecuteCommand();
 | 
			
		||||
        VerificatInfoService.RemoveCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,9 @@ public static class DbContext
 | 
			
		||||
    {
 | 
			
		||||
        db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings
 | 
			
		||||
        {
 | 
			
		||||
            SqlServerCodeFirstNvarchar = true//设置默认nvarchar
 | 
			
		||||
            SqlServerCodeFirstNvarchar = true, //设置默认nvarchar
 | 
			
		||||
 | 
			
		||||
            IsNoReadXmlDescription = true
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,11 @@ public sealed class SqlSugarOption : ConnectionConfig
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool InitSeedData { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化数据库
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool InitDatabase { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -32,7 +37,7 @@ public sealed class SqlSugarOption : ConnectionConfig
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否控制台显示Sql语句
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsShowSql { get; set; }
 | 
			
		||||
    public bool? IsShowSql { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 更新数据
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ public class Startup : AppStartup
 | 
			
		||||
        services.AddSingleton<ISugarAopService, SugarAopService>();
 | 
			
		||||
        services.AddSingleton<ISugarConfigAopService, SugarConfigAopService>();
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<IUserAgentService, UserAgentService>();
 | 
			
		||||
        services.AddSingleton<IAppService, AppService>();
 | 
			
		||||
 | 
			
		||||
        StaticConfig.EnableAllWhereIF = true;
 | 
			
		||||
@@ -89,7 +90,7 @@ public class Startup : AppStartup
 | 
			
		||||
        DbContext.DbConfigs?.ForEach(it =>
 | 
			
		||||
        {
 | 
			
		||||
            var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
            if (it.InitDatabase == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -97,6 +98,21 @@ public class Startup : AppStartup
 | 
			
		||||
        CodeFirstUtils.CodeFirst(fullName!);//CodeFirst
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = DbContext.GetDB<SysOperateLog>();
 | 
			
		||||
            if (db.CurrentConnectionConfig.DbType == SqlSugar.DbType.Sqlite)
 | 
			
		||||
            {
 | 
			
		||||
                if (!db.DbMaintenance.IsAnyIndex("idx_operatelog_optime_date"))
 | 
			
		||||
                {
 | 
			
		||||
                    var indexsql = "CREATE INDEX idx_operatelog_optime_date ON sys_operatelog(strftime('%Y-%m-%d', OpTime));";
 | 
			
		||||
                    db.Ado.ExecuteCommand(indexsql);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch { }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //删除在线用户统计
 | 
			
		||||
        var verificatInfoService = App.RootServices.GetService<IVerificatInfoService>();
 | 
			
		||||
        verificatInfoService.RemoveAllClientId();
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,7 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
 | 
			
		||||
		<PackageReference Include="UAParser" Version="3.1.47" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.5" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
@@ -30,9 +29,9 @@
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Threading.RateLimiting" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Threading.RateLimiting" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<Content Remove="SeedData\Admin\*.json" />
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>Default interface for UserAgentService</summary>
 | 
			
		||||
    public interface IUserAgentService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>Gets or sets the settings.</summary>
 | 
			
		||||
        public UserAgentSettings Settings { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>Parses the specified user agent string.</summary>
 | 
			
		||||
        /// <param name="userAgentString">The user agent string.</param>
 | 
			
		||||
        /// <returns>An UserAgent object</returns>
 | 
			
		||||
        UserAgent? Parse(string userAgentString);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								src/Admin/ThingsGateway.Admin.Application/UserAgent/UserAgent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/Admin/ThingsGateway.Admin.Application/UserAgent/UserAgent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Parsed UserAgent object
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserAgent
 | 
			
		||||
    {
 | 
			
		||||
        private readonly UserAgentSettings settings;
 | 
			
		||||
 | 
			
		||||
        internal string Agent = "";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a browser.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a browser; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsBrowser { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a robot.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a robot; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsRobot { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value indicating whether this UserAgent is a mobile device.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        ///   <c>true</c> if this UserAgent is a mobile device; otherwise, <c>false</c>.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public bool IsMobile { get; set; } = false;
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the platform.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The platform or operating system.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Platform { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the browser.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The browser.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Browser { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the browser version.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The browser version.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string BrowserVersion { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the mobile device.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The mobile device.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Mobile { get; set; } = "";
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the robot.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>
 | 
			
		||||
        /// The robot.
 | 
			
		||||
        /// </value>
 | 
			
		||||
        public string Robot { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
        internal UserAgent(UserAgentSettings settings, string? userAgentString = null)
 | 
			
		||||
        {
 | 
			
		||||
            this.settings = settings;
 | 
			
		||||
 | 
			
		||||
            if (userAgentString != null)
 | 
			
		||||
            {
 | 
			
		||||
                Agent = userAgentString.Trim();
 | 
			
		||||
                SetPlatform();
 | 
			
		||||
                if (SetRobot()) return;
 | 
			
		||||
                if (SetBrowser()) return;
 | 
			
		||||
                if (SetMobile()) return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetPlatform()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Platforms)
 | 
			
		||||
            {
 | 
			
		||||
                if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    Platform = item.Value;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Platform = "Unknown Platform";
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetBrowser()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Browsers)
 | 
			
		||||
            {
 | 
			
		||||
                var match = Regex.Match(Agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase);
 | 
			
		||||
                if (match.Success)
 | 
			
		||||
                {
 | 
			
		||||
                    IsBrowser = true;
 | 
			
		||||
                    BrowserVersion = match.Groups[1].Value;
 | 
			
		||||
                    Browser = item.Value;
 | 
			
		||||
                    SetMobile();
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetRobot()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Robots)
 | 
			
		||||
            {
 | 
			
		||||
                if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase))
 | 
			
		||||
                {
 | 
			
		||||
                    IsRobot = true;
 | 
			
		||||
                    Robot = item.Value;
 | 
			
		||||
                    SetMobile();
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal bool SetMobile()
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in settings.Mobiles)
 | 
			
		||||
            {
 | 
			
		||||
                if (Agent?.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1)
 | 
			
		||||
                {
 | 
			
		||||
                    IsMobile = true;
 | 
			
		||||
                    Mobile = item.Value;
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The UserAgent service
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <seealso cref="ThingsGateway.Admin.Application.IUserAgentService" />
 | 
			
		||||
    public class UserAgentService : IUserAgentService
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the settings.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public UserAgentSettings Settings { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="UserAgentService"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public UserAgentService()
 | 
			
		||||
        {
 | 
			
		||||
            Settings = new UserAgentSettings();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private MemoryCache MemoryCache { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Parses the specified user agent string.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="userAgentString">The user agent string.</param>
 | 
			
		||||
        /// <returns>
 | 
			
		||||
        /// An UserAgent object
 | 
			
		||||
        /// </returns>
 | 
			
		||||
        public UserAgent? Parse(string? userAgentString)
 | 
			
		||||
        {
 | 
			
		||||
            userAgentString = ((userAgentString?.Length > Settings.UaStringSizeLimit) ? userAgentString?.Trim().Substring(0, Settings.UaStringSizeLimit) : userAgentString?.Trim()) ?? "";
 | 
			
		||||
            return MemoryCache.GetOrAdd(userAgentString, entry =>
 | 
			
		||||
            {
 | 
			
		||||
                return new UserAgent(Settings, userAgentString);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,214 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// UserAgent settings container.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserAgentSettings
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the maximum size of the useragent string. Limiting the length of the useragent string protects from hackers sending in extremely long user agent strings.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int UaStringSizeLimit { get; set; } = 512;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for platforms.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Platforms { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"windows nt 10.0", "Windows 10"},
 | 
			
		||||
            {"windows nt 6.3", "Windows 8.1"},
 | 
			
		||||
            {"windows nt 6.2", "Windows 8"},
 | 
			
		||||
            {"windows nt 6.1", "Windows 7"},
 | 
			
		||||
            {"windows nt 6.0", "Windows Vista"},
 | 
			
		||||
            {"windows nt 5.2", "Windows 2003"},
 | 
			
		||||
            {"windows nt 5.1", "Windows XP"},
 | 
			
		||||
            {"windows nt 5.0", "Windows 2000"},
 | 
			
		||||
            {"windows nt 4.0", "Windows NT 4.0"},
 | 
			
		||||
            {"winnt4.0", "Windows NT 4.0"},
 | 
			
		||||
            {"winnt 4.0", "Windows NT"},
 | 
			
		||||
            {"winnt", "Windows NT"},
 | 
			
		||||
            {"windows 98", "Windows 98"},
 | 
			
		||||
            {"win98", "Windows 98"},
 | 
			
		||||
            {"windows 95", "Windows 95"},
 | 
			
		||||
            {"win95", "Windows 95"},
 | 
			
		||||
            {"windows phone", "Windows Phone"},
 | 
			
		||||
            {"windows", "Unknown Windows OS"},
 | 
			
		||||
            {"android", "Android"},
 | 
			
		||||
            {"blackberry", "BlackBerry"},
 | 
			
		||||
            {"iphone", "iOS"},
 | 
			
		||||
            {"ipad", "iOS"},
 | 
			
		||||
            {"ipod", "iOS"},
 | 
			
		||||
            {"os x", "Mac OS X"},
 | 
			
		||||
            {"ppc mac", "Power PC Mac"},
 | 
			
		||||
            {"freebsd", "FreeBSD"},
 | 
			
		||||
            {"ppc", "Macintosh"},
 | 
			
		||||
            {"linux", "Linux"},
 | 
			
		||||
            {"debian", "Debian"},
 | 
			
		||||
            {"sunos", "Sun Solaris"},
 | 
			
		||||
            {"beos", "BeOS"},
 | 
			
		||||
            {"apachebench", "ApacheBench"},
 | 
			
		||||
            {"aix", "AIX"},
 | 
			
		||||
            {"irix", "Irix"},
 | 
			
		||||
            {"osf", "DEC OSF"},
 | 
			
		||||
            {"hp-ux", "HP-UX"},
 | 
			
		||||
            {"netbsd", "NetBSD"},
 | 
			
		||||
            {"bsdi", "BSDi"},
 | 
			
		||||
            {"openbsd", "OpenBSD"},
 | 
			
		||||
            {"gnu", "GNU/Linux"},
 | 
			
		||||
            {"unix", "Unknown Unix OS"},
 | 
			
		||||
            {"symbian", "Symbian OS"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for browsers.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Browsers { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"Microsoft Outlook", "Microsoft Outlook"},
 | 
			
		||||
            {"OPR", "Opera"},
 | 
			
		||||
            {"Flock", "Flock"},
 | 
			
		||||
            {"Edge", "Edge"},
 | 
			
		||||
            {"Edg", "Edge"},
 | 
			
		||||
            {"Chrome", "Chrome"},
 | 
			
		||||
            {"Opera.*?Version", "Opera"},
 | 
			
		||||
            {"Opera", "Opera"},
 | 
			
		||||
            {"MSIE", "Internet Explorer"},
 | 
			
		||||
            {"Internet Explorer", "Internet Explorer"},
 | 
			
		||||
            {"Trident.* rv" , "Internet Explorer"},
 | 
			
		||||
            {"Shiira", "Shiira"},
 | 
			
		||||
            {"Firefox", "Firefox"},
 | 
			
		||||
            {"Chimera", "Chimera"},
 | 
			
		||||
            {"Phoenix", "Phoenix"},
 | 
			
		||||
            {"Firebird", "Firebird"},
 | 
			
		||||
            {"Camino", "Camino"},
 | 
			
		||||
            {"Netscape", "Netscape"},
 | 
			
		||||
            {"OmniWeb", "OmniWeb"},
 | 
			
		||||
            {"Safari", "Safari"},
 | 
			
		||||
            {"Mozilla", "Mozilla"},
 | 
			
		||||
            {"Konqueror", "Konqueror"},
 | 
			
		||||
            {"icab", "iCab"},
 | 
			
		||||
            {"Lynx", "Lynx"},
 | 
			
		||||
            {"Links", "Links"},
 | 
			
		||||
            {"hotjava", "HotJava"},
 | 
			
		||||
            {"amaya", "Amaya"},
 | 
			
		||||
            {"IBrowse", "IBrowse"},
 | 
			
		||||
            {"Maxthon", "Maxthon"},
 | 
			
		||||
            {"Ubuntu", "Ubuntu Web Browser"},
 | 
			
		||||
            {"Vivaldi", "Vivaldi"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for mobiles.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Mobiles { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            // Legacy
 | 
			
		||||
            {"mobileexplorer", "Mobile Explorer"},
 | 
			
		||||
            {"palmsource", "Palm"},
 | 
			
		||||
            {"palmscape", "Palmscape"},
 | 
			
		||||
            // Phones and Manufacturers
 | 
			
		||||
            {"motorola", "Motorola"},
 | 
			
		||||
            {"nokia", "Nokia"},
 | 
			
		||||
            {"palm", "Palm"},
 | 
			
		||||
            {"iphone", "Apple iPhone"},
 | 
			
		||||
            {"ipad", "iPad"},
 | 
			
		||||
            {"ipod", "Apple iPod Touch"},
 | 
			
		||||
            {"sony", "Sony Ericsson"},
 | 
			
		||||
            {"ericsson", "Sony Ericsson"},
 | 
			
		||||
            {"blackberry", "BlackBerry"},
 | 
			
		||||
            {"cocoon", "O2 Cocoon"},
 | 
			
		||||
            {"blazer", "Treo"},
 | 
			
		||||
            {"lg", "LG"},
 | 
			
		||||
            {"amoi", "Amoi"},
 | 
			
		||||
            {"xda", "XDA"},
 | 
			
		||||
            {"mda", "MDA"},
 | 
			
		||||
            {"vario", "Vario"},
 | 
			
		||||
            {"htc", "HTC"},
 | 
			
		||||
            {"samsung", "Samsung"},
 | 
			
		||||
            {"sharp", "Sharp"},
 | 
			
		||||
            {"sie-", "Siemens"},
 | 
			
		||||
            {"alcatel", "Alcatel"},
 | 
			
		||||
            {"benq", "BenQ"},
 | 
			
		||||
            {"ipaq", "HP iPaq"},
 | 
			
		||||
            {"mot-", "Motorola"},
 | 
			
		||||
            {"playstation portable", "PlayStation Portable"},
 | 
			
		||||
            {"playstation 3", "PlayStation 3"},
 | 
			
		||||
            {"playstation vita", "PlayStation Vita"},
 | 
			
		||||
            {"hiptop", "Danger Hiptop"},
 | 
			
		||||
            {"nec-", "NEC"},
 | 
			
		||||
            {"panasonic", "Panasonic"},
 | 
			
		||||
            {"philips", "Philips"},
 | 
			
		||||
            {"sagem", "Sagem"},
 | 
			
		||||
            {"sanyo", "Sanyo"},
 | 
			
		||||
            {"spv", "SPV"},
 | 
			
		||||
            {"zte", "ZTE"},
 | 
			
		||||
            {"sendo", "Sendo"},
 | 
			
		||||
            {"nintendo dsi", "Nintendo DSi"},
 | 
			
		||||
            {"nintendo ds", "Nintendo DS"},
 | 
			
		||||
            {"nintendo 3ds", "Nintendo 3DS"},
 | 
			
		||||
            {"wii", "Nintendo Wii"},
 | 
			
		||||
            {"open web", "Open Web"},
 | 
			
		||||
            {"openweb", "OpenWeb"},
 | 
			
		||||
            // Operating Systems
 | 
			
		||||
            {"android", "Android"},
 | 
			
		||||
            {"symbian", "Symbian"},
 | 
			
		||||
            {"SymbianOS", "SymbianOS"},
 | 
			
		||||
            {"elaine", "Palm"},
 | 
			
		||||
            {"series60", "Symbian S60"},
 | 
			
		||||
            {"windows ce", "Windows CE"},
 | 
			
		||||
            // Browsers
 | 
			
		||||
            {"obigo", "Obigo"},
 | 
			
		||||
            {"netfront", "Netfront Browser"},
 | 
			
		||||
            {"openwave", "Openwave Browser"},
 | 
			
		||||
            {"mobilexplorer", "Mobile Explorer"},
 | 
			
		||||
            {"operamini", "Opera Mini"},
 | 
			
		||||
            {"opera mini", "Opera Mini"},
 | 
			
		||||
            {"opera mobi", "Opera Mobile"},
 | 
			
		||||
            {"fennec", "Firefox Mobile"},
 | 
			
		||||
            // Other
 | 
			
		||||
            {"digital paths", "Digital Paths"},
 | 
			
		||||
            {"avantgo", "AvantGo"},
 | 
			
		||||
            {"xiino", "Xiino"},
 | 
			
		||||
            {"novarra", "Novarra Transcoder"},
 | 
			
		||||
            {"vodafone", "Vodafone"},
 | 
			
		||||
            {"docomo", "NTT DoCoMo"},
 | 
			
		||||
            {"o2", "O2"},
 | 
			
		||||
            // Fallback
 | 
			
		||||
            {"mobile", "Generic Mobile"},
 | 
			
		||||
            {"wireless", "Generic Mobile"},
 | 
			
		||||
            {"j2me", "Generic Mobile"},
 | 
			
		||||
            {"midp", "Generic Mobile"},
 | 
			
		||||
            {"cldc", "Generic Mobile"},
 | 
			
		||||
            {"up.link", "Generic Mobile"},
 | 
			
		||||
            {"up.browser", "Generic Mobile"},
 | 
			
		||||
            {"smartphone", "Generic Mobile"},
 | 
			
		||||
            {"cellphone", "Generic Mobile"},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets a dictionary containing mappings for robots.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Dictionary<string, string> Robots { get; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"googlebot", "Googlebot"},
 | 
			
		||||
            {"msnbot", "MSNBot"},
 | 
			
		||||
            {"baiduspider", "Baiduspider"},
 | 
			
		||||
            {"bingbot", "Bing"},
 | 
			
		||||
            {"slurp", "Inktomi Slurp"},
 | 
			
		||||
            {"yahoo", "Yahoo"},
 | 
			
		||||
            {"ask jeeves", "Ask Jeeves"},
 | 
			
		||||
            {"fastcrawler", "FastCrawler"},
 | 
			
		||||
            {"infoseek", "InfoSeek Robot 1.0"},
 | 
			
		||||
            {"lycos", "Lycos"},
 | 
			
		||||
            {"yandex", "YandexBot"},
 | 
			
		||||
            {"mediapartners-google", "MediaPartners Google"},
 | 
			
		||||
            {"CRAZYWEBCRAWLER", "Crazy Webcrawler"},
 | 
			
		||||
            {"adsbot-google", "AdsBot Google"},
 | 
			
		||||
            {"feedfetcher-google", "Feedfetcher Google"},
 | 
			
		||||
            {"curious george", "Curious George"},
 | 
			
		||||
            {"ia_archiver", "Alexa Crawler"},
 | 
			
		||||
            {"MJ12bot", "Majestic-12"},
 | 
			
		||||
            {"Uptimebot", "Uptimebot"},
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -97,7 +97,7 @@ public class BlazorAppContext
 | 
			
		||||
            AllResource = sysResources;
 | 
			
		||||
            var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
 | 
			
		||||
            AllMenus = sysResources.Where(a => a.Category == ResourceCategoryEnum.Menu);
 | 
			
		||||
            AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
 | 
			
		||||
 | 
			
		||||
            if (moduleId == null)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public partial class SessionPage
 | 
			
		||||
        {
 | 
			
		||||
            var op = new DialogOption()
 | 
			
		||||
            {
 | 
			
		||||
                IsScrolling = false,
 | 
			
		||||
                IsScrolling = true,
 | 
			
		||||
                Title = Localizer[nameof(VerificatInfo)],
 | 
			
		||||
                ShowMaximizeButton = true,
 | 
			
		||||
                Class = "dialog-table",
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,10 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -39,19 +39,4 @@
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row g-2 mx-1 form-inline">
 | 
			
		||||
    <div class="col-12  col-md-12">
 | 
			
		||||
        <Card IsShadow=true class="m-2 flex-fill" Color="Color.Primary">
 | 
			
		||||
            <HeaderTemplate>
 | 
			
		||||
                @Localizer["HardwareInfoChart"]
 | 
			
		||||
            </HeaderTemplate>
 | 
			
		||||
 | 
			
		||||
            <BodyTemplate>
 | 
			
		||||
                <Chart @ref=CPULineChart OnInitAsync="OnCPUInit" Height="var(--line-chart-height)" Width="100%" OnAfterInitAsync="()=>{chartInit=true;return Task.CompletedTask;}" />
 | 
			
		||||
            </BodyTemplate>
 | 
			
		||||
        </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.Extensions.Localization;
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Admin.Razor;
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
@@ -31,118 +29,8 @@ namespace ThingsGateway.AdminServer;
 | 
			
		||||
[IgnoreRolePermission]
 | 
			
		||||
[Route("/")]
 | 
			
		||||
[TabItemOption(Text = "Home", Icon = "fas fa-house")]
 | 
			
		||||
public partial class AdminIndex : IDisposable
 | 
			
		||||
public partial class AdminIndex
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private IHardwareJob HardwareJob { get; set; }
 | 
			
		||||
 | 
			
		||||
    protected override void OnInitialized()
 | 
			
		||||
    {
 | 
			
		||||
        _ = RunTimerAsync();
 | 
			
		||||
        base.OnInitialized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool Disposed { get; set; }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        Disposed = true;
 | 
			
		||||
        GC.SuppressFinalize(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task RunTimerAsync()
 | 
			
		||||
    {
 | 
			
		||||
        while (!Disposed)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (chartInit)
 | 
			
		||||
                    await CPULineChart.Update(ChartAction.Update);
 | 
			
		||||
 | 
			
		||||
                await InvokeAsync(StateHasChanged);
 | 
			
		||||
                await Task.Delay(30000);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                NewLife.Log.XTrace.WriteException(ex);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 曲线
 | 
			
		||||
 | 
			
		||||
    private bool chartInit { get; set; }
 | 
			
		||||
    private Chart CPULineChart { get; set; }
 | 
			
		||||
    private ChartDataSource? ChartDataSource { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private IStringLocalizer<HistoryHardwareInfo> HistoryHardwareInfoLocalizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task<ChartDataSource> OnCPUInit()
 | 
			
		||||
    {
 | 
			
		||||
        if (ChartDataSource == null)
 | 
			
		||||
        {
 | 
			
		||||
            var hisHardwareInfos = await HardwareJob.GetHistoryHardwareInfos();
 | 
			
		||||
            ChartDataSource = new ChartDataSource();
 | 
			
		||||
            ChartDataSource.Options.Title = Localizer[nameof(HistoryHardwareInfo)];
 | 
			
		||||
            ChartDataSource.Options.X.Title = Localizer["DateTime"];
 | 
			
		||||
            ChartDataSource.Options.Y.Title = Localizer["Data"];
 | 
			
		||||
            ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd HH:mm zz"));
 | 
			
		||||
            ChartDataSource.Data.Add(new ChartDataset()
 | 
			
		||||
            {
 | 
			
		||||
                Tension = 0.4f,
 | 
			
		||||
                PointRadius = 1,
 | 
			
		||||
                Label = HistoryHardwareInfoLocalizer[nameof(HistoryHardwareInfo.CpuUsage)],
 | 
			
		||||
                Data = hisHardwareInfos.Select(a => (object)a.CpuUsage),
 | 
			
		||||
            });
 | 
			
		||||
            ChartDataSource.Data.Add(new ChartDataset()
 | 
			
		||||
            {
 | 
			
		||||
                Tension = 0.4f,
 | 
			
		||||
                PointRadius = 1,
 | 
			
		||||
                Label = HistoryHardwareInfoLocalizer[nameof(HistoryHardwareInfo.MemoryUsage)],
 | 
			
		||||
                Data = hisHardwareInfos.Select(a => (object)a.MemoryUsage),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ChartDataSource.Data.Add(new ChartDataset()
 | 
			
		||||
            {
 | 
			
		||||
                Tension = 0.4f,
 | 
			
		||||
                PointRadius = 1,
 | 
			
		||||
                Label = HistoryHardwareInfoLocalizer[nameof(HistoryHardwareInfo.DriveUsage)],
 | 
			
		||||
                Data = hisHardwareInfos.Select(a => (object)a.DriveUsage),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ChartDataSource.Data.Add(new ChartDataset()
 | 
			
		||||
            {
 | 
			
		||||
                ShowPointStyle = false,
 | 
			
		||||
                Tension = 0.4f,
 | 
			
		||||
                PointRadius = 1,
 | 
			
		||||
                Label = HistoryHardwareInfoLocalizer[nameof(HistoryHardwareInfo.Temperature)],
 | 
			
		||||
                Data = hisHardwareInfos.Select(a => (object)a.Temperature),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ChartDataSource.Data.Add(new ChartDataset()
 | 
			
		||||
            {
 | 
			
		||||
                Tension = 0.4f,
 | 
			
		||||
                PointRadius = 1,
 | 
			
		||||
                Label = HistoryHardwareInfoLocalizer[nameof(HistoryHardwareInfo.Battery)],
 | 
			
		||||
                Data = hisHardwareInfos.Select(a => (object)a.Battery),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var hisHardwareInfos = await HardwareJob.GetHistoryHardwareInfos();
 | 
			
		||||
            ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd HH:mm zz"));
 | 
			
		||||
            ChartDataSource.Data[0].Data = hisHardwareInfos.Select(a => (object)a.CpuUsage);
 | 
			
		||||
            ChartDataSource.Data[1].Data = hisHardwareInfos.Select(a => (object)a.MemoryUsage);
 | 
			
		||||
            ChartDataSource.Data[2].Data = hisHardwareInfos.Select(a => (object)a.DriveUsage);
 | 
			
		||||
            ChartDataSource.Data[3].Data = hisHardwareInfos.Select(a => (object)a.Temperature);
 | 
			
		||||
            ChartDataSource.Data[4].Data = hisHardwareInfos.Select(a => (object)a.Battery);
 | 
			
		||||
        }
 | 
			
		||||
        return ChartDataSource;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion 曲线
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private BlazorAppContext AppContext { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ using Microsoft.AspNetCore.DataProtection;
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
 | 
			
		||||
using Microsoft.AspNetCore.HttpOverrides;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.Controllers;
 | 
			
		||||
using Microsoft.AspNetCore.StaticFiles;
 | 
			
		||||
using Microsoft.Extensions.Localization;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
@@ -29,7 +28,6 @@ using System.Text.Unicode;
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Admin.Razor;
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.Logging;
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.AdminServer;
 | 
			
		||||
@@ -89,6 +87,7 @@ public class Startup : AppStartup
 | 
			
		||||
        }
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        services.AddMvcFilter<RequestAuditFilter>();
 | 
			
		||||
        services.AddControllers()
 | 
			
		||||
            .AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings))
 | 
			
		||||
            //.AddXmlSerializerFormatters()
 | 
			
		||||
@@ -161,7 +160,9 @@ public class Startup : AppStartup
 | 
			
		||||
        {
 | 
			
		||||
            options.WriteFilter = (logMsg) =>
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
                if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
 | 
			
		||||
                if (string.IsNullOrEmpty(logMsg.Message)) return false;
 | 
			
		||||
                else return true;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            options.MessageFormat = (logMsg) =>
 | 
			
		||||
@@ -211,39 +212,39 @@ public class Startup : AppStartup
 | 
			
		||||
        #region api日志
 | 
			
		||||
 | 
			
		||||
        //Monitor日志配置
 | 
			
		||||
        services.AddMonitorLogging(options =>
 | 
			
		||||
        {
 | 
			
		||||
            options.JsonIndented = true;// 是否美化 JSON
 | 
			
		||||
            options.GlobalEnabled = false;//全局启用
 | 
			
		||||
            options.ConfigureLogger((logger, logContext, context) =>
 | 
			
		||||
            {
 | 
			
		||||
                var httpContext = context.HttpContext;//获取httpContext
 | 
			
		||||
        //services.AddMonitorLogging(options =>
 | 
			
		||||
        //{
 | 
			
		||||
        //    options.JsonIndented = true;// 是否美化 JSON
 | 
			
		||||
        //    options.GlobalEnabled = false;//全局启用
 | 
			
		||||
        //    options.ConfigureLogger((logger, logContext, context) =>
 | 
			
		||||
        //    {
 | 
			
		||||
        //        var httpContext = context.HttpContext;//获取httpContext
 | 
			
		||||
 | 
			
		||||
                //获取客户端信息
 | 
			
		||||
                var client = App.GetService<IAppService>().ClientInfo;
 | 
			
		||||
                // 获取控制器/操作描述器
 | 
			
		||||
                var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
 | 
			
		||||
                //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
 | 
			
		||||
                var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
 | 
			
		||||
        //        //获取客户端信息
 | 
			
		||||
        //        var client = App.GetService<IAppService>().UserAgent;
 | 
			
		||||
        //        // 获取控制器/操作描述器
 | 
			
		||||
        //        var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
 | 
			
		||||
        //        //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
 | 
			
		||||
        //        var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
 | 
			
		||||
 | 
			
		||||
                var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
 | 
			
		||||
                //获取特性
 | 
			
		||||
                option = desc.Value;//则将操作名称赋值为控制器上写的title
 | 
			
		||||
        //        var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
 | 
			
		||||
        //        //获取特性
 | 
			
		||||
        //        option = desc.Value;//则将操作名称赋值为控制器上写的title
 | 
			
		||||
 | 
			
		||||
                logContext.Set(LoggingConst.CateGory, option);//传操作名称
 | 
			
		||||
                logContext.Set(LoggingConst.Operation, option);//传操作名称
 | 
			
		||||
                logContext.Set(LoggingConst.Client, client);//客户端信息
 | 
			
		||||
                logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
 | 
			
		||||
                logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        //        logContext.Set(LoggingConst.CateGory, option);//传操作名称
 | 
			
		||||
        //        logContext.Set(LoggingConst.Operation, option);//传操作名称
 | 
			
		||||
        //        logContext.Set(LoggingConst.Client, client);//客户端信息
 | 
			
		||||
        //        logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
 | 
			
		||||
        //        logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
 | 
			
		||||
        //    });
 | 
			
		||||
        //});
 | 
			
		||||
 | 
			
		||||
        //日志写入数据库配置
 | 
			
		||||
        services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
 | 
			
		||||
        {
 | 
			
		||||
            options.WriteFilter = (logMsg) =>
 | 
			
		||||
            {
 | 
			
		||||
                return logMsg.LogName == "System.Logging.LoggingMonitor";//只写入LoggingMonitor日志
 | 
			
		||||
                return logMsg.LogName == "System.Logging.RequestAudit";
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -302,7 +303,7 @@ public class Startup : AppStartup
 | 
			
		||||
        var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
 | 
			
		||||
#endif
 | 
			
		||||
        services.AddDataProtection()
 | 
			
		||||
            .PersistKeysToFileSystem(new DirectoryInfo("../keys"))
 | 
			
		||||
            .PersistKeysToFileSystem(new DirectoryInfo("keys"))
 | 
			
		||||
            .ProtectKeysWithCertificate(certificate)
 | 
			
		||||
            .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<!--安装服务守护-->
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
@@ -54,8 +54,8 @@
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -71,13 +71,25 @@ public static class App
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IServiceProvider RootServices => InternalApp.RootServices;
 | 
			
		||||
 | 
			
		||||
    private static IHostApplicationLifetime hostApplicationLifetime;
 | 
			
		||||
    public static IHostApplicationLifetime HostApplicationLifetime
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            if ((hostApplicationLifetime == null))
 | 
			
		||||
            {
 | 
			
		||||
                hostApplicationLifetime = RootServices?.GetService<IHostApplicationLifetime>();
 | 
			
		||||
            }
 | 
			
		||||
            return hostApplicationLifetime;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static IStringLocalizerFactory? stringLocalizerFactory;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 本地化服务工厂
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IStringLocalizerFactory? StringLocalizerFactory
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// HostApplication 拓展
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class AppHostApplicationBuilderExtensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Host 应用注入
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="hostApplicationBuilder">Host 应用构建器</param>
 | 
			
		||||
    /// <param name="autoRegisterBackgroundService"></param>
 | 
			
		||||
    /// <returns>HostApplicationBuilder</returns>
 | 
			
		||||
    public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
 | 
			
		||||
    {
 | 
			
		||||
        // 初始化配置
 | 
			
		||||
        InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService);
 | 
			
		||||
 | 
			
		||||
        return hostApplicationBuilder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 注册依赖组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
 | 
			
		||||
    /// <param name="hostApplicationBuilder">Host 应用构建器</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static HostApplicationBuilder AddComponent<TComponent>(this HostApplicationBuilder hostApplicationBuilder, object options = default)
 | 
			
		||||
        where TComponent : class, IServiceComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        hostApplicationBuilder.Services.AddComponent<TComponent>(options);
 | 
			
		||||
 | 
			
		||||
        return hostApplicationBuilder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 注册依赖组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
 | 
			
		||||
    /// <typeparam name="TComponentOptions">组件参数</typeparam>
 | 
			
		||||
    /// <param name="hostApplicationBuilder">Host 应用构建器</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns><see cref="HostApplicationBuilder"/></returns>
 | 
			
		||||
    public static HostApplicationBuilder AddComponent<TComponent, TComponentOptions>(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default)
 | 
			
		||||
        where TComponent : class, IServiceComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        hostApplicationBuilder.Services.AddComponent<TComponent, TComponentOptions>(options);
 | 
			
		||||
 | 
			
		||||
        return hostApplicationBuilder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 注册依赖组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="hostApplicationBuilder">Host 应用构建器</param>
 | 
			
		||||
    /// <param name="componentType">组件类型</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns><see cref="HostApplicationBuilder"/></returns>
 | 
			
		||||
    public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default)
 | 
			
		||||
    {
 | 
			
		||||
        hostApplicationBuilder.Services.AddComponent(componentType, options);
 | 
			
		||||
 | 
			
		||||
        return hostApplicationBuilder;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -467,18 +467,20 @@ public static class ObjectExtensions
 | 
			
		||||
        return obj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 查找方法指定特性,如果没找到则继续查找声明类
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TAttribute"></typeparam>
 | 
			
		||||
    /// <param name="method"></param>
 | 
			
		||||
    /// <param name="inherit"></param>
 | 
			
		||||
    /// <param name="searchFromReflectedType">searchFromRuntimeType</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit)
 | 
			
		||||
    internal static TAttribute GetFoundAttribute<TAttribute>(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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 格式化字符串
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,34 @@ internal static class InternalApp
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 Furion 框架(非 Web)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="hostApplicationBuilder"></param>
 | 
			
		||||
    /// <param name="autoRegisterBackgroundService"></param>
 | 
			
		||||
    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<GenericHostLifetimeEventsHostedService>();
 | 
			
		||||
 | 
			
		||||
        // 初始化应用服务
 | 
			
		||||
        hostApplicationBuilder.Services.AddApp();
 | 
			
		||||
 | 
			
		||||
        // 自动注册 BackgroundService
 | 
			
		||||
        if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService();
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自动装载主机配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns>加密后的字节数组</returns>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -176,25 +175,13 @@ public class AESEncryption
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 生成随机 IV
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private static byte[] GenerateRandomIV()
 | 
			
		||||
    {
 | 
			
		||||
        using var aes = Aes.Create();
 | 
			
		||||
        aes.GenerateIV();
 | 
			
		||||
        return aes.IV;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// GZip 压缩解压
 | 
			
		||||
/// </summary>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public static class GzipEncryption
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 压缩字符串并返回字节数组
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="text"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从字节数组解压
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="bytes"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 压缩字符串并返回 Base64 字符串
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="text"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从 Base64 字符串解压
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="base64String"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -77,10 +77,11 @@ public static class StringEncryptionExtensions
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns>string</returns>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -91,10 +92,11 @@ public static class StringEncryptionExtensions
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns>string</returns>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -105,10 +107,11 @@ public static class StringEncryptionExtensions
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns>string</returns>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -119,10 +122,11 @@ public static class StringEncryptionExtensions
 | 
			
		||||
    /// <param name="iv">偏移量</param>
 | 
			
		||||
    /// <param name="mode">模式</param>
 | 
			
		||||
    /// <param name="padding">填充</param>
 | 
			
		||||
    /// <param name="isBase64"></param>
 | 
			
		||||
    /// <returns>string</returns>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -243,4 +247,44 @@ public static class StringEncryptionExtensions
 | 
			
		||||
    {
 | 
			
		||||
        return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gzip 压缩字符串并返回字节数组
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="text"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static byte[] ToGzipCompress(this string text)
 | 
			
		||||
    {
 | 
			
		||||
        return GzipEncryption.Compress(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gzip 从字节数组解压
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="bytes"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToGzipDecompress(this byte[] bytes)
 | 
			
		||||
    {
 | 
			
		||||
        return GzipEncryption.Decompress(bytes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gzip 压缩字符串并返回 Base64 字符串
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="text"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToGzipCompressToBase64(this string text)
 | 
			
		||||
    {
 | 
			
		||||
        return GzipEncryption.CompressToBase64(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gzip 从 Base64 字符串解压
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="base64String"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToGzipDecompressFromBase64(this string base64String)
 | 
			
		||||
    {
 | 
			
		||||
        return GzipEncryption.DecompressFromBase64(base64String);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange
 | 
			
		||||
                if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            GC.Collect();
 | 
			
		||||
            GC.WaitForPendingFinalizers();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否启用执行完成触发 GC 回收
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool GCCollect { get; set; } = true;
 | 
			
		||||
    public bool GCCollect { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否启用日志记录
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.EventBus;
 | 
			
		||||
 | 
			
		||||
@@ -57,4 +58,31 @@ public abstract class EventHandlerContext
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks>
 | 
			
		||||
    public EventSubscribeAttribute Attribute { get; }
 | 
			
		||||
 | 
			
		||||
    private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default)
 | 
			
		||||
    {
 | 
			
		||||
        PropertyNameCaseInsensitive = true
 | 
			
		||||
    };
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取负载数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T"></typeparam>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public T GetPayload<T>()
 | 
			
		||||
    {
 | 
			
		||||
        var rawPayload = Source.Payload;
 | 
			
		||||
 | 
			
		||||
        if (rawPayload is null)
 | 
			
		||||
        {
 | 
			
		||||
            return default;
 | 
			
		||||
        }
 | 
			
		||||
        else if (rawPayload is JsonElement jsonElement)
 | 
			
		||||
        {
 | 
			
		||||
            return JsonSerializer.Deserialize<T>(jsonElement.GetRawText(), JsonSerializerOptions);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return (T)rawPayload;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext
 | 
			
		||||
    /// 执行前时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTime ExecutingTime { get; internal set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行结果
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal object Result { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置执行结果
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="result"></param>
 | 
			
		||||
    public void SetResult(object result)
 | 
			
		||||
    {
 | 
			
		||||
        Result = result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs
 | 
			
		||||
    /// 异常信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Exception Exception { get; internal set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行结果
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public object Result { get; internal set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -198,8 +198,9 @@ public class JWTEncryption
 | 
			
		||||
    /// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
 | 
			
		||||
    /// <param name="tokenPrefix"></param>
 | 
			
		||||
    /// <param name="clockSkew"></param>
 | 
			
		||||
    /// <param name="onRefreshing">当刷新时触发</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5)
 | 
			
		||||
    public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action<string, string> 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);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ public static class ILoggerExtensions
 | 
			
		||||
    /// <param name="logger"></param>
 | 
			
		||||
    /// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static IDisposable ScopeContext(this ILogger logger, IDictionary<object, object> properties)
 | 
			
		||||
    public static IDisposable ScopeContext(this ILogger logger, IDictionary<string, object> properties)
 | 
			
		||||
    {
 | 
			
		||||
        if (logger == null) throw new ArgumentNullException(nameof(logger));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,11 @@ public static class LogContextExtensions
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <param name="value">值</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static LogContext Set(this LogContext logContext, object key, object value)
 | 
			
		||||
    public static LogContext Set(this LogContext logContext, string key, object value)
 | 
			
		||||
    {
 | 
			
		||||
        if (logContext == null || key == null) return logContext;
 | 
			
		||||
 | 
			
		||||
        logContext.Properties ??= new Dictionary<object, object>();
 | 
			
		||||
        logContext.Properties ??= new Dictionary<string, object>();
 | 
			
		||||
 | 
			
		||||
        logContext.Properties.Remove(key);
 | 
			
		||||
        logContext.Properties.Add(key, value);
 | 
			
		||||
@@ -43,7 +43,7 @@ public static class LogContextExtensions
 | 
			
		||||
    /// <param name="logContext"></param>
 | 
			
		||||
    /// <param name="properties"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static LogContext SetRange(this LogContext logContext, IDictionary<object, object> properties)
 | 
			
		||||
    public static LogContext SetRange(this LogContext logContext, IDictionary<string, object> properties)
 | 
			
		||||
    {
 | 
			
		||||
        if (logContext == null
 | 
			
		||||
            || properties == null
 | 
			
		||||
@@ -63,7 +63,7 @@ public static class LogContextExtensions
 | 
			
		||||
    /// <param name="logContext"></param>
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static object Get(this LogContext logContext, object key)
 | 
			
		||||
    public static object Get(this LogContext logContext, string key)
 | 
			
		||||
    {
 | 
			
		||||
        if (logContext == null
 | 
			
		||||
            || key == null
 | 
			
		||||
@@ -80,7 +80,7 @@ public static class LogContextExtensions
 | 
			
		||||
    /// <param name="logContext"></param>
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static T Get<T>(this LogContext logContext, object key)
 | 
			
		||||
    public static T Get<T>(this LogContext logContext, string key)
 | 
			
		||||
    {
 | 
			
		||||
        var value = logContext.Get(key);
 | 
			
		||||
        return value.ChangeType<T>();
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ public static class StringLoggingExtensions
 | 
			
		||||
    /// <param name="message"></param>
 | 
			
		||||
    /// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static StringLoggingPart ScopeContext(this string message, IDictionary<object, object> properties)
 | 
			
		||||
    public static StringLoggingPart ScopeContext(this string message, IDictionary<string, object> properties)
 | 
			
		||||
    {
 | 
			
		||||
        return StringLoggingPart.Default().SetMessage(message).ScopeContext(properties);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable
 | 
			
		||||
               , true
 | 
			
		||||
               , _disableColors
 | 
			
		||||
               , _formatterOptions.WithTraceId
 | 
			
		||||
               , _formatterOptions.WithStackFrame);
 | 
			
		||||
               , _formatterOptions.WithStackFrame
 | 
			
		||||
               , _formatterOptions.FormatProvider);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 判断是否自定义了日志筛选器,如果是则检查是否符合条件
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Logging.Console;
 | 
			
		||||
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions
 | 
			
		||||
    /// 日志消息内容转换(如脱敏处理)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Func<string, string> MessageProcess { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 格式化提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks></remarks>
 | 
			
		||||
    public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
 | 
			
		||||
}
 | 
			
		||||
@@ -20,7 +20,7 @@ namespace ThingsGateway.Logging;
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public sealed class DatabaseLogger : ILogger
 | 
			
		||||
public sealed class DatabaseLogger : ILogger, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 记录器类别名称
 | 
			
		||||
@@ -60,6 +60,11 @@ public sealed class DatabaseLogger : ILogger
 | 
			
		||||
        return _databaseLoggerProvider.ScopeProvider?.Push(state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        _databaseLoggerProvider.RemoveCache(_logName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 检查是否已启用给定日志级别
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -118,7 +123,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions
 | 
			
		||||
    /// 日志消息内容转换(如脱敏处理)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Func<string, string> MessageProcess { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 格式化提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks></remarks>
 | 
			
		||||
    public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,8 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -54,6 +56,8 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
 | 
			
		||||
    /// <remarks>实现不间断写入</remarks>
 | 
			
		||||
    private Task _processQueueTask;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构造函数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -82,7 +86,10 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
 | 
			
		||||
    {
 | 
			
		||||
        return _databaseLoggers.GetOrAdd(categoryName, name => new DatabaseLogger(name, this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void RemoveCache(string categoryName)
 | 
			
		||||
    {
 | 
			
		||||
        _databaseLoggers.Remove(categoryName);
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置作用域提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,17 @@ namespace ThingsGateway.Logging;
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public sealed class EmptyLogger : ILogger
 | 
			
		||||
public sealed class EmptyLogger : ILogger, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    public EmptyLogger(string categoryName, EmptyLoggerProvider emptyLoggerProvider)
 | 
			
		||||
    {
 | 
			
		||||
        _logName = categoryName;
 | 
			
		||||
        _emptyLoggerProvider = emptyLoggerProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string _logName { get; }
 | 
			
		||||
    private EmptyLoggerProvider _emptyLoggerProvider { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 开始逻辑操作范围
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -31,6 +40,11 @@ public sealed class EmptyLogger : ILogger
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        _emptyLoggerProvider.RemoveCache(_logName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 检查是否已启用给定日志级别
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -34,9 +36,12 @@ public sealed class EmptyLoggerProvider : ILoggerProvider
 | 
			
		||||
    /// <returns><see cref="ILogger"/></returns>
 | 
			
		||||
    public ILogger CreateLogger(string categoryName)
 | 
			
		||||
    {
 | 
			
		||||
        return _emptyLoggers.GetOrAdd(categoryName, name => new EmptyLogger());
 | 
			
		||||
        return _emptyLoggers.GetOrAdd(categoryName, name => new EmptyLogger(categoryName, this));
 | 
			
		||||
    }
 | 
			
		||||
    public void RemoveCache(string categoryName)
 | 
			
		||||
    {
 | 
			
		||||
        _emptyLoggers.Remove(categoryName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放非托管资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace ThingsGateway.Logging;
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public sealed class FileLogger : ILogger
 | 
			
		||||
public sealed class FileLogger : ILogger, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 记录器类别名称
 | 
			
		||||
@@ -58,6 +58,11 @@ public sealed class FileLogger : ILogger
 | 
			
		||||
        return _fileLoggerProvider.ScopeProvider?.Push(state);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        _fileLoggerProvider.RemoveCache(_logName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 检查是否已启用给定日志级别
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -116,7 +121,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -104,4 +106,10 @@ public sealed class FileLoggerOptions
 | 
			
		||||
    /// 日志消息内容转换(如脱敏处理)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Func<string, string> MessageProcess { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 格式化提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks></remarks>
 | 
			
		||||
    public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension.Generic;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -116,6 +118,10 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
 | 
			
		||||
    {
 | 
			
		||||
        return _fileLoggers.GetOrAdd(categoryName, name => new FileLogger(name, this));
 | 
			
		||||
    }
 | 
			
		||||
    public void RemoveCache(string categoryName)
 | 
			
		||||
    {
 | 
			
		||||
        _fileLoggers.Remove(categoryName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置作用域提供器
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,10 @@ namespace ThingsGateway.Logging;
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public sealed class LogContext : IDisposable
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 日志上下文数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IDictionary<object, object> Properties { get; set; }
 | 
			
		||||
    public IDictionary<string, object> Properties { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 原生日志上下文数据
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Logging;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -120,6 +122,6 @@ public struct LogMessage
 | 
			
		||||
    /// <returns><see cref="string"/></returns>
 | 
			
		||||
    public override readonly string ToString()
 | 
			
		||||
    {
 | 
			
		||||
        return Penetrates.OutputStandardMessage(this);
 | 
			
		||||
        return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
 | 
			
		||||
    /// <param name="claimsPrincipal"></param>
 | 
			
		||||
    /// <param name="authorization"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private static List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
 | 
			
		||||
    private List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
 | 
			
		||||
    {
 | 
			
		||||
        var templates = new List<string>();
 | 
			
		||||
 | 
			
		||||
@@ -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)";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 格式化提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks></remarks>
 | 
			
		||||
    public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
 | 
			
		||||
}
 | 
			
		||||
@@ -107,13 +107,15 @@ internal static class Penetrates
 | 
			
		||||
    /// <param name="isConsole"></param>
 | 
			
		||||
    /// <param name="withTraceId"></param>
 | 
			
		||||
    /// <param name="withStackFrame"></param>
 | 
			
		||||
    /// <param name="provider"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    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(' ');
 | 
			
		||||
 
 | 
			
		||||
@@ -96,7 +96,7 @@ public sealed partial class StringLoggingPart
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public StringLoggingPart ScopeContext(IDictionary<object, object> properties)
 | 
			
		||||
    public StringLoggingPart ScopeContext(IDictionary<string, object> properties)
 | 
			
		||||
    {
 | 
			
		||||
        if (properties == null) return this;
 | 
			
		||||
        LogContext = new LogContext { Properties = properties };
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ public static class Log
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static (ILogger logger, IDisposable scope) ScopeContext(IDictionary<object, object> properties)
 | 
			
		||||
    public static (ILogger logger, IDisposable scope) ScopeContext(IDictionary<string, object> properties)
 | 
			
		||||
    {
 | 
			
		||||
        return GetLogger(StringLoggingPart.Default().ScopeContext(properties));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -78,9 +78,9 @@ public partial interface ISchedulerFactory
 | 
			
		||||
    /// <returns><see cref="IJob"/></returns>
 | 
			
		||||
    IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// GC 垃圾回收器回收处理
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>避免频繁 GC 回收</remarks>
 | 
			
		||||
    void GCCollect();
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// GC 垃圾回收器回收处理
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    ///// <remarks>避免频繁 GC 回收</remarks>
 | 
			
		||||
    //void GCCollect();
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// GC 垃圾回收器回收处理
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>避免频繁 GC 回收</remarks>
 | 
			
		||||
    public void GCCollect()
 | 
			
		||||
    {
 | 
			
		||||
        var nowTime = DateTime.UtcNow;
 | 
			
		||||
        if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
 | 
			
		||||
        {
 | 
			
		||||
            LastGCCollectTime = nowTime;
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// GC 垃圾回收器回收处理
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    ///// <remarks>避免频繁 GC 回收</remarks>
 | 
			
		||||
    //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();
 | 
			
		||||
    //    }
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放非托管资源
 | 
			
		||||
@@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
 | 
			
		||||
            //_logger.LogWarning("Schedule hosted service cancels hibernation.");
 | 
			
		||||
 | 
			
		||||
            // 通知 GC 垃圾回收器立即回收
 | 
			
		||||
            GCCollect();
 | 
			
		||||
            //GCCollect();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService
 | 
			
		||||
                            _jobCancellationToken.Cancel(jobId, triggerId, false);
 | 
			
		||||
 | 
			
		||||
                            // 通知 GC 垃圾回收器回收
 | 
			
		||||
                            _schedulerFactory.GCCollect();
 | 
			
		||||
                            //_schedulerFactory.GCCollect();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, stoppingToken);
 | 
			
		||||
                });
 | 
			
		||||
 
 | 
			
		||||
@@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口
 | 
			
		||||
        var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true);
 | 
			
		||||
 | 
			
		||||
        var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true);
 | 
			
		||||
 | 
			
		||||
        var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true, true);
 | 
			
		||||
        var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true, true);
 | 
			
		||||
        if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false;
 | 
			
		||||
 | 
			
		||||
        if (currentGroup == AllGroupsKey)
 | 
			
		||||
 
 | 
			
		||||
@@ -39,22 +39,22 @@
 | 
			
		||||
		<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
 | 
			
		||||
		<PackageReference Include="Mapster" Version="7.4.0" />
 | 
			
		||||
		<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
 | 
			
		||||
		<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
 | 
			
		||||
		<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.16" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="8.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="9.0.5" />
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<ITimeParser>().ToList();
 | 
			
		||||
        randomMinute = minuteParsers.OfType<RandomParser>().Any();
 | 
			
		||||
        var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
 | 
			
		||||
        randomHour = hourParsers.OfType<RandomParser>().Any();
 | 
			
		||||
 | 
			
		||||
        // 获取秒、分钟、小时解析器中最小起始值
 | 
			
		||||
        // 该值主要用来获取下一个发生值的输入参数
 | 
			
		||||
@@ -456,7 +461,7 @@ public partial class Crontab
 | 
			
		||||
        {
 | 
			
		||||
            // 获取秒所有字符解析器
 | 
			
		||||
            var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
 | 
			
		||||
 | 
			
		||||
            randomSecond = secondParsers.OfType<RandomParser>().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
 | 
			
		||||
    /// <param name="defaultValue">默认值</param>
 | 
			
		||||
    /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
 | 
			
		||||
    /// <returns><see cref="int"/></returns>
 | 
			
		||||
    private static int Increment(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
 | 
			
		||||
    private static int Increment(List<ITimeParser> 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
 | 
			
		||||
    /// <param name="defaultValue">默认值</param>
 | 
			
		||||
    /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
 | 
			
		||||
    /// <returns><see cref="int"/></returns>
 | 
			
		||||
    private static int Decrement(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
 | 
			
		||||
    private static int Decrement(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
 | 
			
		||||
    {
 | 
			
		||||
        var previousValue = parsers.Select(x => x.Previous(value))
 | 
			
		||||
            .Where(x => x < value)
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser
 | 
			
		||||
    /// <returns><see cref="bool"/></returns>
 | 
			
		||||
    public bool IsMatch(DateTime datetime)
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
        return Kind is not CrontabFieldKind.Hour;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -168,7 +168,7 @@ public static class UnifyContext
 | 
			
		||||
        if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null;
 | 
			
		||||
 | 
			
		||||
        // 获取序列化配置
 | 
			
		||||
        var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true);
 | 
			
		||||
        var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(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;
 | 
			
		||||
@@ -347,7 +349,7 @@ public static class UnifyContext
 | 
			
		||||
    /// <param name="result"></param>
 | 
			
		||||
    /// <param name="data"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    internal static bool CheckVaildResult(IActionResult result, out object data)
 | 
			
		||||
    public static bool CheckVaildResult(IActionResult result, out object data)
 | 
			
		||||
    {
 | 
			
		||||
        data = default;
 | 
			
		||||
 | 
			
		||||
@@ -398,7 +400,7 @@ public static class UnifyContext
 | 
			
		||||
    {
 | 
			
		||||
        if (method == default) return default;
 | 
			
		||||
 | 
			
		||||
        var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true);
 | 
			
		||||
        var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true, true);
 | 
			
		||||
 | 
			
		||||
        // 获取元数据
 | 
			
		||||
        var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     <see cref="DateTime" /> JSON 序列化转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTime" /> 时间使用 <c>DateTime.Parse</c> 作为回退。</remarks>
 | 
			
		||||
public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter<DateTime>
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) =>
 | 
			
		||||
        JsonSerializer.Serialize(writer, value);
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     <see cref="DateTimeOffset" /> JSON 序列化转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTimeOffset" /> 时间使用 <c>DateTimeOffset.Parse</c> 作为回退。</remarks>
 | 
			
		||||
public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter<DateTimeOffset>
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
 | 
			
		||||
        JsonSerializer.Serialize(writer, value);
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     <see cref="string" /> JSON 序列化转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <remarks>解决 Number 类型和 Boolean 类型转 String 类型时异常。</remarks>
 | 
			
		||||
public sealed class StringJsonConverter : JsonConverter<string>
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    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()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
 | 
			
		||||
        writer.WriteStringValue(value);
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Extensions;
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     解析表达式属性名称
 | 
			
		||||
    ///     解析表达式并获取属性的 <see cref="PropertyInfo" /> 实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T">对象类型</typeparam>
 | 
			
		||||
    /// <typeparam name="TProperty">属性类型</typeparam>
 | 
			
		||||
@@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions
 | 
			
		||||
    ///     <see cref="Expression{TDelegate}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="string" />
 | 
			
		||||
    ///     <see cref="PropertyInfo" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    /// <exception cref="ArgumentException"></exception>
 | 
			
		||||
    internal static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
 | 
			
		||||
    internal static PropertyInfo GetProperty<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
 | 
			
		||||
        propertySelector.Body switch
 | 
			
		||||
        {
 | 
			
		||||
            // 检查 Lambda 表达式的主体是否是 MemberExpression 类型
 | 
			
		||||
            MemberExpression memberExpression => GetPropertyName<T>(memberExpression),
 | 
			
		||||
 | 
			
		||||
            MemberExpression memberExpression => GetProperty<T>(memberExpression),
 | 
			
		||||
            // 如果主体是 UnaryExpression 类型,则继续解析
 | 
			
		||||
            UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName<T>(
 | 
			
		||||
            UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty<T>(
 | 
			
		||||
                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))
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     解析表达式属性名称
 | 
			
		||||
    ///     从成员表达式中提取 <see cref="PropertyInfo" /> 实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T">对象类型</typeparam>
 | 
			
		||||
    /// <param name="memberExpression">
 | 
			
		||||
    ///     <see cref="MemberExpression" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <typeparam name="T">对象类型</typeparam>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="string" />
 | 
			
		||||
    ///     <see cref="PropertyInfo" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    /// <exception cref="ArgumentException"></exception>
 | 
			
		||||
    internal static string GetPropertyName<T>(MemberExpression memberExpression)
 | 
			
		||||
    internal static PropertyInfo GetProperty<T>(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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList();
 | 
			
		||||
@@ -328,6 +329,18 @@ internal static partial class StringExtensions
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     转换输入字符串中的任何转义字符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="input">
 | 
			
		||||
    ///     <see cref="string" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="string" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
 | 
			
		||||
        string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     占位符匹配正则表达式
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     从 <see cref="Utf8JsonReader" /> 中提取原始值,并将其转换为字符串
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>支持处理各种类型的原始值(例如数字、布尔值等)。</remarks>
 | 
			
		||||
    /// <param name="reader">
 | 
			
		||||
    ///     <see cref="Utf8JsonReader" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="string" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    internal static string ConvertRawValueToString(this Utf8JsonReader reader) =>
 | 
			
		||||
        Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     忽略在转发时需要跳过的请求标头列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal static HashSet<string> _ignoreRequestHeaders =
 | 
			
		||||
    internal static readonly HashSet<string> _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
 | 
			
		||||
    /// <param name="httpMultipartFormDataBuilder">
 | 
			
		||||
    ///     <see cref="HttpMultipartFormDataBuilder" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="httpRequestBuilder">
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="cancellationToken">
 | 
			
		||||
    ///     <see cref="CancellationToken" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder
 | 
			
		||||
    ///     <see cref="HttpMultipartFormDataBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    /// <exception cref="JsonException"></exception>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder
 | 
			
		||||
    /// <param name="fileName">文件的名称</param>
 | 
			
		||||
    /// <param name="contentType">内容类型</param>
 | 
			
		||||
    /// <param name="contentEncoding">内容编码</param>
 | 
			
		||||
    /// <param name="disposeStreamOnRequestCompletion">是否在请求结束后自动释放流。默认值为:<c>false</c></param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpMultipartFormDataBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置是否移除默认的多部分内容的 <c>Content-Type</c>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpMultipartFormDataBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
 | 
			
		||||
    {
 | 
			
		||||
        OmitContentType = omit;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     构建 <see cref="MultipartFormDataContent" /> 实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <param name="value">值</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? 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<string, object?> { { key, value } }, escape, culture, comparer, replace);
 | 
			
		||||
        return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, replace, culture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <remarks>支持多次调用。</remarks>
 | 
			
		||||
    /// <param name="headers">请求标头集合</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false)
 | 
			
		||||
        bool replace = false, CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(headers);
 | 
			
		||||
 | 
			
		||||
        // 初始化请求标头
 | 
			
		||||
        Headers ??= new Dictionary<string, List<string?>>(comparer);
 | 
			
		||||
        var objectHeaders = new Dictionary<string, List<object?>>(comparer);
 | 
			
		||||
        Headers ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
        var objectHeaders = new Dictionary<string, List<object?>>(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
 | 
			
		||||
    /// <remarks>支持多次调用。</remarks>
 | 
			
		||||
    /// <param name="headerSource">请求标头源对象</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置超时时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="timeout">超时时间</param>
 | 
			
		||||
    /// <param name="onTimeout">超时发生时要执行的操作</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(onTimeout);
 | 
			
		||||
 | 
			
		||||
        SetTimeout(timeout).TimeoutAction = onTimeout;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置超时时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="timeoutMilliseconds">超时时间(毫秒)</param>
 | 
			
		||||
    /// <param name="onTimeout">超时发生时要执行的操作</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    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
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <param name="value">值</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null, IEqualityComparer<string>? 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<string, object?> { { key, value } }, escape, culture, comparer,
 | 
			
		||||
            replace, ignoreNullValues);
 | 
			
		||||
        return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, replace,
 | 
			
		||||
            ignoreNullValues, culture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <remarks>支持多次调用。</remarks>
 | 
			
		||||
    /// <param name="parameters">查询参数集合</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false,
 | 
			
		||||
        bool ignoreNullValues = false)
 | 
			
		||||
        bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(parameters);
 | 
			
		||||
 | 
			
		||||
        // 初始化查询参数
 | 
			
		||||
        QueryParameters ??= new Dictionary<string, List<string?>>(comparer);
 | 
			
		||||
        var objectQueryParameters = new Dictionary<string, List<object?>>(comparer);
 | 
			
		||||
        QueryParameters ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
        var objectQueryParameters = new Dictionary<string, List<object?>>(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
 | 
			
		||||
    /// <param name="parameterSource">查询参数集合</param>
 | 
			
		||||
    /// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param>
 | 
			
		||||
    /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null, IEqualityComparer<string>? 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null, IEqualityComparer<string>? comparer = null)
 | 
			
		||||
        CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
 | 
			
		||||
 | 
			
		||||
        return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer);
 | 
			
		||||
        return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters,
 | 
			
		||||
        bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? comparer = null)
 | 
			
		||||
    public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(parameters);
 | 
			
		||||
 | 
			
		||||
        PathParameters ??= new Dictionary<string, string?>(comparer);
 | 
			
		||||
        PathParameters ??= new Dictionary<string, string?>(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
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? 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<string, object?>();
 | 
			
		||||
@@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? comparer = null)
 | 
			
		||||
    public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
 | 
			
		||||
 | 
			
		||||
        return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer);
 | 
			
		||||
        return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies,
 | 
			
		||||
        bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? comparer = null)
 | 
			
		||||
    public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(cookies);
 | 
			
		||||
 | 
			
		||||
        Cookies ??= new Dictionary<string, string?>(comparer);
 | 
			
		||||
        Cookies ??= new Dictionary<string, string?>(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
 | 
			
		||||
    /// <param name="culture">
 | 
			
		||||
    ///     <see cref="CultureInfo" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="comparer">
 | 
			
		||||
    ///     <see cref="IEqualityComparer{T}" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false,
 | 
			
		||||
        CultureInfo? culture = null,
 | 
			
		||||
        IEqualityComparer<string>? 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置身份验证凭据请求授权标头
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="scheme">身份验证的方案</param>
 | 
			
		||||
    /// <param name="parameter">身份验证的凭证</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) =>
 | 
			
		||||
        AddAuthentication(new AuthenticationHeaderValue(scheme, parameter));
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置身份验证凭据请求授权标头
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
        ReleaseDisposables();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置请求来源地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>设置此配置后,将在单次请求标头中添加 <c>Referer</c> 标头。</remarks>
 | 
			
		||||
    /// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SetReferer(string? referer) =>
 | 
			
		||||
        WithHeader(HeaderNames.Referer, referer, replace: true);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置模拟浏览器环境
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
 | 
			
		||||
        WithStatusCodeHandler(["*"], handler);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     添加请求成功(200-299)状态码处理程序
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="handler">自定义处理程序</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder
 | 
			
		||||
        WithSuccessStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
 | 
			
		||||
        WithStatusCodeHandler("200-299", handler);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     添加状态码处理程序
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
            ? null
 | 
			
		||||
            : new Uri(baseAddress, UriKind.RelativeOrAbsolute));
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置 HTTP 版本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="version">版本号</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SetVersion(string? version) =>
 | 
			
		||||
        SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version));
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置 HTTP 版本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="version">
 | 
			
		||||
    ///     <see cref="Version" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SetVersion(Version? version)
 | 
			
		||||
    {
 | 
			
		||||
        Version = version;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置异常抑制
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>抑制所有异常。重复调用仅最后一次调用生效。</remarks>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置异常抑制
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>重复调用仅最后一次调用生效。</remarks>
 | 
			
		||||
    /// <param name="enable">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置是否移除默认的内容的 <c>Content-Type</c>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public HttpRequestBuilder SetOmitContentType(bool omit)
 | 
			
		||||
    {
 | 
			
		||||
        OmitContentType = omit;
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     设置异常抑制
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>重复调用仅最后一次调用生效。</remarks>
 | 
			
		||||
    /// <param name="exceptionTypes">异常抑制类型集合</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    /// <exception cref="ArgumentException"></exception>
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     释放可释放的对象集合
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Uri? BaseAddress { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     HTTP 版本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Version? Version { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     <see cref="HttpClient" /> 实例提供器
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public Action<HttpContent?>? OnPreSetContent { get; private set; }
 | 
			
		||||
    public Action<HttpContent>? OnPreSetContent { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     用于处理在发送 HTTP 请求之前的操作
 | 
			
		||||
@@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     <inheritdoc cref="HttpMultipartFormDataBuilder" />
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
 | 
			
		||||
    public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     是否移除默认的内容的 <c>Content-Type</c>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>默认值为:<c>false</c>。</remarks>
 | 
			
		||||
    public bool OmitContentType { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。
 | 
			
		||||
@@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
        get;
 | 
			
		||||
        private set;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     异常抑制类型集合
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。</remarks>
 | 
			
		||||
    internal HashSet<Type>? SuppressExceptionTypes { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     超时发生时要执行的操作
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action? TimeoutAction { get; private set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) =>
 | 
			
		||||
        new(method, args);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     从 JSON 中创建 <see cref="HttpRequestBuilder" /> 实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="json">JSON 字符串</param>
 | 
			
		||||
    /// <param name="configure">自定义配置委托</param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="HttpRequestBuilder" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    /// <exception cref="ArgumentException"></exception>
 | 
			
		||||
    /// <exception cref="InvalidOperationException"></exception>
 | 
			
		||||
    public static HttpRequestBuilder FromJson(string json, Action<HttpRequestBuilder>? 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<string?>());
 | 
			
		||||
 | 
			
		||||
        // 处理可选字段
 | 
			
		||||
        HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue<string>()));
 | 
			
		||||
        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<double>()));
 | 
			
		||||
        HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue<string?>()));
 | 
			
		||||
        HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue<bool>()));
 | 
			
		||||
 | 
			
		||||
        // 处理请求内容
 | 
			
		||||
        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<string>()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 处理多部分表单
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     处理 <see cref="JsonNode" />
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jsonObject">
 | 
			
		||||
    ///     <see cref="JsonObject" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <param name="propertyName">属性名</param>
 | 
			
		||||
    /// <param name="action">自定义操作</param>
 | 
			
		||||
    internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action<JsonNode> action)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(jsonObject);
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(propertyName);
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(action);
 | 
			
		||||
 | 
			
		||||
        if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null)
 | 
			
		||||
        {
 | 
			
		||||
            action(node);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    /// </param>
 | 
			
		||||
    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 = "/";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder
 | 
			
		||||
    /// </param>
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -88,15 +88,26 @@ internal static class Constants
 | 
			
		||||
    /// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
 | 
			
		||||
    internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     HTTP 请求 <see cref="HttpClient" /> 实例的配置名称键
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
 | 
			
		||||
    internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     浏览器的 <c>User-Agent</c> 标头值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    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";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     移动端浏览器的 <c>User-Agent</c> 标头值
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    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";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     <c>Referer</c> 标头请求基地址模板
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}";
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) =>
 | 
			
		||||
        httpResponseMessage.Content.ReadFromJsonAsync(resultType,
 | 
			
		||||
            ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
 | 
			
		||||
            HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult();
 | 
			
		||||
        httpResponseMessage.Content
 | 
			
		||||
            .ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken)
 | 
			
		||||
            .GetAwaiter().GetResult();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) =>
 | 
			
		||||
        await httpResponseMessage.Content.ReadFromJsonAsync(resultType,
 | 
			
		||||
            ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
 | 
			
		||||
            HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await httpResponseMessage.Content.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage),
 | 
			
		||||
            cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     获取 JSON 序列化选项实例
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="httpResponseMessage">
 | 
			
		||||
    ///     <see cref="HttpResponseMessage" />
 | 
			
		||||
    /// </param>
 | 
			
		||||
    /// <returns>
 | 
			
		||||
    ///     <see cref="JsonSerializerOptions" />
 | 
			
		||||
    /// </returns>
 | 
			
		||||
    protected virtual JsonSerializerOptions GetJsonSerializerOptions(HttpResponseMessage httpResponseMessage)
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(httpResponseMessage);
 | 
			
		||||
 | 
			
		||||
        // 获取 HttpClient 实例的配置名称
 | 
			
		||||
        if (httpResponseMessage.RequestMessage?.Options.TryGetValue(
 | 
			
		||||
                new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME), out var httpClientName) != true)
 | 
			
		||||
        {
 | 
			
		||||
            httpClientName = string.Empty;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 获取 HttpClientOptions 实例
 | 
			
		||||
        var httpClientOptions = ServiceProvider?.GetService<IOptionsMonitor<HttpClientOptions>>()?.Get(httpClientName);
 | 
			
		||||
 | 
			
		||||
        // 优先级:指定名称的 HttpClientOptions -> HttpRemoteOptions -> 默认值
 | 
			
		||||
        return (httpClientOptions?.IsDefault != false ? null : httpClientOptions.JsonSerializerOptions) ??
 | 
			
		||||
               ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
 | 
			
		||||
               HttpRemoteOptions.JsonSerializerOptionsDefault;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -48,14 +78,13 @@ public class ObjectContentConverter<TResult> : ObjectContentConverter, IHttpCont
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public virtual TResult? Read(HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) =>
 | 
			
		||||
        httpResponseMessage.Content.ReadFromJsonAsync<TResult>(
 | 
			
		||||
            ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
 | 
			
		||||
            HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult();
 | 
			
		||||
        httpResponseMessage.Content
 | 
			
		||||
            .ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage), cancellationToken).GetAwaiter()
 | 
			
		||||
            .GetResult();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) =>
 | 
			
		||||
        await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(
 | 
			
		||||
            ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
 | 
			
		||||
            HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage),
 | 
			
		||||
            cancellationToken).ConfigureAwait(false);
 | 
			
		||||
}
 | 
			
		||||
@@ -18,10 +18,10 @@ public class VoidContentConverter : HttpContentConverterBase<VoidContent>
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public override VoidContent? Read(HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) => default;
 | 
			
		||||
        CancellationToken cancellationToken = default) => null;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage,
 | 
			
		||||
        CancellationToken cancellationToken = default) =>
 | 
			
		||||
        Task.FromResult<VoidContent?>(default);
 | 
			
		||||
        Task.FromResult<VoidContent?>(null);
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user