Compare commits
	
		
			50 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					bef9de88e2 | ||
| 
						 | 
					48cd5e7c7f | ||
| 
						 | 
					3b44fda51c | ||
| 
						 | 
					dbfc9a5bb4 | ||
| 
						 | 
					1b758aa41a | ||
| 
						 | 
					43bdc70899 | ||
| 
						 | 
					fadda000a6 | ||
| 
						 | 
					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 | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -365,4 +365,5 @@ FodyWeavers.xsd
 | 
			
		||||
/src/*Pro*/
 | 
			
		||||
/src/*Pro*
 | 
			
		||||
/src/*pro*
 | 
			
		||||
/src/*pro*/
 | 
			
		||||
/src/*pro*/
 | 
			
		||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
@@ -15,7 +16,7 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
[ApiDescriptionSettings(false)]
 | 
			
		||||
[Route("api/auth")]
 | 
			
		||||
[LoggingMonitor]
 | 
			
		||||
[RequestAudit]
 | 
			
		||||
public class AuthController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private readonly IAuthService _authService;
 | 
			
		||||
@@ -29,9 +30,23 @@ public class AuthController : ControllerBase
 | 
			
		||||
    [AllowAnonymous]
 | 
			
		||||
    public Task<LoginOutput> LoginAsync([FromBody] LoginInput input)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        return _authService.LoginAsync(input);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("oauth-login")]
 | 
			
		||||
    [AllowAnonymous]
 | 
			
		||||
    public IActionResult OAuthLogin(string scheme = "Gitee", string returnUrl = "/")
 | 
			
		||||
    {
 | 
			
		||||
        var props = new AuthenticationProperties
 | 
			
		||||
        {
 | 
			
		||||
            RedirectUri = returnUrl
 | 
			
		||||
        };
 | 
			
		||||
        return Challenge(props, scheme);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [HttpPost("logout")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [IgnoreRolePermission]
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,212 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication.OAuth;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Encodings.Web;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 只适合 Demo 登录,会直接授权超管的权限
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class AdminOAuthHandler<TOptions>(
 | 
			
		||||
   IVerificatInfoService verificatInfoService,
 | 
			
		||||
   IAppService appService,
 | 
			
		||||
   ISysUserService sysUserService,
 | 
			
		||||
   ISysDictService configService,
 | 
			
		||||
    IOptionsMonitor<TOptions> options,
 | 
			
		||||
    ILoggerFactory logger,
 | 
			
		||||
    UrlEncoder encoder
 | 
			
		||||
) : OAuthHandler<TOptions>(options, logger, encoder)
 | 
			
		||||
    where TOptions : AdminOAuthOptions, new()
 | 
			
		||||
{
 | 
			
		||||
    private async Task<LoginEvent> GetLogin()
 | 
			
		||||
    {
 | 
			
		||||
        var sysUser = await sysUserService.GetUserByIdAsync(RoleConst.SuperAdminId).ConfigureAwait(false);//获取用户信息
 | 
			
		||||
 | 
			
		||||
        var appConfig = await configService.GetAppConfigAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var expire = appConfig.LoginPolicy.VerificatExpireTime;
 | 
			
		||||
 | 
			
		||||
        var loginEvent = new LoginEvent
 | 
			
		||||
        {
 | 
			
		||||
            Ip = appService.RemoteIpAddress,
 | 
			
		||||
            Device = appService.UserAgent?.Platform,
 | 
			
		||||
            Expire = expire,
 | 
			
		||||
            SysUser = sysUser,
 | 
			
		||||
            VerificatId = CommonUtils.GetSingleId()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        //获取verificat列表
 | 
			
		||||
        var tokenTimeout = loginEvent.DateTime.AddMinutes(loginEvent.Expire);
 | 
			
		||||
        //生成verificat信息
 | 
			
		||||
        var verificatInfo = new VerificatInfo
 | 
			
		||||
        {
 | 
			
		||||
            Device = loginEvent.Device,
 | 
			
		||||
            Expire = loginEvent.Expire,
 | 
			
		||||
            VerificatTimeout = tokenTimeout,
 | 
			
		||||
            Id = loginEvent.VerificatId,
 | 
			
		||||
            UserId = loginEvent.SysUser.Id,
 | 
			
		||||
            LoginIp = loginEvent.Ip,
 | 
			
		||||
            LoginTime = loginEvent.DateTime
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //添加到verificat列表
 | 
			
		||||
        verificatInfoService.Add(verificatInfo);
 | 
			
		||||
 | 
			
		||||
        return loginEvent;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 登录事件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="loginEvent"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private async Task UpdateUser(LoginEvent loginEvent)
 | 
			
		||||
    {
 | 
			
		||||
        var sysUser = loginEvent.SysUser;
 | 
			
		||||
 | 
			
		||||
        #region 登录/密码策略
 | 
			
		||||
 | 
			
		||||
        var key = CacheConst.Cache_LoginErrorCount + sysUser.Account;//获取登录错误次数Key值
 | 
			
		||||
        App.CacheService.Remove(key);//移除登录错误次数
 | 
			
		||||
 | 
			
		||||
        //获取用户verificat列表
 | 
			
		||||
        var userToken = verificatInfoService.GetOne(loginEvent.VerificatId);
 | 
			
		||||
 | 
			
		||||
        #endregion 登录/密码策略
 | 
			
		||||
 | 
			
		||||
        #region 重新赋值属性,设置本次登录信息为最新的信息
 | 
			
		||||
 | 
			
		||||
        sysUser.LastLoginIp = sysUser.LatestLoginIp;
 | 
			
		||||
        sysUser.LastLoginTime = sysUser.LatestLoginTime;
 | 
			
		||||
        sysUser.LatestLoginIp = loginEvent.Ip;
 | 
			
		||||
        sysUser.LatestLoginTime = loginEvent.DateTime;
 | 
			
		||||
 | 
			
		||||
        #endregion 重新赋值属性,设置本次登录信息为最新的信息
 | 
			
		||||
 | 
			
		||||
        using var db = DbContext.Db.GetConnectionScopeWithAttr<SysUser>().CopyNew();
 | 
			
		||||
        //更新用户登录信息
 | 
			
		||||
        if (await db.Updateable(sysUser).UpdateColumns(it => new
 | 
			
		||||
        {
 | 
			
		||||
            it.LastLoginIp,
 | 
			
		||||
            it.LastLoginTime,
 | 
			
		||||
            it.LatestLoginIp,
 | 
			
		||||
            it.LatestLoginTime,
 | 
			
		||||
        }).ExecuteCommandAsync().ConfigureAwait(false) > 0)
 | 
			
		||||
            App.CacheService.HashAdd(CacheConst.Cache_SysUser, sysUser.Id.ToString(), sysUser);//更新Cache信息
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override async Task<AuthenticationTicket> CreateTicketAsync(
 | 
			
		||||
        ClaimsIdentity identity,
 | 
			
		||||
        AuthenticationProperties properties,
 | 
			
		||||
        OAuthTokenResponse tokens)
 | 
			
		||||
    {
 | 
			
		||||
        properties.RedirectUri = Options.HomePath;
 | 
			
		||||
        properties.IsPersistent = true;
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrEmpty(tokens.ExpiresIn) && int.TryParse(tokens.ExpiresIn, out var result))
 | 
			
		||||
        {
 | 
			
		||||
            properties.ExpiresUtc = TimeProvider.System.GetUtcNow().AddSeconds(result);
 | 
			
		||||
        }
 | 
			
		||||
        var user = await HandleUserInfoAsync(tokens).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var sysUser = await GetLogin().ConfigureAwait(false);
 | 
			
		||||
        await UpdateUser(sysUser).ConfigureAwait(false);
 | 
			
		||||
        identity.AddClaim(new Claim(ClaimConst.VerificatId, sysUser.VerificatId.ToString()));
 | 
			
		||||
        identity.AddClaim(new Claim(ClaimConst.UserId, RoleConst.SuperAdminId.ToString()));
 | 
			
		||||
 | 
			
		||||
        identity.AddClaim(new Claim(ClaimConst.SuperAdmin, "true"));
 | 
			
		||||
        identity.AddClaim(new Claim(ClaimConst.OrgId, RoleConst.DefaultTenantId.ToString()));
 | 
			
		||||
        identity.AddClaim(new Claim(ClaimConst.TenantId, RoleConst.DefaultTenantId.ToString()));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var context = new OAuthCreatingTicketContext(
 | 
			
		||||
            new ClaimsPrincipal(identity),
 | 
			
		||||
            properties,
 | 
			
		||||
            Context,
 | 
			
		||||
            Scheme,
 | 
			
		||||
            Options,
 | 
			
		||||
            Backchannel,
 | 
			
		||||
            tokens,
 | 
			
		||||
            user
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        context.RunClaimActions();
 | 
			
		||||
        await Events.CreatingTicket(context).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>刷新 Token 方法</summary>
 | 
			
		||||
    protected virtual async Task<OAuthTokenResponse> RefreshTokenAsync(OAuthTokenResponse oAuthToken)
 | 
			
		||||
    {
 | 
			
		||||
        var query = new Dictionary<string, string>
 | 
			
		||||
        {
 | 
			
		||||
            { "refresh_token", oAuthToken.RefreshToken },
 | 
			
		||||
            { "grant_type", "refresh_token" }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var request = new HttpRequestMessage(HttpMethod.Post, QueryHelpers.AddQueryString(Options.TokenEndpoint, query));
 | 
			
		||||
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 | 
			
		||||
 | 
			
		||||
        var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (response.IsSuccessStatusCode)
 | 
			
		||||
        {
 | 
			
		||||
            return OAuthTokenResponse.Success(JsonDocument.Parse(content));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return OAuthTokenResponse.Failed(new OAuthTokenException($"OAuth token endpoint failure: {await Display(response).ConfigureAwait(false)}"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>处理用户信息方法</summary>
 | 
			
		||||
    protected virtual async Task<JsonElement> HandleUserInfoAsync(OAuthTokenResponse tokens)
 | 
			
		||||
    {
 | 
			
		||||
        var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens));
 | 
			
		||||
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 | 
			
		||||
 | 
			
		||||
        var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (response.IsSuccessStatusCode)
 | 
			
		||||
        {
 | 
			
		||||
            return JsonDocument.Parse(content).RootElement;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>生成用户信息请求地址方法</summary>
 | 
			
		||||
    protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens)
 | 
			
		||||
    {
 | 
			
		||||
        return QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string>
 | 
			
		||||
        {
 | 
			
		||||
            { "access_token", tokens.AccessToken }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>生成错误信息方法</summary>
 | 
			
		||||
    protected static async Task<string> Display(HttpResponseMessage response)
 | 
			
		||||
    {
 | 
			
		||||
        var output = new StringBuilder();
 | 
			
		||||
        output.Append($"Status: {response.StatusCode}; ");
 | 
			
		||||
        output.Append($"Headers: {response.Headers}; ");
 | 
			
		||||
        output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
 | 
			
		||||
 | 
			
		||||
        return output.ToString();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>自定义 Token 异常</summary>
 | 
			
		||||
public class OAuthTokenException(string message) : Exception(message);
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication.OAuth;
 | 
			
		||||
using Microsoft.AspNetCore.Identity;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>OAuthOptions 配置类</summary>
 | 
			
		||||
public abstract class AdminOAuthOptions : OAuthOptions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>默认构造函数</summary>
 | 
			
		||||
    protected AdminOAuthOptions()
 | 
			
		||||
    {
 | 
			
		||||
        ConfigureClaims();
 | 
			
		||||
        this.Events.OnRemoteFailure = context =>
 | 
			
		||||
        {
 | 
			
		||||
            var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath;
 | 
			
		||||
            context.Response.Redirect(redirectUri);
 | 
			
		||||
            context.HandleResponse();
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>配置 Claims 映射</summary>
 | 
			
		||||
    protected virtual void ConfigureClaims()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>获得/设置 登陆后首页</summary>
 | 
			
		||||
    public string HomePath { get; set; } = "/";
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
using Microsoft.AspNetCore.Authentication;
 | 
			
		||||
using Microsoft.AspNetCore.Authentication.OAuth;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class GiteeOAuthOptions : AdminOAuthOptions
 | 
			
		||||
{
 | 
			
		||||
    public GiteeOAuthOptions() : base()
 | 
			
		||||
    {
 | 
			
		||||
        this.SignInScheme = ClaimConst.Scheme;
 | 
			
		||||
        this.AuthorizationEndpoint = "https://gitee.com/oauth/authorize";
 | 
			
		||||
        this.TokenEndpoint = "https://gitee.com/oauth/token";
 | 
			
		||||
        this.UserInformationEndpoint = "https://gitee.com/api/v5/user";
 | 
			
		||||
        this.HomePath = "/";
 | 
			
		||||
        this.CallbackPath = "/signin-gitee";
 | 
			
		||||
        Scope.Add("user_info");
 | 
			
		||||
        Scope.Add("projects");
 | 
			
		||||
 | 
			
		||||
        Events.OnCreatingTicket = async context =>
 | 
			
		||||
        {
 | 
			
		||||
            await HandlerGiteeStarredUrl(context).ConfigureAwait(false);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Events.OnRedirectToAuthorizationEndpoint = context =>
 | 
			
		||||
        {
 | 
			
		||||
            //context.RedirectUri = context.RedirectUri.Replace("http%3A%2F%2F", "https%3A%2F%2F"); // 强制替换
 | 
			
		||||
            context.Response.Redirect(context.RedirectUri);
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    private static async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(context.AccessToken))
 | 
			
		||||
            throw new InvalidOperationException("Access token is missing.");
 | 
			
		||||
 | 
			
		||||
        var uri = $"https://gitee.com/api/v5/user/starred/{repoFullName}";
 | 
			
		||||
 | 
			
		||||
        var queryString = new Dictionary<string, string>
 | 
			
		||||
        {
 | 
			
		||||
            { "access_token", context.AccessToken }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var request = new HttpRequestMessage(HttpMethod.Put, QueryHelpers.AddQueryString(uri, queryString))
 | 
			
		||||
        {
 | 
			
		||||
            Headers = { Accept = { new MediaTypeWithQualityHeaderValue("application/json") } }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (!response.IsSuccessStatusCode)
 | 
			
		||||
        {
 | 
			
		||||
            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
            throw new Exception($"Failed to star repository: {response.StatusCode}, {content}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    protected override void ConfigureClaims()
 | 
			
		||||
    {
 | 
			
		||||
        ClaimActions.MapJsonKey(ClaimConst.AvatarUrl, "avatar_url");
 | 
			
		||||
        ClaimActions.MapJsonKey(ClaimConst.Account, "name");
 | 
			
		||||
 | 
			
		||||
        base.ConfigureClaims();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class GiteeOAuthSettings
 | 
			
		||||
{
 | 
			
		||||
    public string ClientId { get; set; }
 | 
			
		||||
    public string ClientSecret { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class GiteeOAuthUser
 | 
			
		||||
{
 | 
			
		||||
    public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Login { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Avatar_Url { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public static class OAuthUserExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static GiteeOAuthUser ToAuthUser(this JsonElement element)
 | 
			
		||||
    {
 | 
			
		||||
        GiteeOAuthUser authUser = new GiteeOAuthUser();
 | 
			
		||||
        JsonElement.ObjectEnumerator target = element.EnumerateObject();
 | 
			
		||||
        authUser.Id = target.TryGetValue("id");
 | 
			
		||||
        authUser.Login = target.TryGetValue("login");
 | 
			
		||||
        authUser.Name = target.TryGetValue("name");
 | 
			
		||||
        authUser.Avatar_Url = target.TryGetValue("avatar_url");
 | 
			
		||||
        return authUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName)
 | 
			
		||||
    {
 | 
			
		||||
        return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 = "时间")]
 | 
			
		||||
 
 | 
			
		||||
@@ -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,17 @@ using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class AppService : IAppService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IUserAgentService UserAgentService;
 | 
			
		||||
    private readonly IClaimsPrincipalService ClaimsPrincipalService;
 | 
			
		||||
    public AppService(IUserAgentService userAgentService, IClaimsPrincipalService claimsPrincipalService)
 | 
			
		||||
    {
 | 
			
		||||
        UserAgentService = userAgentService;
 | 
			
		||||
        ClaimsPrincipalService = claimsPrincipalService;
 | 
			
		||||
    }
 | 
			
		||||
    public string GetReturnUrl(string returnUrl)
 | 
			
		||||
    {
 | 
			
		||||
        var url = QueryHelpers.AddQueryString(CookieAuthenticationDefaults.LoginPath, new Dictionary<string, string?>
 | 
			
		||||
@@ -41,18 +46,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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -69,7 +72,7 @@ public class AppService : IAppService
 | 
			
		||||
            ExpiresUtc = diffTime,
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    public ClaimsPrincipal? User => App.User;
 | 
			
		||||
    public ClaimsPrincipal? User => ClaimsPrincipalService.User;
 | 
			
		||||
 | 
			
		||||
    public string? RemoteIpAddress => App.HttpContext?.GetRemoteIpAddressToIPv4();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class HybridClaimsPrincipalService : IClaimsPrincipalService
 | 
			
		||||
{
 | 
			
		||||
    HybridAppService _hybridAppService;
 | 
			
		||||
    public HybridClaimsPrincipalService(HybridAppService hybridAppService)
 | 
			
		||||
    {
 | 
			
		||||
        _hybridAppService = hybridAppService;
 | 
			
		||||
    }
 | 
			
		||||
    public ClaimsPrincipal? User => _hybridAppService.User;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.Extensions.Localization;
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.DataEncryption;
 | 
			
		||||
@@ -64,6 +62,10 @@ public class AuthService : IAuthService
 | 
			
		||||
        {
 | 
			
		||||
            throw Oops.Bah(appConfig.WebsitePolicy.CloseTip);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        string? password = input.Password;
 | 
			
		||||
        if (isCookie) //openApi登录不再需要解密
 | 
			
		||||
        {
 | 
			
		||||
@@ -237,25 +239,20 @@ public class AuthService : IAuthService
 | 
			
		||||
        var logingEvent = new LoginEvent
 | 
			
		||||
        {
 | 
			
		||||
            Ip = _appService.RemoteIpAddress,
 | 
			
		||||
            Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(),
 | 
			
		||||
            Device = _appService.UserAgent?.Platform,
 | 
			
		||||
            Expire = expire,
 | 
			
		||||
            SysUser = sysUser,
 | 
			
		||||
            VerificatId = verificatId
 | 
			
		||||
        };
 | 
			
		||||
        await WriteTokenToCache(loginPolicy, logingEvent).ConfigureAwait(false);//写入verificat到cache
 | 
			
		||||
        await UpdateUser(logingEvent).ConfigureAwait(false);
 | 
			
		||||
        if (sysUser.Account == RoleConst.SuperAdmin)
 | 
			
		||||
        {
 | 
			
		||||
            var modules = (await _sysResourceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.Category == ResourceCategoryEnum.Module).OrderBy(a => a.SortCode);//获取模块列表
 | 
			
		||||
            sysUser.ModuleList = modules.ToList();//模块列表赋值给用户
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //返回结果
 | 
			
		||||
        return new LoginOutput
 | 
			
		||||
        {
 | 
			
		||||
            VerificatId = verificatId,
 | 
			
		||||
            Account = sysUser.Account,
 | 
			
		||||
            Id = sysUser.Id,
 | 
			
		||||
            ModuleList = sysUser.ModuleList,
 | 
			
		||||
            AccessToken = accessToken,
 | 
			
		||||
            RefreshToken = refreshToken
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -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);//清除角色下用户缓存
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
@@ -466,7 +466,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
            var exist = await GetUserByIdAsync(input.Id).ConfigureAwait(false);//获取用户信息
 | 
			
		||||
            if (exist != null)
 | 
			
		||||
            {
 | 
			
		||||
                var isSuperAdmin = exist.Account == RoleConst.SuperAdmin;//判断是否有超管
 | 
			
		||||
                var isSuperAdmin = exist.Id == RoleConst.SuperAdminId;//判断是否有超管
 | 
			
		||||
                if (isSuperAdmin && !UserManager.SuperAdmin)
 | 
			
		||||
                    throw Oops.Bah(Localizer["CanotEditAdminUser"]);
 | 
			
		||||
 | 
			
		||||
@@ -540,7 +540,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
        await CheckApiDataScopeAsync(sysUser.OrgId, sysUser.CreateUserId).ConfigureAwait(false);
 | 
			
		||||
        if (sysUser != null)
 | 
			
		||||
        {
 | 
			
		||||
            var isSuperAdmin = (sysUser.Account == RoleConst.SuperAdmin || input.GrantInfoList.Any(a => a == RoleConst.SuperAdminRoleId)) && !UserManager.SuperAdmin;//判断是否有超管
 | 
			
		||||
            var isSuperAdmin = (sysUser.Id == RoleConst.SuperAdminId || input.GrantInfoList.Any(a => a == RoleConst.SuperAdminRoleId)) && !UserManager.SuperAdmin;//判断是否有超管
 | 
			
		||||
            if (isSuperAdmin)
 | 
			
		||||
                throw Oops.Bah(Localizer["CanotGrantAdmin"]);
 | 
			
		||||
 | 
			
		||||
@@ -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);//合并列表
 | 
			
		||||
            }
 | 
			
		||||
@@ -660,7 +660,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
    public async Task<bool> DeleteUserAsync(IEnumerable<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        var containsSuperAdmin = await db.Queryable<SysUser>().Where(it => it.Account == RoleConst.SuperAdmin && ids.Contains(it.Id)).AnyAsync().ConfigureAwait(false);//判断是否有超管
 | 
			
		||||
        var containsSuperAdmin = await db.Queryable<SysUser>().Where(it => it.Id == RoleConst.SuperAdminId && ids.Contains(it.Id)).AnyAsync().ConfigureAwait(false);//判断是否有超管
 | 
			
		||||
        if (containsSuperAdmin)
 | 
			
		||||
            throw Oops.Bah(Localizer["CanotDeleteAdminUser"]);
 | 
			
		||||
        if (ids.Contains(UserManager.UserId))
 | 
			
		||||
@@ -899,7 +899,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
            var tenantId = await _sysOrgService.GetTenantIdByOrgIdAsync(sysUser.OrgId, sysOrgList).ConfigureAwait(false);
 | 
			
		||||
            sysUser.TenantId = tenantId;
 | 
			
		||||
 | 
			
		||||
            if (sysUser.Account == RoleConst.SuperAdmin)
 | 
			
		||||
            if (sysUser.Id == RoleConst.SuperAdminId)
 | 
			
		||||
            {
 | 
			
		||||
                var modules = (await _sysResourceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.Category == ResourceCategoryEnum.Module).OrderBy(a => a.SortCode);
 | 
			
		||||
                sysUser.ModuleList = modules.ToList();//模块列表赋值给用户
 | 
			
		||||
 
 | 
			
		||||
@@ -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(a=>a.ClientIds==null).Where(a => a.Id > 0).ExecuteCommand();
 | 
			
		||||
        db.Updateable<VerificatInfo>().SetColumns(a => a.ClientIds == null).Where(a => a.Id > 0).ExecuteCommand();
 | 
			
		||||
        VerificatInfoService.RemoveCache();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@ using BootstrapBlazor.Components;
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.UnifyResult;
 | 
			
		||||
@@ -28,18 +26,12 @@ public class Startup : AppStartup
 | 
			
		||||
    {
 | 
			
		||||
        Directory.CreateDirectory("DB");
 | 
			
		||||
 | 
			
		||||
        services.AddConfigurableOptions<SqlSugarOptions>();
 | 
			
		||||
        services.AddConfigurableOptions<AdminLogOptions>();
 | 
			
		||||
        services.AddConfigurableOptions<TenantOptions>();
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton(typeof(IDataService<>), typeof(BaseService<>));
 | 
			
		||||
        services.AddSingleton<ISugarAopService, SugarAopService>();
 | 
			
		||||
        services.AddSingleton<ISugarConfigAopService, SugarConfigAopService>();
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<IUserAgentService, UserAgentService>();
 | 
			
		||||
        services.AddSingleton<IAppService, AppService>();
 | 
			
		||||
 | 
			
		||||
        StaticConfig.EnableAllWhereIF = true;
 | 
			
		||||
 | 
			
		||||
        services.AddConfigurableOptions<EmailOptions>();
 | 
			
		||||
        services.AddConfigurableOptions<HardwareInfoOptions>();
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +48,6 @@ public class Startup : AppStartup
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<IVerificatInfoService, VerificatInfoService>();
 | 
			
		||||
        services.AddSingleton<IUserCenterService, UserCenterService>();
 | 
			
		||||
        services.AddSingleton<ISugarAopService, SugarAopService>();
 | 
			
		||||
        services.AddSingleton<ISysDictService, SysDictService>();
 | 
			
		||||
        services.AddSingleton<ISysOperateLogService, SysOperateLogService>();
 | 
			
		||||
        services.AddSingleton<IRelationService, RelationService>();
 | 
			
		||||
@@ -97,6 +88,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,10 +18,7 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
 | 
			
		||||
		<PackageReference Include="UAParser" Version="3.1.47" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
 | 
			
		||||
@@ -50,6 +47,7 @@
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.SqlSugar\ThingsGateway.SqlSugar.csproj" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 
 | 
			
		||||
@@ -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"},
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -40,6 +40,8 @@ public class BlazorAppContext
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public SysUser CurrentUser { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public string? Avatar => UserManager.AvatarUrl.IsNullOrEmpty() ? CurrentUser.Avatar : UserManager.AvatarUrl;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用户个人菜单
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -97,7 +99,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)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ public partial class UserCenterPage
 | 
			
		||||
    protected override async Task OnParametersSetAsync()
 | 
			
		||||
    {
 | 
			
		||||
        SysUser = AppContext.CurrentUser.Adapt<SysUser>();
 | 
			
		||||
        SysUser.Avatar = AppContext.CurrentUser.Avatar;
 | 
			
		||||
        SysUser.Avatar = AppContext.Avatar;
 | 
			
		||||
        WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).Adapt<WorkbenchInfo>();
 | 
			
		||||
 | 
			
		||||
        await base.OnParametersSetAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
 | 
			
		||||
    // nuget动态加载的程序集
 | 
			
		||||
    "SupportPackageNamePrefixs": [
 | 
			
		||||
      "ThingsGateway.SqlSugar",
 | 
			
		||||
      "ThingsGateway.Admin.Application",
 | 
			
		||||
      "ThingsGateway.Admin.Razor",
 | 
			
		||||
      "ThingsGateway.Razor"
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
 | 
			
		||||
    // nuget动态加载的程序集
 | 
			
		||||
    "SupportPackageNamePrefixs": [
 | 
			
		||||
      "ThingsGateway.SqlSugar",
 | 
			
		||||
      "ThingsGateway.Admin.Application",
 | 
			
		||||
      "ThingsGateway.Admin.Razor",
 | 
			
		||||
      "ThingsGateway.Razor"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								src/Admin/ThingsGateway.AdminServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/Admin/ThingsGateway.AdminServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
// 版权信息
 | 
			
		||||
// 版权归百小僧及百签科技(广东)有限公司所有。
 | 
			
		||||
// 所有权利保留。
 | 
			
		||||
// 官方网站:https://baiqian.com
 | 
			
		||||
//
 | 
			
		||||
// 许可证信息
 | 
			
		||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
 | 
			
		||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
global using System.Collections;
 | 
			
		||||
 | 
			
		||||
global using ThingsGateway.Admin.Application;
 | 
			
		||||
@@ -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,9 +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 +28,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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@ using Microsoft.Extensions.Localization;
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.AdminServer;
 | 
			
		||||
 | 
			
		||||
public partial class AccessDenied
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,11 @@ using Microsoft.Extensions.Options;
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.DataEncryption;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
using ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.AdminServer;
 | 
			
		||||
 | 
			
		||||
public partial class Login
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
                        <CultureChooser />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <Logout ImageUrl="@(AppContext.CurrentUser.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]>
 | 
			
		||||
                    <Logout ImageUrl="@(AppContext.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]>
 | 
			
		||||
                        <LinkTemplate>
 | 
			
		||||
                            <a href=@("/") class="h6"><i class="fa-solid fa-suitcase me-2"></i>@Localizer["系统首页"]</a>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ using Microsoft.Extensions.Options;
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.Admin.Razor;
 | 
			
		||||
using ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
@@ -27,38 +26,6 @@ public partial class MainLayout : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; }
 | 
			
		||||
    private Task OnRefresh(ContextMenuItem item, object? context)
 | 
			
		||||
    {
 | 
			
		||||
        if (context is TabItem tabItem)
 | 
			
		||||
        {
 | 
			
		||||
            _tab.Refresh(tabItem);
 | 
			
		||||
        }
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task OnClose(ContextMenuItem item, object? context)
 | 
			
		||||
    {
 | 
			
		||||
        if (context is TabItem tabItem)
 | 
			
		||||
        {
 | 
			
		||||
            await _tab.RemoveTab(tabItem);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnCloseOther(ContextMenuItem item, object? context)
 | 
			
		||||
    {
 | 
			
		||||
        if (context is TabItem tabItem)
 | 
			
		||||
        {
 | 
			
		||||
            _tab.ActiveTab(tabItem);
 | 
			
		||||
        }
 | 
			
		||||
        _tab.CloseOtherTabs();
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task OnCloseAll(ContextMenuItem item, object? context)
 | 
			
		||||
    {
 | 
			
		||||
        _tab.CloseAllTabs();
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 全局通知
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,8 @@ public class SingleFilePublish : ISingleFilePublish
 | 
			
		||||
            "ThingsGateway.NewLife.X",
 | 
			
		||||
            "ThingsGateway.Razor",
 | 
			
		||||
            "ThingsGateway.Admin.Razor"   ,
 | 
			
		||||
            "ThingsGateway.Admin.Application"
 | 
			
		||||
            "ThingsGateway.Admin.Application",
 | 
			
		||||
            "ThingsGateway.SqlSugar",
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -26,10 +25,8 @@ using System.Text;
 | 
			
		||||
using System.Text.Encodings.Web;
 | 
			
		||||
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 +86,7 @@ public class Startup : AppStartup
 | 
			
		||||
        }
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        services.AddMvcFilter<RequestAuditFilter>();
 | 
			
		||||
        services.AddControllers()
 | 
			
		||||
            .AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings))
 | 
			
		||||
            //.AddXmlSerializerFormatters()
 | 
			
		||||
@@ -161,7 +159,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 +211,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 +302,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
 | 
			
		||||
            {
 | 
			
		||||
@@ -368,12 +368,6 @@ public class Startup : AppStartup
 | 
			
		||||
        app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
 | 
			
		||||
        app.UseStaticFiles();
 | 
			
		||||
 | 
			
		||||
        app.Use(async (context, next) =>
 | 
			
		||||
        {
 | 
			
		||||
            context.Response.Headers.Append("ThingsGateway", "ThingsGateway");
 | 
			
		||||
            await next().ConfigureAwait(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 特定文件类型(文件后缀)处理
 | 
			
		||||
        var contentTypeProvider = GetFileExtensionContentTypeProvider();
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
    /// 原生日志上下文数据
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
		<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' ">
 | 
			
		||||
 
 | 
			
		||||
@@ -349,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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Buffers;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
 | 
			
		||||
#if NETFRAMEWORK || NETSTANDARD2_0
 | 
			
		||||
#if NETFRAMEWORK || NETSTANDARD2_0
 | 
			
		||||
using ValueTask = System.Threading.Tasks.Task;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
@@ -95,7 +94,7 @@ public static class Pool
 | 
			
		||||
    {
 | 
			
		||||
        //if (ms == null) return null;
 | 
			
		||||
 | 
			
		||||
        var buf = returnResult ? ms.ToArray() : Empty;
 | 
			
		||||
        var buf = returnResult ? ms.ToArray() : Array.Empty<byte>();
 | 
			
		||||
 | 
			
		||||
        Pool.MemoryStream.Return(ms);
 | 
			
		||||
 | 
			
		||||
@@ -133,11 +132,5 @@ public static class Pool
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region ByteArray
 | 
			
		||||
    /// <summary>字节数组共享存储</summary>
 | 
			
		||||
    public static ArrayPool<Byte> Shared { get; set; } = ArrayPool<Byte>.Shared;
 | 
			
		||||
 | 
			
		||||
    /// <summary>空数组</summary>
 | 
			
		||||
    public static Byte[] Empty { get; } = [];
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -324,13 +324,21 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
            // 移除扩展空闲集合里面的超时项
 | 
			
		||||
            while (_free2.TryPeek(out var pi) && pi.LastTime < exp)
 | 
			
		||||
            {
 | 
			
		||||
                // 取出来销毁
 | 
			
		||||
                if (_free2.TryDequeue(out pi))
 | 
			
		||||
                // 取出来销毁。在并行操作中,此时返回可能是另一个对象
 | 
			
		||||
                if (_free2.TryDequeue(out var pi2))
 | 
			
		||||
                {
 | 
			
		||||
                    pi.Value.TryDispose();
 | 
			
		||||
                    if (pi2.LastTime < exp)
 | 
			
		||||
                    {
 | 
			
		||||
                        pi2.Value.TryDispose();
 | 
			
		||||
 | 
			
		||||
                    count++;
 | 
			
		||||
                    Interlocked.Decrement(ref _FreeCount);
 | 
			
		||||
                        count++;
 | 
			
		||||
                        Interlocked.Decrement(ref _FreeCount);
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // 可能是另一个对象,放回去
 | 
			
		||||
                        _free2.Enqueue(pi2);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
/// <summary>工具类</summary>
 | 
			
		||||
@@ -452,12 +450,12 @@ public class DefaultConvert
 | 
			
		||||
                    // 凑够8字节
 | 
			
		||||
                    if (buf.Length < 8)
 | 
			
		||||
                    {
 | 
			
		||||
                        var bts = Pool.Shared.Rent(8);
 | 
			
		||||
                        var bts = ArrayPool<Byte>.Shared.Rent(8);
 | 
			
		||||
                        Buffer.BlockCopy(buf, 0, bts, 0, buf.Length);
 | 
			
		||||
 | 
			
		||||
                        var dec = BitConverter.ToDouble(bts, 0).ToDecimal();
 | 
			
		||||
 | 
			
		||||
                        Pool.Shared.Return(bts);
 | 
			
		||||
                        ArrayPool<Byte>.Shared.Return(bts);
 | 
			
		||||
 | 
			
		||||
                        return dec;
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
public class ByteArrayToNumberArrayConverter : JsonConverter<byte[]>
 | 
			
		||||
{
 | 
			
		||||
    public override void WriteJson(JsonWriter writer, byte[]? value, JsonSerializer serializer)
 | 
			
		||||
    {
 | 
			
		||||
        if (value == null)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteNull();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 将 byte[] 转换为数值数组
 | 
			
		||||
        writer.WriteStartArray();
 | 
			
		||||
        foreach (var b in value)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteValue(b);
 | 
			
		||||
        }
 | 
			
		||||
        writer.WriteEndArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override byte[] ReadJson(JsonReader reader, Type objectType, byte[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
 | 
			
		||||
    {
 | 
			
		||||
        // 从数值数组读取 byte[]
 | 
			
		||||
        if (reader.TokenType == JsonToken.StartArray)
 | 
			
		||||
        {
 | 
			
		||||
            var byteList = new System.Collections.Generic.List<byte>();
 | 
			
		||||
            while (reader.Read())
 | 
			
		||||
            {
 | 
			
		||||
                if (reader.TokenType == JsonToken.EndArray)
 | 
			
		||||
                {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (reader.TokenType == JsonToken.Integer)
 | 
			
		||||
                {
 | 
			
		||||
                    byteList.Add(Convert.ToByte(reader.Value));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return byteList.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
        throw new JsonSerializationException("Invalid JSON format for byte array.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override bool CanRead => true;
 | 
			
		||||
}
 | 
			
		||||
@@ -15,14 +15,14 @@ namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// json扩展
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class JsonExtensions
 | 
			
		||||
public static class JsonExtension
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerSettings IndentedOptions;
 | 
			
		||||
    public static JsonSerializerSettings NoneIndentedOptions;
 | 
			
		||||
    static JsonExtensions()
 | 
			
		||||
    static JsonExtension()
 | 
			
		||||
    {
 | 
			
		||||
        IndentedOptions = new JsonSerializerSettings
 | 
			
		||||
        {
 | 
			
		||||
@@ -81,4 +81,52 @@ public static class JsonExtensions
 | 
			
		||||
    {
 | 
			
		||||
        return Newtonsoft.Json.JsonConvert.SerializeObject(item, indented == false ? NoneIndentedOptions : IndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class ByteArrayToNumberArrayConverter : JsonConverter<byte[]>
 | 
			
		||||
{
 | 
			
		||||
    public override void WriteJson(JsonWriter writer, byte[]? value, JsonSerializer serializer)
 | 
			
		||||
    {
 | 
			
		||||
        if (value == null)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteNull();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 将 byte[] 转换为数值数组
 | 
			
		||||
        writer.WriteStartArray();
 | 
			
		||||
        foreach (var b in value)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteValue(b);
 | 
			
		||||
        }
 | 
			
		||||
        writer.WriteEndArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override byte[] ReadJson(JsonReader reader, Type objectType, byte[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
 | 
			
		||||
    {
 | 
			
		||||
        // 从数值数组读取 byte[]
 | 
			
		||||
        if (reader.TokenType == JsonToken.StartArray)
 | 
			
		||||
        {
 | 
			
		||||
            var byteList = new System.Collections.Generic.List<byte>();
 | 
			
		||||
            while (reader.Read())
 | 
			
		||||
            {
 | 
			
		||||
                if (reader.TokenType == JsonToken.EndArray)
 | 
			
		||||
                {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (reader.TokenType == JsonToken.Integer)
 | 
			
		||||
                {
 | 
			
		||||
                    byteList.Add(Convert.ToByte(reader.Value));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return byteList.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
        throw new JsonSerializationException("Invalid JSON format for byte array.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override bool CanRead => true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,694 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
using System.Text.Encodings.Web;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// System.Text.Json 扩展
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class SystemTextJsonExtension
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(带缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions IndentedOptions;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(无缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions NoneIndentedOptions;
 | 
			
		||||
 | 
			
		||||
    static SystemTextJsonExtension()
 | 
			
		||||
    {
 | 
			
		||||
        IndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = true, // 缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        // 如有自定义Converter,这里添加
 | 
			
		||||
        // IndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = false, // 不缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        // NoneIndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 反序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="json"></param>
 | 
			
		||||
    /// <param name="type"></param>
 | 
			
		||||
    /// <param name="options"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static object? FromSystemTextJsonString(this string json, Type type, JsonSerializerOptions? options = null)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Deserialize(json, type, options ?? IndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 反序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static T? FromSystemTextJsonString<T>(this string json, JsonSerializerOptions? options = null)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Deserialize<T>(json, options ?? IndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="item"></param>
 | 
			
		||||
    /// <param name="options"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, JsonSerializerOptions? options)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), options ?? IndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, bool indented = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 将 byte[] 序列化为数值数组,反序列化数值数组为 byte[]
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ByteArrayToNumberArrayConverterSystemTextJson : JsonConverter<byte[]>
 | 
			
		||||
{
 | 
			
		||||
    public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        if (reader.TokenType != JsonTokenType.StartArray)
 | 
			
		||||
        {
 | 
			
		||||
            throw new JsonException("Expected StartArray token.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var bytes = new List<byte>();
 | 
			
		||||
        while (reader.Read())
 | 
			
		||||
        {
 | 
			
		||||
            if (reader.TokenType == JsonTokenType.EndArray)
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            if (reader.TokenType == JsonTokenType.Number)
 | 
			
		||||
            {
 | 
			
		||||
                if (reader.TryGetByte(out byte value))
 | 
			
		||||
                {
 | 
			
		||||
                    bytes.Add(value);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    throw new JsonException("Invalid number value for byte array.");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throw new JsonException($"Unexpected token {reader.TokenType} in byte array.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return bytes.ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        if (value == null)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteNullValue();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        writer.WriteStartArray();
 | 
			
		||||
        foreach (var b in value)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WriteNumberValue(b);
 | 
			
		||||
        }
 | 
			
		||||
        writer.WriteEndArray();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// System.Text.Json → JToken / JObject / JArray 转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class JTokenSystemTextJsonConverter : JsonConverter<JToken>
 | 
			
		||||
{
 | 
			
		||||
    public override JToken? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        return ReadToken(ref reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static JToken ReadToken(ref Utf8JsonReader reader)
 | 
			
		||||
    {
 | 
			
		||||
        switch (reader.TokenType)
 | 
			
		||||
        {
 | 
			
		||||
            case JsonTokenType.StartObject:
 | 
			
		||||
                var obj = new JObject();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndObject)
 | 
			
		||||
                        return obj;
 | 
			
		||||
 | 
			
		||||
                    var propertyName = reader.GetString();
 | 
			
		||||
                    reader.Read();
 | 
			
		||||
                    var value = ReadToken(ref reader);
 | 
			
		||||
                    obj[propertyName!] = value;
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.StartArray:
 | 
			
		||||
                var array = new JArray();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndArray)
 | 
			
		||||
                        return array;
 | 
			
		||||
 | 
			
		||||
                    array.Add(ReadToken(ref reader));
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.String:
 | 
			
		||||
                if (reader.TryGetDateTime(out var date))
 | 
			
		||||
                    return new JValue(date);
 | 
			
		||||
                return new JValue(reader.GetString());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Number:
 | 
			
		||||
                if (reader.TryGetInt64(out var l))
 | 
			
		||||
                    return new JValue(l);
 | 
			
		||||
                return new JValue(reader.GetDouble());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.True:
 | 
			
		||||
                return new JValue(true);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.False:
 | 
			
		||||
                return new JValue(false);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Null:
 | 
			
		||||
                return JValue.CreateNull();
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new JsonException($"Unsupported token type {reader.TokenType}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, JToken value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        switch (value.Type)
 | 
			
		||||
        {
 | 
			
		||||
            case JTokenType.Object:
 | 
			
		||||
                writer.WriteStartObject();
 | 
			
		||||
                foreach (var prop in (JObject)value)
 | 
			
		||||
                {
 | 
			
		||||
                    writer.WritePropertyName(prop.Key);
 | 
			
		||||
                    Write(writer, prop.Value!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndObject();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Array:
 | 
			
		||||
                writer.WriteStartArray();
 | 
			
		||||
                foreach (var item in (JArray)value)
 | 
			
		||||
                {
 | 
			
		||||
                    Write(writer, item!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndArray();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Null:
 | 
			
		||||
                writer.WriteNullValue();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Boolean:
 | 
			
		||||
                writer.WriteBooleanValue(value.Value<bool>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Integer:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<long>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Float:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<double>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.String:
 | 
			
		||||
                writer.WriteStringValue(value.Value<string>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Date:
 | 
			
		||||
                writer.WriteStringValue(value.Value<DateTime>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Guid:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Guid>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Uri:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Uri>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.TimeSpan:
 | 
			
		||||
                writer.WriteStringValue(value.Value<TimeSpan>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // fallback — 转字符串
 | 
			
		||||
                writer.WriteStringValue(value.ToString());
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// System.Text.Json → JToken / JObject / JArray 转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class JObjectSystemTextJsonConverter : JsonConverter<JObject>
 | 
			
		||||
{
 | 
			
		||||
    public override JObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        var obj = new JObject();
 | 
			
		||||
        while (reader.Read())
 | 
			
		||||
        {
 | 
			
		||||
            if (reader.TokenType == JsonTokenType.EndObject)
 | 
			
		||||
                return obj;
 | 
			
		||||
 | 
			
		||||
            var propertyName = reader.GetString();
 | 
			
		||||
            reader.Read();
 | 
			
		||||
            var value = ReadJToken(ref reader);
 | 
			
		||||
            obj[propertyName!] = value;
 | 
			
		||||
        }
 | 
			
		||||
        throw new JsonException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static JToken ReadJToken(ref Utf8JsonReader reader)
 | 
			
		||||
    {
 | 
			
		||||
        switch (reader.TokenType)
 | 
			
		||||
        {
 | 
			
		||||
            case JsonTokenType.StartObject:
 | 
			
		||||
                var obj = new JObject();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndObject)
 | 
			
		||||
                        return obj;
 | 
			
		||||
 | 
			
		||||
                    var propertyName = reader.GetString();
 | 
			
		||||
                    reader.Read();
 | 
			
		||||
                    var value = ReadJToken(ref reader);
 | 
			
		||||
                    obj[propertyName!] = value;
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.StartArray:
 | 
			
		||||
                var array = new JArray();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndArray)
 | 
			
		||||
                        return array;
 | 
			
		||||
 | 
			
		||||
                    array.Add(ReadJToken(ref reader));
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.String:
 | 
			
		||||
                if (reader.TryGetDateTime(out var date))
 | 
			
		||||
                    return new JValue(date);
 | 
			
		||||
                return new JValue(reader.GetString());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Number:
 | 
			
		||||
                if (reader.TryGetInt64(out var l))
 | 
			
		||||
                    return new JValue(l);
 | 
			
		||||
                return new JValue(reader.GetDouble());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.True:
 | 
			
		||||
                return new JValue(true);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.False:
 | 
			
		||||
                return new JValue(false);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Null:
 | 
			
		||||
                return JValue.CreateNull();
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new JsonException($"Unsupported token type {reader.TokenType}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, JObject value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        writer.WriteStartObject();
 | 
			
		||||
        foreach (var prop in (JObject)value)
 | 
			
		||||
        {
 | 
			
		||||
            writer.WritePropertyName(prop.Key);
 | 
			
		||||
            Write(writer, prop.Value!, options);
 | 
			
		||||
        }
 | 
			
		||||
        writer.WriteEndObject();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static void Write(Utf8JsonWriter writer, JToken value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        switch (value.Type)
 | 
			
		||||
        {
 | 
			
		||||
            case JTokenType.Object:
 | 
			
		||||
                writer.WriteStartObject();
 | 
			
		||||
                foreach (var prop in (JObject)value)
 | 
			
		||||
                {
 | 
			
		||||
                    writer.WritePropertyName(prop.Key);
 | 
			
		||||
                    Write(writer, prop.Value!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndObject();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Array:
 | 
			
		||||
                writer.WriteStartArray();
 | 
			
		||||
                foreach (var item in (JArray)value)
 | 
			
		||||
                {
 | 
			
		||||
                    Write(writer, item!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndArray();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Null:
 | 
			
		||||
                writer.WriteNullValue();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Boolean:
 | 
			
		||||
                writer.WriteBooleanValue(value.Value<bool>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Integer:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<long>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Float:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<double>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.String:
 | 
			
		||||
                writer.WriteStringValue(value.Value<string>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Date:
 | 
			
		||||
                writer.WriteStringValue(value.Value<DateTime>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Guid:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Guid>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Uri:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Uri>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.TimeSpan:
 | 
			
		||||
                writer.WriteStringValue(value.Value<TimeSpan>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // fallback — 转字符串
 | 
			
		||||
                writer.WriteStringValue(value.ToString());
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// System.Text.Json → JToken / JObject / JArray 转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class JArraySystemTextJsonConverter : JsonConverter<JArray>
 | 
			
		||||
{
 | 
			
		||||
    public override JArray? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        var array = new JArray();
 | 
			
		||||
        while (reader.Read())
 | 
			
		||||
        {
 | 
			
		||||
            if (reader.TokenType == JsonTokenType.EndArray)
 | 
			
		||||
                return array;
 | 
			
		||||
 | 
			
		||||
            array.Add(ReadToken(ref reader));
 | 
			
		||||
        }
 | 
			
		||||
        throw new JsonException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static JToken ReadToken(ref Utf8JsonReader reader)
 | 
			
		||||
    {
 | 
			
		||||
        switch (reader.TokenType)
 | 
			
		||||
        {
 | 
			
		||||
            case JsonTokenType.StartObject:
 | 
			
		||||
                var obj = new JObject();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndObject)
 | 
			
		||||
                        return obj;
 | 
			
		||||
 | 
			
		||||
                    var propertyName = reader.GetString();
 | 
			
		||||
                    reader.Read();
 | 
			
		||||
                    var value = ReadToken(ref reader);
 | 
			
		||||
                    obj[propertyName!] = value;
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.StartArray:
 | 
			
		||||
                var array = new JArray();
 | 
			
		||||
                while (reader.Read())
 | 
			
		||||
                {
 | 
			
		||||
                    if (reader.TokenType == JsonTokenType.EndArray)
 | 
			
		||||
                        return array;
 | 
			
		||||
 | 
			
		||||
                    array.Add(ReadToken(ref reader));
 | 
			
		||||
                }
 | 
			
		||||
                throw new JsonException();
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.String:
 | 
			
		||||
                if (reader.TryGetDateTime(out var date))
 | 
			
		||||
                    return new JValue(date);
 | 
			
		||||
                return new JValue(reader.GetString());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Number:
 | 
			
		||||
                if (reader.TryGetInt64(out var l))
 | 
			
		||||
                    return new JValue(l);
 | 
			
		||||
                return new JValue(reader.GetDouble());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.True:
 | 
			
		||||
                return new JValue(true);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.False:
 | 
			
		||||
                return new JValue(false);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Null:
 | 
			
		||||
                return JValue.CreateNull();
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new JsonException($"Unsupported token type {reader.TokenType}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, JArray value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        writer.WriteStartArray();
 | 
			
		||||
        foreach (var item in (JArray)value)
 | 
			
		||||
        {
 | 
			
		||||
            Write(writer, item!, options);
 | 
			
		||||
        }
 | 
			
		||||
        writer.WriteEndArray();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void Write(Utf8JsonWriter writer, JToken value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        switch (value.Type)
 | 
			
		||||
        {
 | 
			
		||||
            case JTokenType.Object:
 | 
			
		||||
                writer.WriteStartObject();
 | 
			
		||||
                foreach (var prop in (JObject)value)
 | 
			
		||||
                {
 | 
			
		||||
                    writer.WritePropertyName(prop.Key);
 | 
			
		||||
                    Write(writer, prop.Value!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndObject();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Array:
 | 
			
		||||
                writer.WriteStartArray();
 | 
			
		||||
                foreach (var item in (JArray)value)
 | 
			
		||||
                {
 | 
			
		||||
                    Write(writer, item!, options);
 | 
			
		||||
                }
 | 
			
		||||
                writer.WriteEndArray();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Null:
 | 
			
		||||
                writer.WriteNullValue();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Boolean:
 | 
			
		||||
                writer.WriteBooleanValue(value.Value<bool>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Integer:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<long>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Float:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<double>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.String:
 | 
			
		||||
                writer.WriteStringValue(value.Value<string>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Date:
 | 
			
		||||
                writer.WriteStringValue(value.Value<DateTime>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Guid:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Guid>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Uri:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Uri>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.TimeSpan:
 | 
			
		||||
                writer.WriteStringValue(value.Value<TimeSpan>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // fallback — 转字符串
 | 
			
		||||
                writer.WriteStringValue(value.ToString());
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// System.Text.Json → JToken / JObject / JArray 转换器
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class JValueSystemTextJsonConverter : JsonConverter<JValue>
 | 
			
		||||
{
 | 
			
		||||
    public override JValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        return ReadJValue(ref reader);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static JValue ReadJValue(ref Utf8JsonReader reader)
 | 
			
		||||
    {
 | 
			
		||||
        switch (reader.TokenType)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.String:
 | 
			
		||||
                if (reader.TryGetDateTime(out var date))
 | 
			
		||||
                    return new JValue(date);
 | 
			
		||||
                return new JValue(reader.GetString());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Number:
 | 
			
		||||
                if (reader.TryGetInt64(out var l))
 | 
			
		||||
                    return new JValue(l);
 | 
			
		||||
                return new JValue(reader.GetDouble());
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.True:
 | 
			
		||||
                return new JValue(true);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.False:
 | 
			
		||||
                return new JValue(false);
 | 
			
		||||
 | 
			
		||||
            case JsonTokenType.Null:
 | 
			
		||||
                return JValue.CreateNull();
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                throw new JsonException($"Unsupported token type {reader.TokenType}");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override void Write(Utf8JsonWriter writer, JValue value, JsonSerializerOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        switch (value.Type)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Null:
 | 
			
		||||
                writer.WriteNullValue();
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Boolean:
 | 
			
		||||
                writer.WriteBooleanValue(value.Value<bool>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Integer:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<long>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Float:
 | 
			
		||||
                writer.WriteNumberValue(value.Value<double>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.String:
 | 
			
		||||
                writer.WriteStringValue(value.Value<string>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Date:
 | 
			
		||||
                writer.WriteStringValue(value.Value<DateTime>());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Guid:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Guid>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.Uri:
 | 
			
		||||
                writer.WriteStringValue(value.Value<Uri>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case JTokenType.TimeSpan:
 | 
			
		||||
                writer.WriteStringValue(value.Value<TimeSpan>().ToString());
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                // fallback — 转字符串
 | 
			
		||||
                writer.WriteStringValue(value.ToString());
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -8,4 +8,6 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
global using System.Buffers;
 | 
			
		||||
 | 
			
		||||
global using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ using System.Net.Sockets;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.Net;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife;
 | 
			
		||||
@@ -52,7 +51,7 @@ public static class NetHelper
 | 
			
		||||
#endif
 | 
			
		||||
        {
 | 
			
		||||
            UInt32 dummy = 0;
 | 
			
		||||
            var inOptionValues = Pool.Shared.Rent(Marshal.SizeOf(dummy) * 3);
 | 
			
		||||
            var inOptionValues = ArrayPool<Byte>.Shared.Rent(Marshal.SizeOf(dummy) * 3);
 | 
			
		||||
 | 
			
		||||
            // 是否启用Keep-Alive
 | 
			
		||||
            BitConverter.GetBytes((UInt32)(isKeepAlive ? 1 : 0)).CopyTo(inOptionValues, 0);
 | 
			
		||||
@@ -63,7 +62,7 @@ public static class NetHelper
 | 
			
		||||
 | 
			
		||||
            socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
 | 
			
		||||
 | 
			
		||||
            Pool.Shared.Return(inOptionValues);
 | 
			
		||||
            ArrayPool<Byte>.Shared.Return(inOptionValues);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -538,11 +537,11 @@ public static class NetHelper
 | 
			
		||||
    private static void Wake(String mac)
 | 
			
		||||
    {
 | 
			
		||||
        mac = mac.Replace("-", null).Replace(":", null);
 | 
			
		||||
        var buffer = Pool.Shared.Rent(mac.Length / 2);
 | 
			
		||||
        var buffer = ArrayPool<Byte>.Shared.Rent(mac.Length / 2);
 | 
			
		||||
        for (var i = 0; i < buffer.Length; i++)
 | 
			
		||||
            buffer[i] = Byte.Parse(mac.Substring(i * 2, 2), NumberStyles.HexNumber);
 | 
			
		||||
 | 
			
		||||
        var bts = Pool.Shared.Rent(6 + 16 * buffer.Length);
 | 
			
		||||
        var bts = ArrayPool<Byte>.Shared.Rent(6 + 16 * buffer.Length);
 | 
			
		||||
        for (var i = 0; i < 6; i++)
 | 
			
		||||
            bts[i] = 0xFF;
 | 
			
		||||
        for (Int32 i = 6, k = 0; i < bts.Length; i++, k++)
 | 
			
		||||
@@ -560,8 +559,8 @@ public static class NetHelper
 | 
			
		||||
        client.Close();
 | 
			
		||||
        //client.SendAsync(bts, bts.Length, new IPEndPoint(IPAddress.Broadcast, 7));
 | 
			
		||||
 | 
			
		||||
        Pool.Shared.Return(bts);
 | 
			
		||||
        Pool.Shared.Return(buffer);
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(bts);
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(buffer);
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -627,6 +627,8 @@ public class DefaultReflect : IReflect
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual Type? GetElementType(Type type)
 | 
			
		||||
    {
 | 
			
		||||
        if (type == null) return null;
 | 
			
		||||
 | 
			
		||||
        if (type.HasElementType) return type.GetElementType();
 | 
			
		||||
 | 
			
		||||
        if (type.As<IEnumerable>())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Serialization;
 | 
			
		||||
@@ -495,7 +493,7 @@ public class Binary : FormatterBase, IBinary
 | 
			
		||||
    /// <param name="max"></param>
 | 
			
		||||
    public void WriteBCD(String value, Int32 max)
 | 
			
		||||
    {
 | 
			
		||||
        var buf = Pool.Shared.Rent(max);
 | 
			
		||||
        var buf = ArrayPool<Byte>.Shared.Rent(max);
 | 
			
		||||
        for (Int32 i = 0, j = 0; i < max && j + 1 < value.Length; i++, j += 2)
 | 
			
		||||
        {
 | 
			
		||||
            var a = (Byte)(value[j] - '0');
 | 
			
		||||
@@ -504,7 +502,7 @@ public class Binary : FormatterBase, IBinary
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Write(buf, 0, max);
 | 
			
		||||
        Pool.Shared.Return(buf);
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(buf);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>写入定长字符串。多余截取,少则补零</summary>
 | 
			
		||||
@@ -512,11 +510,11 @@ public class Binary : FormatterBase, IBinary
 | 
			
		||||
    /// <param name="max"></param>
 | 
			
		||||
    public void WriteFixedString(String? value, Int32 max)
 | 
			
		||||
    {
 | 
			
		||||
        var buf = Pool.Shared.Rent(max);
 | 
			
		||||
        var buf = ArrayPool<Byte>.Shared.Rent(max);
 | 
			
		||||
        if (!value.IsNullOrEmpty()) Encoding.GetBytes(value, 0, value.Length, buf, 0);
 | 
			
		||||
 | 
			
		||||
        Write(buf, 0, max);
 | 
			
		||||
        Pool.Shared.Return(buf);
 | 
			
		||||
        ArrayPool<Byte>.Shared.Return(buf);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>读取定长字符串。多余截取,少则补零</summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Xml;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.NewLife.Serialization;
 | 
			
		||||
@@ -144,10 +143,10 @@ public class XmlGeneral : XmlHandlerBase
 | 
			
		||||
        else if (type == typeof(Byte[]))
 | 
			
		||||
        {
 | 
			
		||||
            // 用字符串长度作为预设缓冲区的长度
 | 
			
		||||
            var buf = Pool.Shared.Rent(reader.Value.Length);
 | 
			
		||||
            var buf = ArrayPool<Byte>.Shared.Rent(reader.Value.Length);
 | 
			
		||||
            var count = reader.ReadContentAsBase64(buf, 0, buf.Length);
 | 
			
		||||
            value = buf.ReadBytes(0, count);
 | 
			
		||||
            Pool.Shared.Return(buf);
 | 
			
		||||
            ArrayPool<Byte>.Shared.Return(buf);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ public partial class CultureChooser
 | 
			
		||||
    {
 | 
			
		||||
        if (_firstRender)
 | 
			
		||||
        {
 | 
			
		||||
            if (OperatingSystem.IsBrowser() || !Runtime.IsWeb)
 | 
			
		||||
            if (OperatingSystem.IsBrowser() || !Runtime.IsWeb || App.EffectiveTypes.FirstOrDefault(a => a.Name.Contains("WebView")) != null)
 | 
			
		||||
            {
 | 
			
		||||
                var cultureName = item.Value;
 | 
			
		||||
                if (cultureName != CultureInfo.CurrentCulture.Name)
 | 
			
		||||
 
 | 
			
		||||
@@ -51,48 +51,6 @@ public static class GenericExtensions
 | 
			
		||||
        return differences;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static IEnumerable<PropertyInfo> GetProperties(this IEnumerable<dynamic> value, params string[] names)
 | 
			
		||||
    {
 | 
			
		||||
        // 获取动态对象集合的类型
 | 
			
		||||
        var type = value.GetType().GetGenericArguments().LastOrDefault() ?? throw new ArgumentNullException(nameof(value));
 | 
			
		||||
 | 
			
		||||
        var namesStr = System.Text.Json.JsonSerializer.Serialize(names);
 | 
			
		||||
        // 构建缓存键,包括属性名和类型信息
 | 
			
		||||
        var cacheKey = $"{namesStr}-{type.FullName}-{type.TypeHandle.Value}";
 | 
			
		||||
 | 
			
		||||
        // 从缓存中获取属性信息,如果缓存不存在,则创建并缓存
 | 
			
		||||
        var result = Instance.GetOrAdd(cacheKey, a =>
 | 
			
		||||
        {
 | 
			
		||||
            // 获取动态对象类型中指定名称的属性信息
 | 
			
		||||
            var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
 | 
			
		||||
                  .Where(pi => names.Contains(pi.Name)) // 筛选出指定属性名的属性信息
 | 
			
		||||
                  .Where(pi => pi != null) // 过滤空属性信息
 | 
			
		||||
                  .AsEnumerable();
 | 
			
		||||
 | 
			
		||||
            // 检查是否找到了所有指定名称的属性,如果没有找到,则抛出异常
 | 
			
		||||
            if (names.Length != properties.Count())
 | 
			
		||||
            {
 | 
			
		||||
                throw new InvalidOperationException($"Couldn't find properties on type:{type.Name},{Environment.NewLine}names:{namesStr}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return properties; // 返回属性信息集合
 | 
			
		||||
        }, 3600); // 缓存有效期为3600秒
 | 
			
		||||
 | 
			
		||||
        return result!; // 返回属性信息集合
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static IEnumerable<IGrouping<object[], dynamic>> GroupByKeys(this IEnumerable<dynamic> values, IEnumerable<string> keys)
 | 
			
		||||
    {
 | 
			
		||||
        // 获取动态对象集合中指定键的属性信息
 | 
			
		||||
        var properties = GetProperties(values, keys.ToArray());
 | 
			
		||||
 | 
			
		||||
        // 使用对象数组作为键进行分组
 | 
			
		||||
        return values.GroupBy(v => properties.Select(property => property.GetValue(v)).ToArray(), new ArrayEqualityComparer());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否都包含
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,8 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <param name="body">要执行的操作</param>
 | 
			
		||||
    public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body)
 | 
			
		||||
    {
 | 
			
		||||
        // 创建并行操作的选项对象,设置最大并行度为当前处理器数量的一半
 | 
			
		||||
        ParallelOptions options = new();
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount / 2 == 0 ? 1 : Environment.ProcessorCount / 2;
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount;
 | 
			
		||||
        // 使用 Parallel.ForEach 执行指定的操作
 | 
			
		||||
        Parallel.ForEach(source, options, (variable) =>
 | 
			
		||||
        {
 | 
			
		||||
@@ -42,9 +41,8 @@ public static class ParallelExtensions
 | 
			
		||||
    /// <param name="body">要执行的操作</param>
 | 
			
		||||
    public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T, ParallelLoopState, long> body)
 | 
			
		||||
    {
 | 
			
		||||
        // 创建并行操作的选项对象,设置最大并行度为当前处理器数量的一半
 | 
			
		||||
        ParallelOptions options = new();
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount / 2 == 0 ? 1 : Environment.ProcessorCount / 2;
 | 
			
		||||
        options.MaxDegreeOfParallelism = Environment.ProcessorCount;
 | 
			
		||||
        // 使用 Parallel.ForEach 执行指定的操作
 | 
			
		||||
        Parallel.ForEach(source, options, (variable, state, index) =>
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,13 @@ public class Startup : AppStartup
 | 
			
		||||
    {
 | 
			
		||||
        services.AddBootstrapBlazor(
 | 
			
		||||
            option => option.JSModuleVersion = Random.Shared.Next(10000).ToString()
 | 
			
		||||
            , jsonLocalizationOptions =>
 | 
			
		||||
            {
 | 
			
		||||
                jsonLocalizationOptions.DisableGetLocalizerFromResourceManager = true;
 | 
			
		||||
                jsonLocalizationOptions.DisableGetLocalizerFromService = true;
 | 
			
		||||
                jsonLocalizationOptions.IgnoreLocalizerMissing = true;
 | 
			
		||||
                jsonLocalizationOptions.UseKeyWhenValueIsNull = true;
 | 
			
		||||
            }
 | 
			
		||||
            );
 | 
			
		||||
        services.AddConfigurableOptions<MenuOptions>();
 | 
			
		||||
        services.ConfigureIconThemeOptions(options => options.ThemeKey = "fa");
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.6.2" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.7.0" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
//下载文件
 | 
			
		||||
export function blazor_downloadFile(url, fileName, dtoObject) {
 | 
			
		||||
export async function blazor_downloadFile(url, fileName, dtoObject) {
 | 
			
		||||
    const params = new URLSearchParams();
 | 
			
		||||
 | 
			
		||||
    // 将 dtoObject 的属性添加到 URLSearchParams 中
 | 
			
		||||
@@ -12,97 +12,92 @@ export function blazor_downloadFile(url, fileName, dtoObject) {
 | 
			
		||||
    // 构建完整的 URL
 | 
			
		||||
    const fullUrl = `${url}?${params.toString()}`;
 | 
			
		||||
 | 
			
		||||
    // 发起 fetch 请求
 | 
			
		||||
    fetch(fullUrl)
 | 
			
		||||
        .then(response => {
 | 
			
		||||
            // 获取响应头中的 content-disposition
 | 
			
		||||
            const dispositionHeader = response.headers.get('content-disposition');
 | 
			
		||||
            let resolvedFileName = fileName;
 | 
			
		||||
    try {
 | 
			
		||||
        // 发起 fetch 请求
 | 
			
		||||
        const response = await fetch(fullUrl);
 | 
			
		||||
 | 
			
		||||
            if (dispositionHeader) {
 | 
			
		||||
                // 解析出文件名
 | 
			
		||||
                const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(dispositionHeader);
 | 
			
		||||
                const serverFileName = match && match[1] ? match[1].replace(/['"]/g, '') : null;
 | 
			
		||||
                if (serverFileName) {
 | 
			
		||||
                    resolvedFileName = serverFileName;
 | 
			
		||||
                }
 | 
			
		||||
        // 获取响应头中的 content-disposition
 | 
			
		||||
        const dispositionHeader = response.headers.get('content-disposition');
 | 
			
		||||
        let resolvedFileName = fileName;
 | 
			
		||||
 | 
			
		||||
        if (dispositionHeader) {
 | 
			
		||||
            // 解析出文件名
 | 
			
		||||
            const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(dispositionHeader);
 | 
			
		||||
            const serverFileName = match && match[1] ? match[1].replace(/['"]/g, '') : null;
 | 
			
		||||
            if (serverFileName) {
 | 
			
		||||
                resolvedFileName = serverFileName;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // 将响应转换为 blob 对象
 | 
			
		||||
            return response.blob().then(blob => {
 | 
			
		||||
                // 创建临时的文件 URL
 | 
			
		||||
                const fileUrl = window.URL.createObjectURL(blob);
 | 
			
		||||
        // 将响应转换为 blob 对象
 | 
			
		||||
        const blob = await response.blob();
 | 
			
		||||
 | 
			
		||||
                // 创建一个 <a> 元素并设置下载链接和文件名
 | 
			
		||||
                const anchorElement = document.createElement('a');
 | 
			
		||||
                anchorElement.href = fileUrl;
 | 
			
		||||
                anchorElement.download = resolvedFileName;
 | 
			
		||||
                anchorElement.style.display = 'none';
 | 
			
		||||
        // 创建临时的文件 URL
 | 
			
		||||
        const fileUrl = window.URL.createObjectURL(blob);
 | 
			
		||||
 | 
			
		||||
                // 将 <a> 元素添加到 body 中并触发下载
 | 
			
		||||
                document.body.appendChild(anchorElement);
 | 
			
		||||
                anchorElement.click();
 | 
			
		||||
                document.body.removeChild(anchorElement);
 | 
			
		||||
        // 创建一个 <a> 元素并设置下载链接和文件名
 | 
			
		||||
        const anchorElement = document.createElement('a');
 | 
			
		||||
        anchorElement.href = fileUrl;
 | 
			
		||||
        anchorElement.download = resolvedFileName;
 | 
			
		||||
        anchorElement.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
                // 撤销临时的文件 URL
 | 
			
		||||
                window.URL.revokeObjectURL(fileUrl);
 | 
			
		||||
            });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
            console.error('DownFile error ', error);
 | 
			
		||||
        });
 | 
			
		||||
        // 将 <a> 元素添加到 body 中并触发下载
 | 
			
		||||
        document.body.appendChild(anchorElement);
 | 
			
		||||
        anchorElement.click();
 | 
			
		||||
        document.body.removeChild(anchorElement);
 | 
			
		||||
 | 
			
		||||
        // 撤销临时的文件 URL
 | 
			
		||||
        window.URL.revokeObjectURL(fileUrl);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('DownFile error ', error);
 | 
			
		||||
        throw error;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//下载文件
 | 
			
		||||
export function postJson_downloadFile(url, fileName, jsonBody) {
 | 
			
		||||
    const params = new URLSearchParams();
 | 
			
		||||
export async function postJson_downloadFile(url, fileName, jsonBody) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // 发起 fetch 请求
 | 
			
		||||
    fetch(url, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        body: jsonBody
 | 
			
		||||
    })
 | 
			
		||||
        .then(response => {
 | 
			
		||||
            // 获取响应头中的 content-disposition
 | 
			
		||||
            const dispositionHeader = response.headers.get('content-disposition');
 | 
			
		||||
            let resolvedFileName = fileName;
 | 
			
		||||
 | 
			
		||||
            if (dispositionHeader) {
 | 
			
		||||
                // 解析出文件名
 | 
			
		||||
                const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(dispositionHeader);
 | 
			
		||||
                const serverFileName = match && match[1] ? match[1].replace(/['"]/g, '') : null;
 | 
			
		||||
                if (serverFileName) {
 | 
			
		||||
                    resolvedFileName = serverFileName;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 将响应转换为 blob 对象
 | 
			
		||||
            return response.blob().then(blob => {
 | 
			
		||||
                // 创建临时的文件 URL
 | 
			
		||||
                const fileUrl = window.URL.createObjectURL(blob);
 | 
			
		||||
 | 
			
		||||
                // 创建一个 <a> 元素并设置下载链接和文件名
 | 
			
		||||
                const anchorElement = document.createElement('a');
 | 
			
		||||
                anchorElement.href = fileUrl;
 | 
			
		||||
                anchorElement.download = resolvedFileName;
 | 
			
		||||
                anchorElement.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
                // 将 <a> 元素添加到 body 中并触发下载
 | 
			
		||||
                document.body.appendChild(anchorElement);
 | 
			
		||||
                anchorElement.click();
 | 
			
		||||
                document.body.removeChild(anchorElement);
 | 
			
		||||
 | 
			
		||||
                // 撤销临时的文件 URL
 | 
			
		||||
                window.URL.revokeObjectURL(fileUrl);
 | 
			
		||||
            });
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
            console.error('downfile error ', error);
 | 
			
		||||
    try {
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: {
 | 
			
		||||
                'Content-Type': 'application/json'
 | 
			
		||||
            },
 | 
			
		||||
            body: jsonBody
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const dispositionHeader = response.headers.get('content-disposition');
 | 
			
		||||
        let resolvedFileName = fileName;
 | 
			
		||||
 | 
			
		||||
        if (dispositionHeader) {
 | 
			
		||||
            const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(dispositionHeader);
 | 
			
		||||
            const serverFileName = match && match[1] ? match[1].replace(/['"]/g, '') : null;
 | 
			
		||||
            if (serverFileName) {
 | 
			
		||||
                resolvedFileName = serverFileName;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blob = await response.blob();
 | 
			
		||||
        const fileUrl = window.URL.createObjectURL(blob);
 | 
			
		||||
 | 
			
		||||
        const anchorElement = document.createElement('a');
 | 
			
		||||
        anchorElement.href = fileUrl;
 | 
			
		||||
        anchorElement.download = resolvedFileName;
 | 
			
		||||
        anchorElement.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
        document.body.appendChild(anchorElement);
 | 
			
		||||
        anchorElement.click();
 | 
			
		||||
        document.body.removeChild(anchorElement);
 | 
			
		||||
 | 
			
		||||
        window.URL.revokeObjectURL(fileUrl);
 | 
			
		||||
 | 
			
		||||
        return true; // 唯一新增的返回值
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('downfile error ', error);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,11 @@ public class ClaimConst
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string UserId = "UserId";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// AvatarUrl
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string AvatarUrl = "AvatarUrl";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 验证Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/Admin/ThingsGateway.SqlSugar/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/Admin/ThingsGateway.SqlSugar/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
global using ThingsGateway.NewLife.Extension;
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class ClaimsPrincipalService : IClaimsPrincipalService
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public ClaimsPrincipal? User => App.User;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public interface IClaimsPrincipalService
 | 
			
		||||
{
 | 
			
		||||
    public ClaimsPrincipal? User { get; }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,10 +17,10 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class SugarAopService : ISugarAopService
 | 
			
		||||
{
 | 
			
		||||
    private IAppService _appService;
 | 
			
		||||
    public SugarAopService(IAppService appService)
 | 
			
		||||
    private IClaimsPrincipalService _claimsPrincipalService;
 | 
			
		||||
    public SugarAopService(IClaimsPrincipalService appService)
 | 
			
		||||
    {
 | 
			
		||||
        _appService = appService;
 | 
			
		||||
        _claimsPrincipalService = appService;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Aop设置
 | 
			
		||||
@@ -85,7 +85,7 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
                if (entityInfo.PropertyName == nameof(BaseEntity.CreateTime))
 | 
			
		||||
                    entityInfo.SetValue(DateTime.Now);
 | 
			
		||||
 | 
			
		||||
                if (_appService.User != null)
 | 
			
		||||
                if (_claimsPrincipalService.User != null)
 | 
			
		||||
                {
 | 
			
		||||
                    //创建人
 | 
			
		||||
                    if (entityInfo.PropertyName == nameof(BaseEntity.CreateUserId))
 | 
			
		||||
@@ -103,7 +103,7 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
                if (entityInfo.PropertyName == nameof(BaseEntity.UpdateTime))
 | 
			
		||||
                    entityInfo.SetValue(DateTime.Now);
 | 
			
		||||
                //更新人
 | 
			
		||||
                if (_appService.User != null)
 | 
			
		||||
                if (_claimsPrincipalService.User != null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (entityInfo.PropertyName == nameof(BaseEntity.UpdateUserId))
 | 
			
		||||
                        entityInfo.SetValue(UserManager.UserId);
 | 
			
		||||
@@ -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));
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user