Compare commits

...

51 Commits

Author SHA1 Message Date
Diego
ff41080dbd 更新授权类 2025-05-26 19:51:21 +08:00
Diego
0e28606e3d 10.6.23 2025-05-26 18:43:42 +08:00
Diego
6a025ceee5 序列化配置增加nan的情况 2025-05-26 17:41:13 +08:00
Diego
6b2e53d6dc 更新配置 2025-05-26 09:17:59 +08:00
2248356998 qq.com
b989aa5561 10.6.21 2025-05-26 00:05:16 +08:00
2248356998 qq.com
f5b0b7ebd2 fix: s7多写 2025-05-24 14:22:58 +08:00
2248356998 qq.com
16881ae076 fix: s7多写 2025-05-24 14:18:58 +08:00
2248356998 qq.com
af04112656 10.6.20
fix: s7多写
fix: opcuaserver写入数组
2025-05-24 14:11:10 +08:00
Diego
a2863112dc 2025-05-23 15:47:41 +08:00
Diego
f531e4dfc5 10.6.19 2025-05-23 13:16:57 +08:00
Diego
8db9b32ba7 10.6.18 2025-05-23 12:55:21 +08:00
Diego
dd5691cbef 更新依赖 2025-05-22 14:40:56 +08:00
Diego
de48b32af3 优化硬件信息曲线数据 2025-05-21 17:10:28 +08:00
Diego
600b5042a1 更新首页 2025-05-21 16:51:40 +08:00
Diego
aac77029da 修改数据保护密钥持久化位置 2025-05-21 13:15:39 +08:00
Diego
e50205f557 导出excel不再按设备名称和变量名称排序 2025-05-21 10:10:59 +08:00
Diego
e227411d1f fix: taos插件查询错误 2025-05-21 09:34:31 +08:00
2248356998 qq.com
2de0ed793f 调整json序列化内容 2025-05-20 23:21:58 +08:00
Diego
cb0276f273 fix: opcuamaster插件属性UI未正确显示 2025-05-20 15:20:14 +08:00
Diego
562b3f17c9 10.6.11 2025-05-19 23:16:49 +08:00
Diego
0f78f81c1c 10.6.7 2025-05-19 18:43:03 +08:00
Diego
6594937d0a 网关冗余备用站和同步插件支持 变量写入 2025-05-19 18:01:23 +08:00
Diego
64bc6084be 更新sln 2025-05-19 12:23:41 +08:00
Diego
20434d5bd2 10.6.5 2025-05-19 12:13:11 +08:00
Diego
9ecc9380e6 同步更改 2025-05-19 12:12:44 +08:00
Diego
a57c55080b 增加数据同步插件 2025-05-19 12:10:11 +08:00
2248356998 qq.com
b8ca06c6be fix: hybrid运行切换语言错误 2025-05-17 16:32:36 +08:00
Diego
5aff6461a1 fix: opcuaServer 业务设备刷新变量时可能导致内存泄露 2025-05-16 19:26:33 +08:00
Diego
6cf53fefec 优化内存占用 2025-05-16 18:00:36 +08:00
Diego
45132f3503 更新依赖 2025-05-16 10:51:44 +08:00
Diego
f1e78a0e8a 恢复配置json 2025-05-15 12:17:43 +08:00
Diego
0bf28ec275 更新语言资源 2025-05-15 12:17:09 +08:00
Diego
41f8412c97 支持达梦数据库 2025-05-15 12:15:32 +08:00
Diego
c535974362 更新规则引擎示例 2025-05-15 10:44:12 +08:00
Diego
1860c5f215 build:10.6.1 2025-05-15 09:12:33 +08:00
Diego
6d778b2d39 增加initDatabase配置项 2025-05-15 09:08:08 +08:00
Diego
f48b99c259 更新示例 2025-05-14 21:12:51 +08:00
Diego
3c73b93051 更新依赖 2025-05-14 18:52:19 +08:00
Diego
98f3f2d519 添加 `过滤离线变量` 插件属性 2025-05-14 13:01:12 +08:00
Diego
b76b4e8d68 变量表索引删除语句兼容性增强 2025-05-13 19:27:50 +08:00
Diego
07285a7c61 mqtt增加qos属性 2025-05-13 19:27:27 +08:00
Diego
03c0dfef37 增加业务设备日志 2025-05-12 15:26:51 +08:00
Diego
6ef6929c35 优化api权限树 2025-05-12 10:21:41 +08:00
Diego
e3c0c173f0 更新依赖 2025-05-12 08:53:25 +08:00
Diego
7d64e058d4 更新依赖 2025-05-08 16:34:48 +08:00
Diego
e97ee9b64b 10.5.15 2025-05-07 22:08:54 +08:00
Diego
6a03e39eeb 10.5.14 2025-05-07 22:00:31 +08:00
Diego
525ec740b5 添加脚本demo 2025-05-06 11:43:56 +08:00
2248356998 qq.com
b790cf5f4e 更新依赖 2025-05-05 20:25:43 +08:00
Diego
d1248811fd build: 10.5.11 2025-04-30 23:05:36 +08:00
2248356998 qq.com
022d016e8e feat: 添加采集组 2025-04-30 23:04:51 +08:00
390 changed files with 9606 additions and 3939 deletions

View File

@@ -64,24 +64,31 @@ public sealed class OperDescAttribute : MoAttribute
public override void OnException(MethodContext context) public override void OnException(MethodContext context)
{ {
//插入异常日志 if (App.HttpContext.Request.Path.StartsWithSegments("/_blazor"))
SysOperateLog log = GetOperLog(LocalizerType, context); {
//插入异常日志
SysOperateLog log = GetOperLog(LocalizerType, context);
log.Category = LogCateGoryEnum.Exception;//操作类型为异常 log.Category = LogCateGoryEnum.Exception;//操作类型为异常
log.ExeStatus = false;//操作状态为失败 log.ExeStatus = false;//操作状态为失败
if (context.Exception is AppFriendlyException exception) if (context.Exception is AppFriendlyException exception)
log.ExeMessage = exception?.Message; log.ExeMessage = exception?.Message;
else else
log.ExeMessage = context.Exception?.ToString(); log.ExeMessage = context.Exception?.ToString();
OperDescAttribute.WriteToQueue(log); OperDescAttribute.WriteToQueue(log);
}
} }
public override void OnSuccess(MethodContext context) public override void OnSuccess(MethodContext context)
{ {
//插入操作日志 if (App.HttpContext.Request.Path.StartsWithSegments("/_blazor"))
SysOperateLog log = GetOperLog(LocalizerType, context); {
OperDescAttribute.WriteToQueue(log);
//插入操作日志
SysOperateLog log = GetOperLog(LocalizerType, context);
OperDescAttribute.WriteToQueue(log);
}
} }
/// <summary> /// <summary>
@@ -115,7 +122,7 @@ public sealed class OperDescAttribute : MoAttribute
private SysOperateLog GetOperLog(Type? localizerType, MethodContext context) private SysOperateLog GetOperLog(Type? localizerType, MethodContext context)
{ {
var methodBase = context.Method; var methodBase = context.Method;
var clientInfo = AppService.ClientInfo; var userAgent = AppService.UserAgent;
string? paramJson = null; string? paramJson = null;
if (IsRecordPar) if (IsRecordPar)
{ {
@@ -127,10 +134,10 @@ public sealed class OperDescAttribute : MoAttribute
{ {
parametersDict[parametersInfo[i].Name!] = args[i]; parametersDict[parametersInfo[i].Name!] = args[i];
} }
paramJson = parametersDict.ToJsonNetString(); paramJson = parametersDict.ToSystemTextJsonString();
} }
var result = context.ReturnValue; var result = context.ReturnValue;
var resultJson = IsRecordPar ? result?.ToJsonNetString() : null; var resultJson = IsRecordPar ? result?.ToSystemTextJsonString() : null;
//操作日志表实体 //操作日志表实体
var log = new SysOperateLog var log = new SysOperateLog
{ {
@@ -138,8 +145,8 @@ public sealed class OperDescAttribute : MoAttribute
Category = LogCateGoryEnum.Operate, Category = LogCateGoryEnum.Operate,
ExeStatus = true, ExeStatus = true,
OpIp = AppService?.RemoteIpAddress ?? string.Empty, OpIp = AppService?.RemoteIpAddress ?? string.Empty,
OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major, OpBrowser = userAgent?.Browser,
OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major, OpOs = userAgent?.Platform,
OpTime = DateTime.Now, OpTime = DateTime.Now,
OpAccount = UserManager.UserAccount, OpAccount = UserManager.UserAccount,
ReqUrl = null, ReqUrl = null,

View File

@@ -15,7 +15,7 @@ namespace ThingsGateway.Admin.Application;
[ApiDescriptionSettings(false)] [ApiDescriptionSettings(false)]
[Route("api/auth")] [Route("api/auth")]
[LoggingMonitor] [RequestAudit]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;

View File

@@ -25,7 +25,8 @@ namespace ThingsGateway.Admin.Application;
[Description("登录")] [Description("登录")]
[Route("openapi/auth")] [Route("openapi/auth")]
[Authorize(AuthenticationSchemes = "Bearer")] [Authorize(AuthenticationSchemes = "Bearer")]
[LoggingMonitor] [RequestAudit]
[ApiController]
public class OpenApiController : ControllerBase public class OpenApiController : ControllerBase
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;

View File

@@ -15,16 +15,13 @@ namespace ThingsGateway.Admin.Application;
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
[AllowAnonymous] [AllowAnonymous]
[ApiController]
public class TestController : ControllerBase public class TestController : ControllerBase
{ {
[HttpPost] [HttpGet]
public Task Test(string data) public void Test()
{ {
for (int i = 0; i < 3; i++) GC.Collect();
{ GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
return Task.CompletedTask;
} }
} }

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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
{
}

View File

@@ -51,7 +51,7 @@ public class HardwareInfo
/// 进程占用内存 /// 进程占用内存
/// </summary> /// </summary>
[AutoGenerateColumn(Ignore = true)] [AutoGenerateColumn(Ignore = true)]
public string WorkingSet { get; set; } public int WorkingSet { get; set; }
/// <summary> /// <summary>
/// 更新时间 /// 更新时间

View File

@@ -17,6 +17,7 @@ using System.Runtime.InteropServices;
using ThingsGateway.Extension; using ThingsGateway.Extension;
using ThingsGateway.NewLife; using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Threading; using ThingsGateway.NewLife.Threading;
using ThingsGateway.Schedule; using ThingsGateway.Schedule;
@@ -51,11 +52,20 @@ public class HardwareJob : IJob, IHardwareJob
#endregion #endregion
private MemoryCache MemoryCache = new() { };
private const string CacheKey = "HistoryHardwareInfo";
/// <inheritdoc/> /// <inheritdoc/>
public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos() public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
{ {
using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew(); var historyHardwareInfos = MemoryCache.Get<List<HistoryHardwareInfo>>(CacheKey);
return await db.Queryable<HistoryHardwareInfo>().ToListAsync().ConfigureAwait(false); 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; private bool error = false;
@@ -94,7 +104,7 @@ public class HardwareJob : IJob, IHardwareJob
{ {
HardwareInfo.MachineInfo.Refresh(); HardwareInfo.MachineInfo.Refresh();
HardwareInfo.UpdateTime = TimerX.Now.ToDefaultDateTimeFormat(); 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; error = false;
} }
catch (Exception ex) catch (Exception ex)
@@ -116,17 +126,22 @@ public class HardwareJob : IJob, IHardwareJob
var his = new HistoryHardwareInfo() var his = new HistoryHardwareInfo()
{ {
Date = TimerX.Now, Date = TimerX.Now,
DriveUsage = (100 - (HardwareInfo.DriveInfo.TotalFreeSpace * 100.00 / HardwareInfo.DriveInfo.TotalSize)).ToString("F2"), DriveUsage = (100 - (HardwareInfo.DriveInfo.TotalFreeSpace * 100.00 / HardwareInfo.DriveInfo.TotalSize)).ToInt(),
Battery = (HardwareInfo.MachineInfo.Battery * 100).ToString("F2"), Battery = (HardwareInfo.MachineInfo.Battery * 100).ToInt(),
MemoryUsage = (HardwareInfo.WorkingSet), MemoryUsage = (HardwareInfo.WorkingSet),
CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToString("F2"), CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToInt(),
Temperature = (HardwareInfo.MachineInfo.Temperature).ToString("F2"), Temperature = (HardwareInfo.MachineInfo.Temperature).ToInt(),
}; };
await db.Insertable(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false); await db.Insertable(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
MemoryCache.Remove(CacheKey);
} }
var sevenDaysAgo = TimerX.Now.AddDays(-HardwareInfoOptions.DaysAgo); 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; error = false;

View File

@@ -19,23 +19,23 @@ public class HistoryHardwareInfo
{ {
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "磁盘使用率")] [SugarColumn(ColumnDescription = "磁盘使用率")]
public string DriveUsage { get; set; } public int DriveUsage { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "内存")] [SugarColumn(ColumnDescription = "内存")]
public string MemoryUsage { get; set; } public int MemoryUsage { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "CPU使用率")] [SugarColumn(ColumnDescription = "CPU使用率")]
public string CpuUsage { get; set; } public int CpuUsage { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "温度")] [SugarColumn(ColumnDescription = "温度")]
public string Temperature { get; set; } public int Temperature { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "电池")] [SugarColumn(ColumnDescription = "电池")]
public string Battery { get; set; } public int Battery { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
[SugarColumn(ColumnDescription = "时间")] [SugarColumn(ColumnDescription = "时间")]

View File

@@ -1,4 +1,15 @@
{ {
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "CreateOrgId"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"CreateUser": "CreateUser",
"UpdateTime": "UpdateTime",
"UpdateUser": "UpdateUser"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": { "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "User expired, please login again" "UserExpire": "User expired, please login again"
}, },
@@ -24,9 +35,6 @@
"LatestLoginTime": "LatestLoginTime", "LatestLoginTime": "LatestLoginTime",
"LatestLoginDevice": "LatestLoginDevice", "LatestLoginDevice": "LatestLoginDevice",
"LatestLoginAddress": "LatestLoginAddress", "LatestLoginAddress": "LatestLoginAddress",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"OrgNames": "OrgNames", "OrgNames": "OrgNames",
"PositionName": "PositionName", "PositionName": "PositionName",
"OrgId": "Org", "OrgId": "Org",
@@ -60,9 +68,6 @@
"Name": "Name", "Name": "Name",
"Name.Required": "{0} is required", "Name.Required": "{0} is required",
"Category": "Category", "Category": "Category",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"OrgId": "Org", "OrgId": "Org",
"Global": "Global", "Global": "Global",
"Status": "Status", "Status": "Status",
@@ -105,9 +110,6 @@
"Category": "Category", "Category": "Category",
"Target": "Target", "Target": "Target",
"NavLinkMatch": "NavLinkMatch", "NavLinkMatch": "NavLinkMatch",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"ParentId": "Parent", "ParentId": "Parent",
"ResourceDup": "Duplicate name {0} exists", "ResourceDup": "Duplicate name {0} exists",
"ResourceParentChoiceSelf": "Parent cannot choose itself", "ResourceParentChoiceSelf": "Parent cannot choose itself",
@@ -134,9 +136,6 @@
"Status": "Status", "Status": "Status",
"OrgId": "Organization", "OrgId": "Organization",
"Remark": "Remarks", "Remark": "Remarks",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"Dup": "Duplicate position exists with Category {0} and Name {1}", "Dup": "Duplicate position exists with Category {0} and Name {1}",
"CodeDup": "Duplicate code {0} exists", "CodeDup": "Duplicate code {0} exists",
"NameDup": "Duplicate name {0} exists", "NameDup": "Duplicate name {0} exists",
@@ -159,9 +158,6 @@
"Names": "Names", "Names": "Names",
"Remark": "Remarks", "Remark": "Remarks",
"DirectorId": "Director", "DirectorId": "Director",
"SortCode": "SortCode",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"Dup": "Duplicate organization exists with Category {0} and Name {1}", "Dup": "Duplicate organization exists with Category {0} and Name {1}",
"CodeDup": "Duplicate code {0} exists", "CodeDup": "Duplicate code {0} exists",
"NameDup": "Duplicate name {0} exists", "NameDup": "Duplicate name {0} exists",
@@ -358,9 +354,6 @@
"Name": "Name", "Name": "Name",
"Code": "Code", "Code": "Code",
"Remark": "Remark", "Remark": "Remark",
"SortCode": "Sort",
"CreateTime": "CreateTime",
"UpdateTime": "UpdateTime",
"DemoCanotUpdateWebsitePolicy": "DEMO environment does not allow modifying website settings", "DemoCanotUpdateWebsitePolicy": "DEMO environment does not allow modifying website settings",
"DictDup": "Duplicate configuration exists, category {0}, name {1}" "DictDup": "Duplicate configuration exists, category {0}, name {1}"
}, },

View File

@@ -1,469 +1,462 @@
{ {
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "创建机构Id"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"SortCode": "排序",
"CreateTime": "创建时间",
"CreateUser": "创建人",
"UpdateTime": "更新时间",
"UpdateUser": "更新人"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": { "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "用户登录已过期,请重新登录" "UserExpire": "用户登录已过期,请重新登录"
}, },
"ThingsGateway.Admin.Application.SysUser": { "ThingsGateway.Admin.Application.SysUser": {
"Disable": "禁用", "Disable": "禁用",
"Enable": "启用", "Enable": "启用",
"GrantRole": "分配角色", "GrantRole": "分配角色",
"ExitVerificat": "您已被强制下线", "ExitVerificat": "您已被强制下线",
"PasswordEdited": "密码被修改,已退出登录", "PasswordEdited": "密码被修改,已退出登录",
"Avatar": "头像", "Avatar": "头像",
"Account": "账号", "Account": "账号",
"Account.Required": " {0} 是必填项", "Account.Required": " {0} 是必填项",
"Password": "密码", "Password": "密码",
"Status": "状态", "Status": "状态",
"Phone": "手机", "Phone": "手机",
"Email": "邮箱", "Email": "邮箱",
"LastLoginIp": "上次登录ip", "LastLoginIp": "上次登录ip",
"LastLoginDevice": "上次登录设备", "LastLoginDevice": "上次登录设备",
"LastLoginTime": "上次登录时间", "LastLoginTime": "上次登录时间",
"LastLoginAddress": "上次登录地点", "LastLoginAddress": "上次登录地点",
"LatestLoginIp": "最新登录ip", "LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间", "LatestLoginTime": "最新登录时间",
"LatestLoginDevice": "最新登录设备", "LatestLoginDevice": "最新登录设备",
"LatestLoginAddress": "最新登录地点", "LatestLoginAddress": "最新登录地点",
"SortCode": "排序", "OrgNames": "机构名称",
"CreateTime": "创建时间", "PositionName": "职位名称",
"UpdateTime": "更新时间", "OrgId": "机构",
"OrgNames": "机构名称", "PositionId": "职位",
"PositionName": "职位名称", "DirectorId": "主管",
"OrgId": "机构", "CheckSelf": "禁止 {0} 自己",
"PositionId": "职位", "CanotDeleteAdminUser": "不可删除系统内置超管用户",
"DirectorId": "主管", "CanotEditAdminUser": "不可编辑超管用户",
"CheckSelf": "禁止 {0} 自己", "CanotGrantAdmin": "不能分配超管角色",
"CanotDeleteAdminUser": "不可删除系统内置超管用户", "EmailDup": "存在重复的邮箱 {0}",
"CanotEditAdminUser": "不可编辑超管用户", "AccountDup": "存在重复的账号 {0}",
"CanotGrantAdmin": "不能分配超管角色", "CanotDeleteSelf": "不可删除自己",
"EmailDup": "存在重复的邮箱 {0}", "EmailError": "邮箱 {0} 格式错误",
"AccountDup": "存在重复的账号 {0}", "PhoneError": "手机号码 {0} 格式错误",
"CanotDeleteSelf": "不可删除自己", "NoOrg": "组织机构不存在",
"EmailError": "邮箱 {0} 格式错误", "DirectorSelf": "不能设置自己为主管",
"PhoneError": "手机号码 {0} 格式错误",
"NoOrg": "组织机构不存在",
"DirectorSelf": "不能设置自己为主管",
"DemoCanotUpdatePassword": "DEMO环境不允许修改密码", "DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
"OldPasswordError": "原密码错误", "OldPasswordError": "原密码错误",
"ConfirmPasswordDiff": "两次输入的密码不一致", "ConfirmPasswordDiff": "两次输入的密码不一致",
"PasswordLengthLess": "密码长度不能小于 {0} ", "PasswordLengthLess": "密码长度不能小于 {0} ",
"PasswordMustNum ": "密码必须包含数字", "PasswordMustNum ": "密码必须包含数字",
"PasswordMustLow": "密码必须包含小写字母", "PasswordMustLow": "密码必须包含小写字母",
"PasswordMustUpp": "密码必须包含大写字母", "PasswordMustUpp": "密码必须包含大写字母",
"PasswordMustSpecial": "密码必须包含特殊字符" "PasswordMustSpecial": "密码必须包含特殊字符"
}, },
"ThingsGateway.Admin.Application.SysRole": { "ThingsGateway.Admin.Application.SysRole": {
"Code": "编码", "Code": "编码",
"Name": "名称", "Name": "名称",
"Name.Required": " {0} 是必填项", "Name.Required": " {0} 是必填项",
"Category": "分类", "Category": "分类",
"SortCode": "排序", "Global": "全局",
"Global": "全局", "Status": "状态",
"Status": "状态", "OrgId": "机构",
"OrgId": "机构",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"CanotDeleteAdmin": "不可删除系统内置超管角色", "CanotDeleteAdmin": "不可删除系统内置超管角色",
"CanotEditAdmin": "不可编辑超管角色", "CanotEditAdmin": "不可编辑超管角色",
"CanotGrantAdmin": "不能分配超管角色", "CanotGrantAdmin": "不能分配超管角色",
"NameDup": "存在重复的角色名称 {0}", "NameDup": "存在重复的角色名称 {0}",
"OrgNotNull": "机构不能为空", "OrgNotNull": "机构不能为空",
"SameOrgNameDup": "存在重复的角色名称 {0}", "SameOrgNameDup": "存在重复的角色名称 {0}",
"CannotRoleScopeAll": "机构角色不能选择全局数据范围", "CannotRoleScopeAll": "机构角色不能选择全局数据范围",
"CodeDup": "存在重复的编码 {0}" "CodeDup": "存在重复的编码 {0}"
}, },
"ThingsGateway.Admin.Application.RoleCategoryEnum": { "ThingsGateway.Admin.Application.RoleCategoryEnum": {
"Global": "全局", "Global": "全局",
"Org": "机构" "Org": "机构"
}, },
"ThingsGateway.Admin.Application.DataScopeEnum": { "ThingsGateway.Admin.Application.DataScopeEnum": {
"SCOPE_SELF": "仅自己", "SCOPE_SELF": "仅自己",
"SCOPE_ALL": "全部", "SCOPE_ALL": "全部",
"SCOPE_ORG": "仅所属组织", "SCOPE_ORG": "仅所属组织",
"SCOPE_ORG_CHILD": "所属组织及以下", "SCOPE_ORG_CHILD": "所属组织及以下",
"SCOPE_ORG_DEFINE": "自定义" "SCOPE_ORG_DEFINE": "自定义"
}, },
"ThingsGateway.Admin.Application.DefaultDataScope": { "ThingsGateway.Admin.Application.DefaultDataScope": {
"ScopeCategory": "数据范围", "ScopeCategory": "数据范围",
"ScopeDefineOrgIdList": "自定义列表" "ScopeDefineOrgIdList": "自定义列表"
}, },
"ThingsGateway.Admin.Application.SysResource": { "ThingsGateway.Admin.Application.SysResource": {
"Title": "标题", "Title": "标题",
"Module": "模块", "Module": "模块",
"Title.Required": "{0} 是必填项", "Title.Required": "{0} 是必填项",
"Href.Required": "{0} 是必填项", "Href.Required": "{0} 是必填项",
"Icon": "图标", "Icon": "图标",
"Href": "路径", "Href": "路径",
"Code": "编码", "Code": "编码",
"Category": "分类", "Category": "分类",
"Target": "跳转类型", "Target": "跳转类型",
"NavLinkMatch": "匹配类型", "NavLinkMatch": "匹配类型",
"SortCode": "排序", "ParentId": "上级菜单",
"ParentId": "上级菜单", "ResourceDup": "存在重复的名称 {0}",
"CreateTime": "创建时间", "ResourceParentChoiceSelf": "父级不能选择自己",
"UpdateTime": "更新时间", "ResourceParentNull": "父级不存在 {0}",
"ResourceDup": "存在重复的名称 {0}", "NotFoundResource": "系统异常,没找到该菜单",
"ResourceParentChoiceSelf": "父级不能选择自己", "ModuleIdDiff": "模块与上级菜单不一致",
"ResourceParentNull": "父级不存在 {0}", "CanotDeleteSystemResource": "不可删除系统资源 {0}",
"NotFoundResource": "系统异常,没找到该菜单", "ResourceMenuHrefNotNull": "菜单的路径不能为空"
"ModuleIdDiff": "模块与上级菜单不一致", },
"CanotDeleteSystemResource": "不可删除系统资源 {0}",
"ResourceMenuHrefNotNull": "菜单的路径不能为空"
},
"ThingsGateway.Admin.Application.SysOrgCopyInput": { "ThingsGateway.Admin.Application.SysOrgCopyInput": {
"TargetId": "目标机构", "TargetId": "目标机构",
"ContainsChild": "包含下级", "ContainsChild": "包含下级",
"ContainsPosition": "包含职位" "ContainsPosition": "包含职位"
}, },
"ThingsGateway.Admin.Application.SysPosition": { "ThingsGateway.Admin.Application.SysPosition": {
"Category.Required": "{0} 是必填项", "Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项", "Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项", "Code.Required": "{0} 是必填项",
"OrgId.MinValue": "{0} 是必填项", "OrgId.MinValue": "{0} 是必填项",
"Category": "分类", "Category": "分类",
"Name": "名称", "Name": "名称",
"Code": "代码", "Code": "代码",
"Status": "状态", "Status": "状态",
"OrgId": "机构", "OrgId": "机构",
"Remark": "备注", "Remark": "备注",
"SortCode": "排序", "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
"CreateTime": "创建时间", "CodeDup": "存在重复的编码 {0}",
"UpdateTime": "更新时间", "NameDup": "存在重复的名称 {0}",
"Dup": "存在重复的岗位 分类 {0} 名称 {1}", "CanotContainsSelf": "不可包含自己",
"CodeDup": "存在重复的编码 {0}", "TargetNameDup": "目标节点存在重复的名称 {0}",
"NameDup": "存在重复的名称 {0}", "ParentChoiceSelf": "父级不能选择自己",
"CanotContainsSelf": "不可包含自己", "ParentNull": "父级不存在 {0}",
"TargetNameDup": "目标节点存在重复的名称 {0}", "DeleteUserFirst": "请先删除职位下的用户"
"ParentChoiceSelf": "父级不能选择自己",
"ParentNull": "父级不存在 {0}",
"DeleteUserFirst": "请先删除职位下的用户"
}, },
"ThingsGateway.Admin.Application.SysOrg": { "ThingsGateway.Admin.Application.SysOrg": {
"Category.Required": "{0} 是必填项", "Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项", "Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项", "Code.Required": "{0} 是必填项",
"Category": "分类", "Category": "分类",
"Name": "名称", "Name": "名称",
"Code": "代码", "Code": "代码",
"Status": "状态", "Status": "状态",
"ParentId": "上级机构", "ParentId": "上级机构",
"Names": "机构全称", "Names": "机构全称",
"Remark": "备注", "Remark": "备注",
"DirectorId": "主管", "DirectorId": "主管",
"SortCode": "排序", "Dup": "存在重复的机构 分类 {0} 名称 {1}",
"CreateTime": "创建时间", "CodeDup": "存在重复的编码 {0}",
"UpdateTime": "更新时间", "NameDup": "存在重复的名称 {0}",
"Dup": "存在重复的机构 分类 {0} 名称 {1}", "CanotContainsSelf": "不可包含自己",
"CodeDup": "存在重复的编码 {0}", "TargetNameDup": "目标节点存在重复的名称 {0}",
"NameDup": "存在重复的名称 {0}", "ParentChoiceSelf": "父级不能选择自己",
"CanotContainsSelf": "不可包含自己", "ParentNull": "父级不存在 {0}",
"TargetNameDup": "目标节点存在重复的名称 {0}", "DeleteUserFirst": "请先删除机构下的用户",
"ParentChoiceSelf": "父级不能选择自己", "DeleteRoleFirst": "请先删除机构下的角色",
"ParentNull": "父级不存在 {0}", "DeletePositionFirst": "请先删除机构下的职位",
"DeleteUserFirst": "请先删除机构下的用户", "RootOrg": "无法创建顶层机构"
"DeleteRoleFirst": "请先删除机构下的角色", },
"DeletePositionFirst": "请先删除机构下的职位", "ThingsGateway.Admin.Application.OrgEnum": {
"RootOrg": "无法创建顶层机构" "COMPANY": "公司",
}, "DEPT": "部门"
"ThingsGateway.Admin.Application.OrgEnum": { },
"COMPANY": "公司", "ThingsGateway.Admin.Application.PositionCategoryEnum": {
"DEPT": "部门" "HIGH": "高层",
}, "MIDDLE": "中层",
"ThingsGateway.Admin.Application.PositionCategoryEnum": { "LOW": "低层"
"HIGH": "高层", },
"MIDDLE": "中层",
"LOW": "低层"
},
//controller //controller
"ThingsGateway.Admin.Application.AuthController": { "ThingsGateway.Admin.Application.AuthController": {
//auth //auth
"AuthController": "登录API", "AuthController": "登录API",
"LoginAsync": "登录", "LoginAsync": "登录",
"LogoutAsync": "注销" "LogoutAsync": "注销"
}, },
"ThingsGateway.Admin.Application.TestController": { "ThingsGateway.Admin.Application.TestController": {
//auth //auth
"TestController": "测试API", "TestController": "测试API",
"Test": "测试" "Test": "测试"
}, },
"ThingsGateway.Admin.Application.OpenApiAuthController": { "ThingsGateway.Admin.Application.OpenApiAuthController": {
//auth //auth
"OpenApiAuthController": "登录API", "OpenApiAuthController": "登录API",
"LoginAsync": "登录", "LoginAsync": "登录",
"LogoutAsync": "注销" "LogoutAsync": "注销"
}, },
"ThingsGateway.Admin.Application.FileService": { "ThingsGateway.Admin.Application.FileService": {
"FileNullError": "文件不能为空", "FileNullError": "文件不能为空",
"FileLengthError": "文件大小不允许超过 {0} M", "FileLengthError": "文件大小不允许超过 {0} M",
"FileTypeError": "不支持 {0} 格式" "FileTypeError": "不支持 {0} 格式"
}, },
"ThingsGateway.Admin.Application.UnifyResultProvider": { "ThingsGateway.Admin.Application.UnifyResultProvider": {
"TokenOver": "登录已过期,请重新登录", "TokenOver": "登录已过期,请重新登录",
"NoPermission": "禁止访问,没有权限" "NoPermission": "禁止访问,没有权限"
}, },
"ThingsGateway.Admin.Application.AuthService": { "ThingsGateway.Admin.Application.AuthService": {
"TenantNull": "租户不存在", "TenantNull": "租户不存在",
"OrgDisable": "所属公司/部门已停用,请联系管理员", "OrgDisable": "所属公司/部门已停用,请联系管理员",
"SingleLoginWarn": "您的账号已在别处登录", "SingleLoginWarn": "您的账号已在别处登录",
"UserNull": "用户 {0} 不存在", "UserNull": "用户 {0} 不存在",
"PasswordError": "密码错误次数过多,请 {0} 分钟后再试", "PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
"AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ", "AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
"UserDisable": "账号 {0} 已停用", "UserDisable": "账号 {0} 已停用",
"MustDesc": "密码需要DESC加密后传入", "MustDesc": "密码需要DESC加密后传入",
"UserNoModule": "该账号未分配模块,请联系管理员" "UserNoModule": "该账号未分配模块,请联系管理员"
}, },
"ThingsGateway.Admin.Application.HardwareInfo": { "ThingsGateway.Admin.Application.HardwareInfo": {
"Environment": "主机环境", "Environment": "主机环境",
"FrameworkDescription": "NET框架", "FrameworkDescription": "NET框架",
"OsArchitecture": "系统架构", "OsArchitecture": "系统架构",
"UUID": "唯一编码", "UUID": "唯一编码",
"UpdateTime": "更新时间" "UpdateTime": "更新时间"
}, },
"ThingsGateway.Admin.Application.HistoryHardwareInfo": { "ThingsGateway.Admin.Application.HistoryHardwareInfo": {
"DriveUsage": "磁盘使用率", "DriveUsage": "磁盘使用率",
"MemoryUsage": "内存", "MemoryUsage": "内存",
"CpuUsage": "CPU使用率", "CpuUsage": "CPU使用率",
"Temperature": "温度", "Temperature": "温度",
"Battery": "电池" "Battery": "电池"
}, },
//oper //oper
"ThingsGateway.Admin.Application.OperDescAttribute": { "ThingsGateway.Admin.Application.OperDescAttribute": {
//dict //dict
"SaveDict": "修改字典", "SaveDict": "修改字典",
"DeleteDict": "删除字典", "DeleteDict": "删除字典",
"EditLoginPolicy": "修改登录策略", "EditLoginPolicy": "修改登录策略",
"EditPasswordPolicy": "修改密码策略", "EditPasswordPolicy": "修改密码策略",
"EditPagePolicy": "修改页面策略", "EditPagePolicy": "修改页面策略",
"EditWebsitePolicy": "修改网站设置", "EditWebsitePolicy": "修改网站设置",
//operlog //operlog
"DeleteOperLog": "删除操作日志", "DeleteOperLog": "删除操作日志",
"ExportOperLog": "导出操作日志", "ExportOperLog": "导出操作日志",
//resource //resource
"SaveResource": "修改资源", "SaveResource": "修改资源",
"DeleteResource": "删除资源", "DeleteResource": "删除资源",
//role //role
"SaveRole": "修改角色", "SaveRole": "修改角色",
"DeleteRole": "删除角色", "DeleteRole": "删除角色",
"RoleGrantResource": "角色授权资源", "RoleGrantResource": "角色授权资源",
"RoleGrantUser": "角色授权用户", "RoleGrantUser": "角色授权用户",
"RoleGrantApiPermission": "角色授权OpenApi", "RoleGrantApiPermission": "角色授权OpenApi",
"GrantApi": "API", "GrantApi": "API",
"GrantUser": "用户", "GrantUser": "用户",
"GrantRole": "角色", "GrantRole": "角色",
"GrantResource": "资源", "GrantResource": "资源",
//user //user
"SaveUser": "修改用户", "SaveUser": "修改用户",
"DeleteuSER": "删除用户", "DeleteuSER": "删除用户",
"ResetPassword": "重置密码", "ResetPassword": "重置密码",
"UserGrantRole": "用户授权角色", "UserGrantRole": "用户授权角色",
"UserGrantResource": "用户授权资源", "UserGrantResource": "用户授权资源",
"UserGrantApiPermission": "用户授权OpenApi", "UserGrantApiPermission": "用户授权OpenApi",
//usercenter //usercenter
"UpdateUserInfo": "更新个人信息", "UpdateUserInfo": "更新个人信息",
"WorkbenchInfo": "更新个人工作台", "WorkbenchInfo": "更新个人工作台",
"UpdatePassword": "更新个人密码", "UpdatePassword": "更新个人密码",
//session //session
"ExitVerificat": "强退令牌", "ExitVerificat": "强退令牌",
"ExitSession": "强退会话", "ExitSession": "强退会话",
"CopyOrg": "复制机构", "CopyOrg": "复制机构",
"DeleteOrg": "删除机构", "DeleteOrg": "删除机构",
"SaveOrg": "保存机构", "SaveOrg": "保存机构",
"DeletePosition": "删除岗位", "DeletePosition": "删除岗位",
"SavePosition": "保存岗位", "SavePosition": "保存岗位",
"NoPermission": "无权限操作", "NoPermission": "无权限操作",
"CopyResource": "复制资源", "CopyResource": "复制资源",
"ChangeParentResource": "更改父节点" "ChangeParentResource": "更改父节点"
}, },
//service //service
"ThingsGateway.Admin.Application.HardwareJob": { "ThingsGateway.Admin.Application.HardwareJob": {
"GetHardwareInfoFail": "获取硬件信息出错" "GetHardwareInfoFail": "获取硬件信息出错"
}, },
//dto //dto
"ThingsGateway.Admin.Application.UserSelectorOutput": { "ThingsGateway.Admin.Application.UserSelectorOutput": {
"Account": "账号", "Account": "账号",
"OrgId": "机构" "OrgId": "机构"
}, },
"ThingsGateway.Admin.Application.ResourceTableSearchModel": { "ThingsGateway.Admin.Application.ResourceTableSearchModel": {
"Module": "模块", "Module": "模块",
"Href": "路径", "Href": "路径",
"Title": "标题" "Title": "标题"
}, },
"ThingsGateway.Admin.Application.WorkbenchInfo": { "ThingsGateway.Admin.Application.WorkbenchInfo": {
"Razor": "主页", "Razor": "主页",
"Shortcuts": "快捷方式" "Shortcuts": "快捷方式"
}, },
"ThingsGateway.Admin.Application.UpdatePasswordInput": { "ThingsGateway.Admin.Application.UpdatePasswordInput": {
"Password": "密码", "Password": "密码",
"NewPassword": "新密码", "NewPassword": "新密码",
"ConfirmPassword": "确认密码", "ConfirmPassword": "确认密码",
"Password.Required": " {0} 是必填项", "Password.Required": " {0} 是必填项",
"NewPassword.Required": " {0} 是必填项", "NewPassword.Required": " {0} 是必填项",
"ConfirmPassword.Required": " {0} 是必填项" "ConfirmPassword.Required": " {0} 是必填项"
}, },
"ThingsGateway.Admin.Application.VerificatInfo": { "ThingsGateway.Admin.Application.VerificatInfo": {
"Expire": "过期时间(分)", "Expire": "过期时间(分)",
"Online": "在线状态", "Online": "在线状态",
"VerificatTimeout": "超时时间", "VerificatTimeout": "超时时间",
"Device": "登录设备", "Device": "登录设备",
"LoginIp": "登录IP", "LoginIp": "登录IP",
"LoginTime": "登录时间" "LoginTime": "登录时间"
}, },
"ThingsGateway.Admin.Application.SessionOutput": { "ThingsGateway.Admin.Application.SessionOutput": {
"Account": "账号", "Account": "账号",
"Online": "在线状态", "Online": "在线状态",
"LatestLoginIp": "最新登录ip", "LatestLoginIp": "最新登录ip",
"LatestLoginTime": "最新登录时间", "LatestLoginTime": "最新登录时间",
"VerificatCount": "令牌数量" "VerificatCount": "令牌数量"
}, },
"ThingsGateway.Admin.Application.SysDict": { "ThingsGateway.Admin.Application.SysDict": {
"Category.Required": "{0} 是必填项", "Category.Required": "{0} 是必填项",
"Name.Required": "{0} 是必填项", "Name.Required": "{0} 是必填项",
"Code.Required": "{0} 是必填项", "Code.Required": "{0} 是必填项",
"Category": "分类", "Category": "分类",
"Name": "名称", "Name": "名称",
"Code": "代码", "Code": "代码",
"Remark": "备注", "Remark": "备注",
"SortCode": "排序", "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
"CreateTime": "创建时间", "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
"UpdateTime": "更新时间", },
"DictDup": "存在重复的配置 分类 {0} 名称 {1}",
"DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
},
"ThingsGateway.Admin.Application.SysOperateLog": { "ThingsGateway.Admin.Application.SysOperateLog": {
"ClassName": "类名", "ClassName": "类名",
"ExeMessage": "具体消息", "ExeMessage": "具体消息",
"MethodName": "方法名称", "MethodName": "方法名称",
"ParamJson": "请求参数", "ParamJson": "请求参数",
"ReqMethod": "请求方式", "ReqMethod": "请求方式",
"ReqUrl": "请求地址", "ReqUrl": "请求地址",
"ResultJson": "返回结果", "ResultJson": "返回结果",
"Category": "日志分类", "Category": "日志分类",
"ExeStatus": "执行状态", "ExeStatus": "执行状态",
"Name": "日志名称", "Name": "日志名称",
"OpAccount": "账号", "OpAccount": "账号",
"OpBrowser": "浏览器", "OpBrowser": "浏览器",
"OpIp": "ip", "OpIp": "ip",
"OpOs": "系统", "OpOs": "系统",
"OpTime": "操作时间", "OpTime": "操作时间",
"VerificatId": "验证Id" "VerificatId": "验证Id"
}, },
"ThingsGateway.Admin.Application.OperateLogPageInput": { "ThingsGateway.Admin.Application.OperateLogPageInput": {
"SearchDate": "时间范围", "SearchDate": "时间范围",
"Account": "操作账号", "Account": "操作账号",
"Category": "分类" "Category": "分类"
}, },
"ThingsGateway.Admin.Application.LoginInput": { "ThingsGateway.Admin.Application.LoginInput": {
"Account": "登录账号", "Account": "登录账号",
"Password": "登录密码", "Password": "登录密码",
"Account.Required": "{0} 是必填项", "Account.Required": "{0} 是必填项",
"Password.Required": "{0} 是必填项" "Password.Required": "{0} 是必填项"
}, },
"ThingsGateway.Admin.Application.LogoutInput": { "ThingsGateway.Admin.Application.LogoutInput": {
"VerificatId.Required": "{0} 是必填项" "VerificatId.Required": "{0} 是必填项"
}, },
"ThingsGateway.Admin.Application.AppConfig": { "ThingsGateway.Admin.Application.AppConfig": {
"LoginPolicy": "登录策略", "LoginPolicy": "登录策略",
"PasswordPolicy": "密码策略", "PasswordPolicy": "密码策略",
"PagePolicy": "页面设置", "PagePolicy": "页面设置",
"WebsitePolicy": "网站设置" "WebsitePolicy": "网站设置"
}, },
"ThingsGateway.Admin.Application.LoginPolicy": { "ThingsGateway.Admin.Application.LoginPolicy": {
"SingleOpen": "单用户登录开关", "SingleOpen": "单用户登录开关",
"ErrorLockTime": "登录错误锁定时长(分)", "ErrorLockTime": "登录错误锁定时长(分)",
"ErrorResetTime": "登录错误次数过期时长(分)", "ErrorResetTime": "登录错误次数过期时长(分)",
"ErrorCount": "登录错误次数锁定阈值", "ErrorCount": "登录错误次数锁定阈值",
"VerificatExpireTime": "登录过期时间(分)", "VerificatExpireTime": "登录过期时间(分)",
"ErrorLockTime.MinValue": " {0} 值太小", "ErrorLockTime.MinValue": " {0} 值太小",
"ErrorResetTime.MinValue": " {0} 值太小", "ErrorResetTime.MinValue": " {0} 值太小",
"ErrorCount.MinValue": " {0} 值太小", "ErrorCount.MinValue": " {0} 值太小",
"VerificatExpireTime.MinValue": " {0} 值太小" "VerificatExpireTime.MinValue": " {0} 值太小"
}, },
"ThingsGateway.Admin.Application.PagePolicy": { "ThingsGateway.Admin.Application.PagePolicy": {
"Shortcuts": "默认快捷方式", "Shortcuts": "默认快捷方式",
"Razor": "默认主页" "Razor": "默认主页"
}, },
"ThingsGateway.Admin.Application.PasswordPolicy": { "ThingsGateway.Admin.Application.PasswordPolicy": {
"DefaultPassword": "默认用户密码", "DefaultPassword": "默认用户密码",
"DefaultPassword.Required": " {0} 是必填项", "DefaultPassword.Required": " {0} 是必填项",
"PasswordMinLen": "密码最小长度", "PasswordMinLen": "密码最小长度",
"PasswordMinLen.MinValue": " {0} 值太小", "PasswordMinLen.MinValue": " {0} 值太小",
"PasswordContainNum": "包含数字", "PasswordContainNum": "包含数字",
"PasswordContainLower": "包含小写字母", "PasswordContainLower": "包含小写字母",
"PasswordContainUpper": "包含大写字母", "PasswordContainUpper": "包含大写字母",
"PasswordContainChar": "包含特殊字符" "PasswordContainChar": "包含特殊字符"
}, },
"ThingsGateway.Admin.Application.WebsitePolicy": { "ThingsGateway.Admin.Application.WebsitePolicy": {
"WebStatus": "是否开放", "WebStatus": "是否开放",
"CloseTip": "关闭提示", "CloseTip": "关闭提示",
"CloseTip.Required": " {0} 是必填项" "CloseTip.Required": " {0} 是必填项"
}, },
//enum //enum
"ThingsGateway.Admin.Application.ResourceCategoryEnum": { "ThingsGateway.Admin.Application.ResourceCategoryEnum": {
"Module": "模块", "Module": "模块",
"Menu": "菜单", "Menu": "菜单",
"Button": "按钮" "Button": "按钮"
}, },
"ThingsGateway.Admin.Application.TargetEnum": { "ThingsGateway.Admin.Application.TargetEnum": {
"_self": "本窗口", "_self": "本窗口",
"_blank": "新窗口", "_blank": "新窗口",
"_parent": "父级窗口", "_parent": "父级窗口",
"_top": "顶级窗口" "_top": "顶级窗口"
}, },
"ThingsGateway.Admin.Application.DictTypeEnum": { "ThingsGateway.Admin.Application.DictTypeEnum": {
"System": "系统配置", "System": "系统配置",
"Define": "业务配置" "Define": "业务配置"
}, },
"ThingsGateway.Admin.Application.LogCateGoryEnum": { "ThingsGateway.Admin.Application.LogCateGoryEnum": {
"Login": "登录", "Login": "登录",
"Logout": "注销", "Logout": "注销",
"Operate": "操作", "Operate": "操作",
"Exception": "异常" "Exception": "异常"
}, },
"ThingsGateway.Admin.Application.LogEnum": { "ThingsGateway.Admin.Application.LogEnum": {
"SUCCESS": "成功", "SUCCESS": "成功",
"FAIL": "失败" "FAIL": "失败"
}
} }
}

View File

@@ -16,9 +16,6 @@ using ThingsGateway.Extension;
using ThingsGateway.FriendlyException; using ThingsGateway.FriendlyException;
using ThingsGateway.Logging; using ThingsGateway.Logging;
using ThingsGateway.NewLife.Json.Extension; using ThingsGateway.NewLife.Json.Extension;
using ThingsGateway.Razor;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
@@ -41,33 +38,31 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
/// <param name="flush"></param> /// <param name="flush"></param>
public async Task WriteAsync(LogMessage logMsg, bool flush) 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; requestAuditData.LogDateTime = logMsg.LogDateTime;
// loggingMonitor.ReturnInformation.Value // requestAuditData.ReturnInformation.Value
//验证失败不记录日志 //验证失败不记录日志
bool save = false; bool save = false;
if (loggingMonitor.Validation == null) if (requestAuditData.Validation == null)
{ {
var operation = logMsg.Context.Get(LoggingConst.Operation).ToString();//获取操作名称 var operation = requestAuditData.Operation;//获取操作名称
var client = (ClientInfo)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息 var client = requestAuditData.Client;//获取客户端信息
var path = logMsg.Context.Get(LoggingConst.Path).ToString();//获取操作名称 var path = requestAuditData.Path;//获取操作名称
var method = logMsg.Context.Get(LoggingConst.Method).ToString();//获取方法 var method = requestAuditData.Method;//获取方法
//表示访问日志 //表示访问日志
if (path == "/api/auth/login" || path == "/api/auth/logout") 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 else
{ {
//添加到异常日志 //添加到异常日志
save = await CreateOperationLog(operation, path, loggingMonitor, client, flush).ConfigureAwait(false); save = await CreateOperationLog(operation, path, requestAuditData, client, flush).ConfigureAwait(false);
} }
} }
else else
@@ -76,7 +71,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
if (!operation.IsNullOrWhiteSpace() && method == "POST") 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> /// </summary>
/// <param name="operation">操作名称</param> /// <param name="operation">操作名称</param>
/// <param name="path">请求地址</param> /// <param name="path">请求地址</param>
/// <param name="loggingMonitor">loggingMonitor</param> /// <param name="requestAuditData">requestAuditData</param>
/// <param name="clientInfo">客户端信息</param> /// <param name="userAgent">客户端信息</param>
/// <param name="flush"></param> /// <param name="flush"></param>
/// <returns></returns> /// <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字符串 //获取参数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字符串 //获取结果json字符串
var resultJson = string.Empty; var resultJson = requestAuditData.ReturnInformation?.ToSystemTextJsonString();
if (loggingMonitor.ReturnInformation != null)//如果有返回值
{
if (loggingMonitor.ReturnInformation.Value != null)//如果返回值不为空
{
resultJson = loggingMonitor.ReturnInformation.Value.ToJsonNetString();
}
}
//操作日志表实体 //操作日志表实体
var sysLogOperate = new SysOperateLog var sysLogOperate = new SysOperateLog
@@ -119,29 +108,29 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
Name = operation, Name = operation,
Category = LogCateGoryEnum.Operate, Category = LogCateGoryEnum.Operate,
ExeStatus = true, ExeStatus = true,
OpIp = loggingMonitor.RemoteIPv4, OpIp = requestAuditData.RemoteIPv4,
OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major, OpBrowser = userAgent?.Browser,
OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major, OpOs = userAgent?.Platform,
OpTime = loggingMonitor.LogDateTime.LocalDateTime, OpTime = requestAuditData.LogDateTime.LocalDateTime,
OpAccount = opAccount, OpAccount = opAccount,
ReqMethod = loggingMonitor.HttpMethod, ReqMethod = requestAuditData.Method,
ReqUrl = path, ReqUrl = path,
ResultJson = resultJson, ResultJson = resultJson,
ClassName = loggingMonitor.DisplayName, ClassName = requestAuditData.ControllerName,
MethodName = loggingMonitor.ActionName, MethodName = requestAuditData.ActionName,
ParamJson = paramJson, ParamJson = paramJson,
VerificatId = UserManager.VerificatId, VerificatId = UserManager.VerificatId,
}; };
//如果异常不为空 //如果异常不为空
if (loggingMonitor.Exception != null) if (requestAuditData.Exception != null)
{ {
sysLogOperate.Category = LogCateGoryEnum.Exception;//操作类型为异常 sysLogOperate.Category = LogCateGoryEnum.Exception;//操作类型为异常
sysLogOperate.ExeStatus = false;//操作状态为失败 sysLogOperate.ExeStatus = false;//操作状态为失败
if (loggingMonitor.Exception.Type == typeof(AppFriendlyException).ToString()) if (requestAuditData.Exception.Type == typeof(AppFriendlyException).ToString())
sysLogOperate.ExeMessage = loggingMonitor?.Exception.Message; sysLogOperate.ExeMessage = requestAuditData?.Exception.Message;
else 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); _operateLogMessageQueue.Enqueue(sysLogOperate);
@@ -160,26 +149,25 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
/// </summary> /// </summary>
/// <param name="operation">访问类型</param> /// <param name="operation">访问类型</param>
/// <param name="path"></param> /// <param name="path"></param>
/// <param name="loggingMonitor">loggingMonitor</param> /// <param name="requestAuditData">requestAuditData</param>
/// <param name="clientInfo">客户端信息</param> /// <param name="userAgent">客户端信息</param>
/// <param name="flush"></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 long verificatId = 0;//验证Id
var opAccount = "";//用户账号 var opAccount = "";//用户账号
if (path == "/api/auth/login") if (path == "/api/auth/login")
{ {
//如果是登录,用户信息就从返回值里拿 //如果是登录,用户信息就从返回值里拿
var result = loggingMonitor.ReturnInformation?.Value?.ToJsonNetString();//返回值转json dynamic userInfo = requestAuditData.ReturnInformation;
var userInfo = result.FromJsonNetString<UnifyResult<LoginOutput>>();//格式化成user表
opAccount = userInfo.Data.Account;//赋值账号 opAccount = userInfo.Data.Account;//赋值账号
verificatId = userInfo.Data.VerificatId; verificatId = userInfo.Data.VerificatId;
} }
else else
{ {
//如果是登录出用户信息就从AuthorizationClaims里拿 //如果是登录出用户信息就从AuthorizationClaims里拿
opAccount = loggingMonitor.AuthorizationClaims.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault(); opAccount = requestAuditData.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(); verificatId = requestAuditData.AuthorizationClaims.Where(it => it.Type == ClaimConst.VerificatId).Select(it => it.Value).FirstOrDefault().ToLong();
} }
//日志表实体 //日志表实体
var sysLogVisit = new SysOperateLog var sysLogVisit = new SysOperateLog
@@ -187,19 +175,19 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
Name = operation, Name = operation,
Category = path == "/api/auth/login" ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout, Category = path == "/api/auth/login" ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout,
ExeStatus = true, ExeStatus = true,
OpIp = loggingMonitor.RemoteIPv4, OpIp = requestAuditData.RemoteIPv4,
OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major, OpBrowser = userAgent?.Browser,
OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major, OpOs = userAgent?.Platform,
OpTime = loggingMonitor.LogDateTime.LocalDateTime, OpTime = requestAuditData.LogDateTime.LocalDateTime,
VerificatId = verificatId, VerificatId = verificatId,
OpAccount = opAccount, OpAccount = opAccount,
ReqMethod = loggingMonitor.HttpMethod, ReqMethod = requestAuditData.Method,
ReqUrl = path, ReqUrl = path,
ResultJson = loggingMonitor.ReturnInformation?.Value?.ToJsonNetString(), ResultJson = requestAuditData.ReturnInformation?.ToSystemTextJsonString(),
ClassName = loggingMonitor.DisplayName, ClassName = requestAuditData.ControllerName,
MethodName = loggingMonitor.ActionName, MethodName = requestAuditData.ActionName,
ParamJson = loggingMonitor.Parameters?.ToJsonNetString(), ParamJson = requestAuditData.Parameters?.ToSystemTextJsonString(),
}; };
_operateLogMessageQueue.Enqueue(sysLogVisit); _operateLogMessageQueue.Enqueue(sysLogVisit);

View File

@@ -22,26 +22,19 @@ using System.Globalization;
using System.Reflection; using System.Reflection;
using ThingsGateway.Extension; using ThingsGateway.Extension;
using ThingsGateway.SpecificationDocument;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
internal sealed class ApiPermissionService : IApiPermissionService internal sealed class ApiPermissionService : IApiPermissionService
{ {
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider; private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider;
private readonly SwaggerGeneratorOptions _generatorOptions;
public ApiPermissionService( public ApiPermissionService(
IOptions<SwaggerGeneratorOptions> generatorOptions, IOptions<SwaggerGeneratorOptions> generatorOptions,
IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider) IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider)
{ {
_generatorOptions = generatorOptions.Value;
_apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider; _apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider;
} }
private IEnumerable<string> GetDocumentNames()
{
return _generatorOptions.SwaggerDocs.Keys;
}
/// <inheritdoc /> /// <inheritdoc />
public List<OpenApiPermissionTreeSelector> ApiPermissionTreeSelector() public List<OpenApiPermissionTreeSelector> ApiPermissionTreeSelector()
@@ -53,37 +46,37 @@ internal sealed class ApiPermissionService : IApiPermissionService
permissions = new(); permissions = new();
Dictionary<string, OpenApiPermissionTreeSelector> groupOpenApis = new(); Dictionary<string, OpenApiPermissionTreeSelector> groupOpenApis = new();
foreach (var item in GetDocumentNames())
{
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = item ?? "Default" };
groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
}
var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items; var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items;
foreach (var item1 in apiDescriptions)
{
foreach (var item in item1.Items)
{
if (item.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = controllerActionDescriptor.ControllerName ?? "Default" };
groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
}
}
}
// 获取所有需要数据权限的控制器 // 获取所有需要数据权限的控制器
var controllerTypes = var controllerTypes =
App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(RolePermissionAttribute), false)); App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(RolePermissionAttribute), false));
foreach (var groupOpenApi in groupOpenApis) //foreach (var groupOpenApi in groupOpenApis)
{ {
foreach (var apiDescriptionGroup in apiDescriptions) foreach (var apiDescriptionGroup in apiDescriptions)
{ {
var routes = apiDescriptionGroup.Items.Where(api => api.ActionDescriptor is ControllerActionDescriptor); var routes = apiDescriptionGroup.Items.Where(api => api.ActionDescriptor is ControllerActionDescriptor);
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = groupOpenApi.Value;
Dictionary<string, OpenApiPermissionTreeSelector> openApiPermissionTreeSelectorDict = new(); Dictionary<string, OpenApiPermissionTreeSelector> openApiPermissionTreeSelectorDict = new();
foreach (var route in routes) foreach (var route in routes)
{ {
if (!SpecificationDocumentBuilder.CheckApiDescriptionInCurrentGroup(groupOpenApi.Key, route))
{
continue;
}
var actionDesc = (ControllerActionDescriptor)route.ActionDescriptor; var actionDesc = (ControllerActionDescriptor)route.ActionDescriptor;
if (!actionDesc.ControllerTypeInfo.CustomAttributes.Any(a => a.AttributeType == typeof(RolePermissionAttribute))) if (!actionDesc.ControllerTypeInfo.CustomAttributes.Any(a => a.AttributeType == typeof(RolePermissionAttribute)))
continue; continue;
@@ -116,10 +109,8 @@ internal sealed class ApiPermissionService : IApiPermissionService
} }
openApiPermissionTreeSelector.Children.AddRange(openApiPermissionTreeSelectorDict.Values); if (openApiPermissionTreeSelectorDict.Values.Any(a => a.Children.Count > 0))
permissions.AddRange(openApiPermissionTreeSelectorDict.Values);
if (openApiPermissionTreeSelector.Children.Any(a => a.Children.Count > 0))
permissions.Add(openApiPermissionTreeSelector);
} }

View File

@@ -15,12 +15,15 @@ using Microsoft.AspNetCore.WebUtilities;
using System.Security.Claims; using System.Security.Claims;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
public class AppService : IAppService public class AppService : IAppService
{ {
private readonly IUserAgentService UserAgentService;
public AppService(IUserAgentService userAgentService)
{
UserAgentService = userAgentService;
}
public string GetReturnUrl(string returnUrl) public string GetReturnUrl(string returnUrl)
{ {
var url = QueryHelpers.AddQueryString(CookieAuthenticationDefaults.LoginPath, new Dictionary<string, string?> var url = QueryHelpers.AddQueryString(CookieAuthenticationDefaults.LoginPath, new Dictionary<string, string?>
@@ -41,18 +44,16 @@ public class AppService : IAppService
{ {
} }
} }
public Parser Parser = Parser.GetDefault(); public UserAgent? UserAgent
public ClientInfo? ClientInfo
{ {
get get
{ {
var str = App.HttpContext?.Request?.Headers?.UserAgent; var str = App.HttpContext?.Request?.Headers?.UserAgent;
ClientInfo? clientInfo = null;
if (!string.IsNullOrEmpty(str)) if (!string.IsNullOrEmpty(str))
{ {
clientInfo = Parser.Parse(str); return UserAgentService.Parse(str);
} }
return clientInfo; return null;
} }
} }

View File

@@ -13,19 +13,17 @@ using Microsoft.Extensions.DependencyInjection;
using System.Security.Claims; using System.Security.Claims;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
public class HybridAppService : IAppService 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"; 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"; RemoteIpAddress = "127.0.0.1";
} }
public ClientInfo? ClientInfo { get; } public UserAgent? UserAgent { get; }
private static BlazorHybridAuthenticationStateProvider _authenticationStateProvider; private static BlazorHybridAuthenticationStateProvider _authenticationStateProvider;
private static BlazorHybridAuthenticationStateProvider AuthenticationStateProvider private static BlazorHybridAuthenticationStateProvider AuthenticationStateProvider

View File

@@ -11,8 +11,6 @@
using System.Security.Claims; using System.Security.Claims;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
public interface IAppService public interface IAppService
@@ -20,7 +18,7 @@ public interface IAppService
/// <summary> /// <summary>
/// ClientInfo /// ClientInfo
/// </summary> /// </summary>
public ClientInfo? ClientInfo { get; } public UserAgent? UserAgent { get; }
/// <summary> /// <summary>
/// ClaimsPrincipal /// ClaimsPrincipal

View File

@@ -96,9 +96,9 @@ public class AuthService : IAuthService
/// </summary> /// </summary>
public async Task LoginOutAsync() public async Task LoginOutAsync()
{ {
if (UserManager.UserId == 0) if (UserManager.VerificatId == 0)
return; return;
var verificatId = UserManager.UserId; var verificatId = UserManager.VerificatId;
//获取用户信息 //获取用户信息
var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false); var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false);
if (userinfo != null) if (userinfo != null)
@@ -237,7 +237,7 @@ public class AuthService : IAuthService
var logingEvent = new LoginEvent var logingEvent = new LoginEvent
{ {
Ip = _appService.RemoteIpAddress, Ip = _appService.RemoteIpAddress,
Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(), Device = App.GetService<IAppService>().UserAgent?.Platform,
Expire = expire, Expire = expire,
SysUser = sysUser, SysUser = sysUser,
VerificatId = verificatId VerificatId = verificatId

View File

@@ -77,7 +77,7 @@ internal sealed class SysDictService : BaseService<SysDict>, ISysDictService
//更新数据 //更新数据
List<SysDict> dicts = new List<SysDict>() 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); var storageable = await db.Storageable(dicts).WhereColumns(it => new { it.DictType, it.Category, it.Name }).ToStorageAsync().ConfigureAwait(false);

View File

@@ -16,9 +16,9 @@ namespace ThingsGateway.Admin.Application;
/// 内存推送事件服务 /// 内存推送事件服务
/// </summary> /// </summary>
/// <typeparam name="TEntry"></typeparam> /// <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() public void Dispose()
{ {

View File

@@ -277,7 +277,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
if (isSuperAdmin) if (isSuperAdmin)
throw Oops.Bah(Localizer["CanotGrantAdmin"]); throw Oops.Bah(Localizer["CanotGrantAdmin"]);
var menuIds = input.GrantInfoList.Select(it => it.MenuId).ToList();//菜单ID 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 relationRoles = new List<SysRelation>();//要添加的角色资源和授权关系表
var sysRole = (await GetAllAsync().ConfigureAwait(false)).FirstOrDefault(it => it.Id == input.Id);//获取角色 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 ExtJson = new RelationPermission
{ {
ApiUrl = it.ApiRoute, ApiUrl = it.ApiRoute,
}.ToJsonNetString() }.ToSystemTextJsonString()
}); });
relationRoles.AddRange(relationRolePer);//合并列表 relationRoles.AddRange(relationRolePer);//合并列表
} }
@@ -410,7 +410,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
if (sysRole != null) if (sysRole != null)
{ {
await _relationService.SaveRelationBatchAsync(RelationCategoryEnum.RoleHasOpenApiPermission, input.Id, 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);//添加到数据库 , true).ConfigureAwait(false);//添加到数据库
await ClearTokenUtil.DeleteUserCacheByRoleIds(new List<long> { input.Id }).ConfigureAwait(false);//清除角色下用户缓存 await ClearTokenUtil.DeleteUserCacheByRoleIds(new List<long> { input.Id }).ConfigureAwait(false);//清除角色下用户缓存
} }

View File

@@ -435,7 +435,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
if (sysUser != null) if (sysUser != null)
{ {
await _relationService.SaveRelationBatchAsync(RelationCategoryEnum.UserHasOpenApiPermission, input.Id, 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);//添加到数据库 true).ConfigureAwait(false);//添加到数据库
DeleteUserFromCache(input.Id); DeleteUserFromCache(input.Id);
} }
@@ -557,7 +557,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
public async Task GrantResourceAsync(GrantResourceData input) public async Task GrantResourceAsync(GrantResourceData input)
{ {
var menuIds = input.GrantInfoList.Select(it => it.MenuId).ToList();//菜单ID 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 relationUsers = new List<SysRelation>();//要添加的用户资源和授权关系表
var sysUser = await GetUserByIdAsync(input.Id).ConfigureAwait(false);//获取用户 var sysUser = await GetUserByIdAsync(input.Id).ConfigureAwait(false);//获取用户
await CheckApiDataScopeAsync(sysUser.OrgId, sysUser.CreateUserId).ConfigureAwait(false); await CheckApiDataScopeAsync(sysUser.OrgId, sysUser.CreateUserId).ConfigureAwait(false);
@@ -613,7 +613,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
TargetId = it.ApiRoute, TargetId = it.ApiRoute,
Category = RelationCategoryEnum.UserHasPermission, Category = RelationCategoryEnum.UserHasPermission,
ExtJson = new RelationPermission { ApiUrl = it.ApiRoute } ExtJson = new RelationPermission { ApiUrl = it.ApiRoute }
.ToJsonNetString() .ToSystemTextJsonString()
}); });
relationUsers.AddRange(relationUserPer);//合并列表 relationUsers.AddRange(relationUserPer);//合并列表
} }

View File

@@ -203,7 +203,7 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
public async Task UpdateWorkbenchInfoAsync(WorkbenchInfo input) 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); true).ConfigureAwait(false);
} }

View File

@@ -10,9 +10,6 @@
using SqlSugar; using SqlSugar;
using ThingsGateway.List;
using ThingsGateway.NewLife.Json.Extension;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
/// <summary> /// <summary>
@@ -169,7 +166,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void RemoveAllClientId() public void RemoveAllClientId()
{ {
using var db = GetDB(); using var db = GetDB();
db.Updateable<VerificatInfo>().SetColumns("ClientIds", new ConcurrentList<long>().ToJsonNetString()).Where(a => a.Id >= 0).ExecuteCommand(); db.Updateable<VerificatInfo>().SetColumns(a => a.ClientIds == null).Where(a => a.Id > 0).ExecuteCommand();
VerificatInfoService.RemoveCache(); VerificatInfoService.RemoveCache();
} }

View File

@@ -80,7 +80,9 @@ public static class DbContext
{ {
db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings
{ {
SqlServerCodeFirstNvarchar = true//设置默认nvarchar SqlServerCodeFirstNvarchar = true, //设置默认nvarchar
IsNoReadXmlDescription = true
}; };
} }

View File

@@ -24,6 +24,11 @@ public sealed class SqlSugarOption : ConnectionConfig
/// </summary> /// </summary>
public bool InitSeedData { get; set; } = false; public bool InitSeedData { get; set; } = false;
/// <summary>
/// 初始化数据库
/// </summary>
public bool InitDatabase { get; set; } = false;
/// <summary> /// <summary>
/// 初始化表 /// 初始化表
/// </summary> /// </summary>

View File

@@ -36,6 +36,7 @@ public class Startup : AppStartup
services.AddSingleton<ISugarAopService, SugarAopService>(); services.AddSingleton<ISugarAopService, SugarAopService>();
services.AddSingleton<ISugarConfigAopService, SugarConfigAopService>(); services.AddSingleton<ISugarConfigAopService, SugarConfigAopService>();
services.AddSingleton<IUserAgentService, UserAgentService>();
services.AddSingleton<IAppService, AppService>(); services.AddSingleton<IAppService, AppService>();
StaticConfig.EnableAllWhereIF = true; StaticConfig.EnableAllWhereIF = true;
@@ -89,7 +90,7 @@ public class Startup : AppStartup
DbContext.DbConfigs?.ForEach(it => DbContext.DbConfigs?.ForEach(it =>
{ {
var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象 var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
if (it.InitTable == true) if (it.InitDatabase == true)
connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建 connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
}); });

View File

@@ -18,10 +18,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" /> <PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.5" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="Rougamo.Fody" Version="5.0.0" /> <PackageReference Include="Rougamo.Fody" Version="5.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.189" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
@@ -30,9 +29,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.4" /> <PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.4" /> <PackageReference Include="System.Threading.RateLimiting" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Remove="SeedData\Admin\*.json" /> <Content Remove="SeedData\Admin\*.json" />

View File

@@ -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);
}
}

View 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;
}
}
}

View File

@@ -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);
});
}
}
}

View File

@@ -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"},
};
}
}

View File

@@ -97,7 +97,7 @@ public class BlazorAppContext
AllResource = sysResources; AllResource = sysResources;
var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet(); var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList(); 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) if (moduleId == null)
{ {

View File

@@ -41,7 +41,7 @@ public partial class SessionPage
{ {
var op = new DialogOption() var op = new DialogOption()
{ {
IsScrolling = false, IsScrolling = true,
Title = Localizer[nameof(VerificatInfo)], Title = Localizer[nameof(VerificatInfo)],
ShowMaximizeButton = true, ShowMaximizeButton = true,
Class = "dialog-table", Class = "dialog-table",

View File

@@ -9,10 +9,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'"> <ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.14" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net9.0'"> <ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -39,19 +39,4 @@
</div> </div>
</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>

View File

@@ -18,8 +18,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using System.Diagnostics.CodeAnalysis;
using ThingsGateway.Admin.Application; using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor; using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension; using ThingsGateway.Extension;
@@ -31,118 +29,8 @@ namespace ThingsGateway.AdminServer;
[IgnoreRolePermission] [IgnoreRolePermission]
[Route("/")] [Route("/")]
[TabItemOption(Text = "Home", Icon = "fas fa-house")] [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] [Inject]
private BlazorAppContext AppContext { get; set; } private BlazorAppContext AppContext { get; set; }

View File

@@ -10,14 +10,17 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Unicode; using System.Text.Unicode;
@@ -25,8 +28,8 @@ using System.Text.Unicode;
using ThingsGateway.Admin.Application; using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor; using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension; using ThingsGateway.Extension;
using ThingsGateway.Logging;
using ThingsGateway.NewLife.Caching; using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
@@ -85,6 +88,7 @@ public class Startup : AppStartup
} }
; ;
services.AddMvcFilter<RequestAuditFilter>();
services.AddControllers() services.AddControllers()
.AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings)) .AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings))
//.AddXmlSerializerFormatters() //.AddXmlSerializerFormatters()
@@ -157,7 +161,8 @@ public class Startup : AppStartup
{ {
options.WriteFilter = (logMsg) => options.WriteFilter = (logMsg) =>
{ {
return true; if (logMsg.Message.IsNullOrEmpty()) return false;
else return true;
}; };
options.MessageFormat = (logMsg) => options.MessageFormat = (logMsg) =>
@@ -207,39 +212,39 @@ public class Startup : AppStartup
#region api日志 #region api日志
//Monitor日志配置 //Monitor日志配置
services.AddMonitorLogging(options => //services.AddMonitorLogging(options =>
{ //{
options.JsonIndented = true;// 是否美化 JSON // options.JsonIndented = true;// 是否美化 JSON
options.GlobalEnabled = false;//全局启用 // options.GlobalEnabled = false;//全局启用
options.ConfigureLogger((logger, logContext, context) => // options.ConfigureLogger((logger, logContext, context) =>
{ // {
var httpContext = context.HttpContext;//获取httpContext // var httpContext = context.HttpContext;//获取httpContext
//获取客户端信息 // //获取客户端信息
var client = App.GetService<IAppService>().ClientInfo; // var client = App.GetService<IAppService>().UserAgent;
// 获取控制器/操作描述器 // // 获取控制器/操作描述器
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; // var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
//操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性 // //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}"; // var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name]; // var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
//获取特性 // //获取特性
option = desc.Value;//则将操作名称赋值为控制器上写的title // option = desc.Value;//则将操作名称赋值为控制器上写的title
logContext.Set(LoggingConst.CateGory, option);//传操作名称 // logContext.Set(LoggingConst.CateGory, option);//传操作名称
logContext.Set(LoggingConst.Operation, option);//传操作名称 // logContext.Set(LoggingConst.Operation, option);//传操作名称
logContext.Set(LoggingConst.Client, client);//客户端信息 // logContext.Set(LoggingConst.Client, client);//客户端信息
logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址 // logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法 // logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
}); // });
}); //});
//日志写入数据库配置 //日志写入数据库配置
services.AddDatabaseLogging<DatabaseLoggingWriter>(options => services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
{ {
options.WriteFilter = (logMsg) => options.WriteFilter = (logMsg) =>
{ {
return logMsg.LogName == "System.Logging.LoggingMonitor";//只写入LoggingMonitor日志 return logMsg.LogName == "System.Logging.RequestAudit";
}; };
}); });
@@ -291,6 +296,21 @@ public class Startup : AppStartup
services.AddAuthorizationCore(); services.AddAuthorizationCore();
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>(); services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
#if NET9_0_OR_GREATER
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#else
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
} }

View File

@@ -45,7 +45,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<!--安装服务守护--> <!--安装服务守护-->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
@@ -54,8 +54,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -72,6 +72,9 @@
<None Update="pm2-linux.json"> <None Update="pm2-linux.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="ThingsGateway.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="thingsgateway.service"> <None Update="thingsgateway.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>

Binary file not shown.

View File

@@ -0,0 +1,81 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using ThingsGateway;
namespace Microsoft.Extensions.Hosting;
/// <summary>
/// HostApplication 拓展
/// </summary>
public static class AppHostApplicationBuilderExtensions
{
/// <summary>
/// Host 应用注入
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="autoRegisterBackgroundService"></param>
/// <returns>HostApplicationBuilder</returns>
public static HostApplicationBuilder Inject(this HostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
{
// 初始化配置
InternalApp.ConfigureApplication(hostApplicationBuilder, autoRegisterBackgroundService);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public static HostApplicationBuilder AddComponent<TComponent>(this HostApplicationBuilder hostApplicationBuilder, object options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <typeparam name="TComponent">派生自 <see cref="IServiceComponent"/></typeparam>
/// <typeparam name="TComponentOptions">组件参数</typeparam>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent<TComponent, TComponentOptions>(this HostApplicationBuilder hostApplicationBuilder, TComponentOptions options = default)
where TComponent : class, IServiceComponent, new()
{
hostApplicationBuilder.Services.AddComponent<TComponent, TComponentOptions>(options);
return hostApplicationBuilder;
}
/// <summary>
/// 注册依赖组件
/// </summary>
/// <param name="hostApplicationBuilder">Host 应用构建器</param>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns><see cref="HostApplicationBuilder"/></returns>
public static HostApplicationBuilder AddComponent(this HostApplicationBuilder hostApplicationBuilder, Type componentType, object options = default)
{
hostApplicationBuilder.Services.AddComponent(componentType, options);
return hostApplicationBuilder;
}
}

View File

@@ -467,18 +467,20 @@ public static class ObjectExtensions
return obj; return obj;
} }
/// <summary> /// <summary>
/// 查找方法指定特性,如果没找到则继续查找声明类 /// 查找方法指定特性,如果没找到则继续查找声明类
/// </summary> /// </summary>
/// <typeparam name="TAttribute"></typeparam> /// <typeparam name="TAttribute"></typeparam>
/// <param name="method"></param> /// <param name="method"></param>
/// <param name="inherit"></param> /// <param name="inherit"></param>
/// <param name="searchFromReflectedType">searchFromRuntimeType</param>
/// <returns></returns> /// <returns></returns>
internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit) internal static TAttribute GetFoundAttribute<TAttribute>(this MethodInfo method, bool inherit, bool searchFromReflectedType = false)
where TAttribute : Attribute where TAttribute : Attribute
{ {
// 获取方法所在类型 // 获取方法所在类型
var declaringType = method.DeclaringType; var declaringType = !searchFromReflectedType ? method.DeclaringType : method.ReflectedType; // 解决嵌套继承问题
var attributeType = typeof(TAttribute); var attributeType = typeof(TAttribute);
@@ -493,7 +495,6 @@ public static class ObjectExtensions
return foundAttribute; return foundAttribute;
} }
/// <summary> /// <summary>
/// 格式化字符串 /// 格式化字符串
/// </summary> /// </summary>

View File

@@ -132,6 +132,34 @@ internal static class InternalApp
}); });
} }
/// <summary>
/// 配置 Furion 框架(非 Web
/// </summary>
/// <param name="hostApplicationBuilder"></param>
/// <param name="autoRegisterBackgroundService"></param>
internal static void ConfigureApplication(IHostApplicationBuilder hostApplicationBuilder, bool autoRegisterBackgroundService = true)
{
// 存储环境对象
HostEnvironment = hostApplicationBuilder.Environment;
// 加载配置
AddJsonFiles(hostApplicationBuilder.Configuration, hostApplicationBuilder.Environment);
// 存储配置对象
Configuration = hostApplicationBuilder.Configuration;
// 存储服务提供器
InternalServices = hostApplicationBuilder.Services;
// 存储根服务
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();
// 初始化应用服务
hostApplicationBuilder.Services.AddApp();
// 自动注册 BackgroundService
if (autoRegisterBackgroundService) hostApplicationBuilder.Services.AddAppHostedService();
}
/// <summary> /// <summary>
/// 自动装载主机配置 /// 自动装载主机配置
/// </summary> /// </summary>

View File

@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -29,34 +29,36 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static string Encrypt(string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
var bKey = Encoding.UTF8.GetBytes(skey); var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
aesAlg.Mode = mode; aesAlg.Mode = mode;
aesAlg.Padding = padding; aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB)
{ {
aesAlg.IV = iv ?? aesAlg.IV; // 如果未提供 IV则使用随机生成的 IV aesAlg.IV = iv ?? aesAlg.IV;
if (iv != null && iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
} }
using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); using var encryptor = aesAlg.CreateEncryptor();
using var msEncrypt = new MemoryStream(); using var msEncrypt = new MemoryStream();
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt)) using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
{ {
swEncrypt.Write(text); swEncrypt.Write(text);
} }
var encryptedContent = msEncrypt.ToArray(); var encryptedContent = msEncrypt.ToArray();
// 如果是 CBC 模式,将 IV 和密文拼接在一起 // 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB && iv == null)
{ {
var result = new byte[aesAlg.IV.Length + encryptedContent.Length]; var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length); Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
@@ -76,35 +78,43 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static string Decrypt(string hash, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
var fullCipher = Convert.FromBase64String(hash); var fullCipher = Convert.FromBase64String(hash);
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var bKey = Encoding.UTF8.GetBytes(skey); if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
aesAlg.Mode = mode; aesAlg.Mode = mode;
aesAlg.Padding = padding; aesAlg.Padding = padding;
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB)
{ {
var bVector = new byte[16]; if (iv == null)
var cipher = new byte[fullCipher.Length - bVector.Length]; {
if (fullCipher.Length < aesAlg.BlockSize / 8) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
Unsafe.CopyBlock(ref bVector[0], ref fullCipher[0], (uint)bVector.Length); iv = new byte[aesAlg.BlockSize / 8];
Unsafe.CopyBlock(ref cipher[0], ref fullCipher[bVector.Length], (uint)(fullCipher.Length - bVector.Length)); var cipher = new byte[fullCipher.Length - iv.Length];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
aesAlg.IV = iv ?? bVector; Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
fullCipher = cipher; aesAlg.IV = iv;
fullCipher = cipher;
}
else
{
if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
aesAlg.IV = iv;
}
} }
using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); using var decryptor = aesAlg.CreateDecryptor();
using var msDecrypt = new MemoryStream(fullCipher); using var msDecrypt = new MemoryStream(fullCipher);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
using var srDecrypt = new StreamReader(csDecrypt); using var srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8);
return srDecrypt.ReadToEnd(); return srDecrypt.ReadToEnd();
} }
@@ -117,19 +127,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>加密后的字节数组</returns> /// <returns>加密后的字节数组</returns>
public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
// 确保密钥长度为 128 位、192 位或 256 位 // 验证密钥长度
var bKey = new byte[32]; // 256 位密钥 var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var keyBytes = Encoding.UTF8.GetBytes(skey); if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
iv ??= GenerateRandomIV(); // 生成随机 IV
}
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
@@ -138,34 +142,29 @@ public class AESEncryption
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB)
{ {
aesAlg.IV = iv; aesAlg.IV = iv ?? GenerateRandomIV();
if (aesAlg.IV.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
} }
using var memoryStream = new MemoryStream(); using var memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Write); using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write))
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
// 如果是 CBC 模式,将 IV 和密文拼接在一起
if (mode != CipherMode.ECB)
{ {
var result = new byte[aesAlg.IV.Length + memoryStream.ToArray().Length]; cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
}
var encryptedContent = memoryStream.ToArray();
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length); Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
Buffer.BlockCopy(memoryStream.ToArray(), 0, result, aesAlg.IV.Length, memoryStream.ToArray().Length); Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length);
return result; return result;
} }
// 如果是 ECB 模式,直接返回密文 return encryptedContent;
return memoryStream.ToArray();
}
// 生成随机 IV
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
} }
/// <summary> /// <summary>
@@ -176,25 +175,13 @@ public class AESEncryption
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns></returns> /// <returns></returns>
public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
// 确保密钥长度为 128 位、192 位或 256 位 // 验证密钥长度
var bKey = new byte[32]; // 256 位密钥 var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
var keyBytes = Encoding.UTF8.GetBytes(skey); if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
Array.Copy(keyBytes, bKey, Math.Min(keyBytes.Length, bKey.Length));
// 如果是 ECB 模式,不需要 IV
if (mode != CipherMode.ECB)
{
if (iv == null)
{
// 从密文中提取 IV
iv = new byte[16];
Array.Copy(bytes, iv, iv.Length);
bytes = bytes.Skip(iv.Length).ToArray();
}
}
using var aesAlg = Aes.Create(); using var aesAlg = Aes.Create();
aesAlg.Key = bKey; aesAlg.Key = bKey;
@@ -203,21 +190,36 @@ public class AESEncryption
if (mode != CipherMode.ECB) if (mode != CipherMode.ECB)
{ {
if (iv == null)
{
// 提取IV
if (bytes.Length < 16) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
iv = bytes.Take(16).ToArray();
bytes = bytes.Skip(16).ToArray();
}
else
{
if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
aesAlg.IV = iv; aesAlg.IV = iv;
} }
using var memoryStream = new MemoryStream(bytes); using var memoryStream = new MemoryStream(bytes);
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV), CryptoStreamMode.Read); using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read);
using var originalStream = new MemoryStream(); using var originalStream = new MemoryStream();
var buffer = new byte[1024]; cryptoStream.CopyTo(originalStream);
var readBytes = 0;
while ((readBytes = cryptoStream.Read(buffer, 0, buffer.Length)) > 0)
{
originalStream.Write(buffer, 0, readBytes);
}
return originalStream.ToArray(); return originalStream.ToArray();
} }
/// <summary>
/// 生成随机 IV
/// </summary>
/// <returns></returns>
private static byte[] GenerateRandomIV()
{
using var aes = Aes.Create();
aes.GenerateIV();
return aes.IV;
}
} }

View File

@@ -0,0 +1,92 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.IO.Compression;
using System.Text;
namespace ThingsGateway.DataEncryption;
/// <summary>
/// GZip 压缩解压
/// </summary>
[SuppressSniffer]
public static class GzipEncryption
{
/// <summary>
/// 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] Compress(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream();
using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
{
zip.Write(buffer, 0, buffer.Length);
}
return ms.ToArray();
}
/// <summary>
/// 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string Decompress(byte[] bytes)
{
using var ms = new MemoryStream(bytes);
using var zip = new GZipStream(ms, CompressionMode.Decompress);
using var outStream = new MemoryStream();
zip.CopyTo(outStream);
return Encoding.UTF8.GetString(outStream.ToArray());
}
/// <summary>
/// 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string CompressToBase64(string text)
{
var buffer = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream();
using (var zip = new GZipStream(ms, CompressionMode.Compress, true))
{
zip.Write(buffer, 0, buffer.Length);
}
return Convert.ToBase64String(ms.ToArray());
}
/// <summary>
/// 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
public static string DecompressFromBase64(string base64String)
{
var compressedData = Convert.FromBase64String(base64String);
using var ms = new MemoryStream(compressedData);
using var zip = new GZipStream(ms, CompressionMode.Decompress);
using var outStream = new MemoryStream();
zip.CopyTo(outStream);
return Encoding.UTF8.GetString(outStream.ToArray());
}
}

View File

@@ -77,10 +77,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static string ToAESEncrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
return AESEncryption.Encrypt(text, skey, iv, mode, padding); return AESEncryption.Encrypt(text, skey, iv, mode, padding, isBase64);
} }
/// <summary> /// <summary>
@@ -91,10 +92,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static string ToAESDecrypt(this string text, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
return AESEncryption.Decrypt(text, skey, iv, mode, padding); return AESEncryption.Decrypt(text, skey, iv, mode, padding, isBase64);
} }
/// <summary> /// <summary>
@@ -105,10 +107,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static byte[] ToAESEncrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
return AESEncryption.Encrypt(bytes, skey, iv, mode, padding); return AESEncryption.Encrypt(bytes, skey, iv, mode, padding, isBase64);
} }
/// <summary> /// <summary>
@@ -119,10 +122,11 @@ public static class StringEncryptionExtensions
/// <param name="iv">偏移量</param> /// <param name="iv">偏移量</param>
/// <param name="mode">模式</param> /// <param name="mode">模式</param>
/// <param name="padding">填充</param> /// <param name="padding">填充</param>
/// <param name="isBase64"></param>
/// <returns>string</returns> /// <returns>string</returns>
public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) public static byte[] ToAESDecrypt(this byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{ {
return AESEncryption.Decrypt(bytes, skey, iv, mode, padding); return AESEncryption.Decrypt(bytes, skey, iv, mode, padding, isBase64);
} }
/// <summary> /// <summary>
@@ -243,4 +247,44 @@ public static class StringEncryptionExtensions
{ {
return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength); return PBKDF2Encryption.Compare(text, hash, saltSize, iterationCount, derivedKeyLength);
} }
/// <summary>
/// Gzip 压缩字符串并返回字节数组
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] ToGzipCompress(this string text)
{
return GzipEncryption.Compress(text);
}
/// <summary>
/// Gzip 从字节数组解压
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string ToGzipDecompress(this byte[] bytes)
{
return GzipEncryption.Decompress(bytes);
}
/// <summary>
/// Gzip 压缩字符串并返回 Base64 字符串
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ToGzipCompressToBase64(this string text)
{
return GzipEncryption.CompressToBase64(text);
}
/// <summary>
/// Gzip 从 Base64 字符串解压
/// </summary>
/// <param name="base64String"></param>
/// <returns></returns>
public static string ToGzipDecompressFromBase64(this string base64String)
{
return GzipEncryption.DecompressFromBase64(base64String);
}
} }

View File

@@ -565,10 +565,10 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase(); if (isLowerCamelCase) parameterModel.ParameterName = parameterModel.ParameterName.ToLowerCamelCase();
// 判断是否贴有任何 [FromXXX] 特性了 // 判断是否贴有任何 [FromXXX] 特性了
var hasFormAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType())); var hasFromAttribute = parameterAttributes.Any(u => typeof(IBindingSourceMetadata).IsAssignableFrom(u.GetType()));
// 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性 // 判断方法贴有 [QueryParameters] 特性且当前参数没有任何 [FromXXX] 特性,则添加 [FromQuery] 特性
if (isQueryParametersAction && !hasFormAttribute) if (isQueryParametersAction && !hasFromAttribute)
{ {
parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() }); parameterModel.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
continue; continue;
@@ -577,7 +577,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过 // 如果没有贴 [FromRoute] 特性且不是基元类型,则跳过
// 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过 // 如果没有贴 [FromRoute] 特性且有任何绑定特性,则跳过
if (!parameterAttributes.Any(u => u is FromRouteAttribute) if (!parameterAttributes.Any(u => u is FromRouteAttribute)
&& (!parameterType.IsRichPrimitive() || hasFormAttribute)) continue; && (!parameterType.IsRichPrimitive() || hasFromAttribute)) continue;
// 处理基元数组数组类型,还有全局配置参数问题 // 处理基元数组数组类型,还有全局配置参数问题
if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray) if (_dynamicApiControllerSettings?.UrlParameterization == true || parameterType.IsArray)
@@ -588,7 +588,7 @@ internal sealed class DynamicApiControllerApplicationModelConvention : IApplicat
// 处理 [ApiController] 特性情况 // 处理 [ApiController] 特性情况
// https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference // https://docs.microsoft.com/en-US/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference
if (!hasFormAttribute && hasApiControllerAttribute) continue; if (!hasFromAttribute && hasApiControllerAttribute) continue;
// 处理默认基元参数绑定方式,若是 query[FromQuery])则跳过 // 处理默认基元参数绑定方式,若是 query[FromQuery])则跳过
if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query") if (_dynamicApiControllerSettings?.DefaultBindingInfo?.ToLower() == "query")

View File

@@ -84,8 +84,6 @@ internal sealed class DynamicApiRuntimeChangeProvider : IDynamicApiRuntimeChange
if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart); if (applicationPart != null) _applicationPartManager.ApplicationParts.Remove(applicationPart);
} }
GC.Collect();
GC.WaitForPendingFinalizers();
} }
} }

View File

@@ -72,7 +72,7 @@ public sealed class EventBusOptionsBuilder
/// <summary> /// <summary>
/// 是否启用执行完成触发 GC 回收 /// 是否启用执行完成触发 GC 回收
/// </summary> /// </summary>
public bool GCCollect { get; set; } = true; public bool GCCollect { get; set; } = false;
/// <summary> /// <summary>
/// 是否启用日志记录 /// 是否启用日志记录

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Reflection; using System.Reflection;
using System.Text.Json;
namespace ThingsGateway.EventBus; namespace ThingsGateway.EventBus;
@@ -57,4 +58,31 @@ public abstract class EventHandlerContext
/// </summary> /// </summary>
/// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks> /// <remarks><remarks>如果是动态订阅,可能为 null</remarks></remarks>
public EventSubscribeAttribute Attribute { get; } public EventSubscribeAttribute Attribute { get; }
private static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerOptions.Default)
{
PropertyNameCaseInsensitive = true
};
/// <summary>
/// 获取负载数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T GetPayload<T>()
{
var rawPayload = Source.Payload;
if (rawPayload is null)
{
return default;
}
else if (rawPayload is JsonElement jsonElement)
{
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText(), JsonSerializerOptions);
}
else
{
return (T)rawPayload;
}
}
} }

View File

@@ -38,4 +38,18 @@ public sealed class EventHandlerExecutingContext : EventHandlerContext
/// 执行前时间 /// 执行前时间
/// </summary> /// </summary>
public DateTime ExecutingTime { get; internal set; } public DateTime ExecutingTime { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
internal object Result { get; private set; }
/// <summary>
/// 设置执行结果
/// </summary>
/// <param name="result"></param>
public void SetResult(object result)
{
Result = result;
}
} }

View File

@@ -42,4 +42,10 @@ public sealed class EventHandlerEventArgs : EventArgs
/// 异常信息 /// 异常信息
/// </summary> /// </summary>
public Exception Exception { get; internal set; } public Exception Exception { get; internal set; }
/// <summary>
/// 执行结果
/// </summary>
public object Result { get; internal set; }
} }

View File

@@ -304,7 +304,10 @@ internal sealed class EventBusHostedService : BackgroundService
} }
// 触发事件处理程序事件 // 触发事件处理程序事件
_eventPublisher.InvokeEvents(new(eventSource, true)); _eventPublisher.InvokeEvents(new(eventSource, true)
{
Result = eventHandlerExecutingContext.Result
});
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -198,8 +198,9 @@ public class JWTEncryption
/// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param> /// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
/// <param name="tokenPrefix"></param> /// <param name="tokenPrefix"></param>
/// <param name="clockSkew"></param> /// <param name="clockSkew"></param>
/// <param name="onRefreshing">当刷新时触发</param>
/// <returns></returns> /// <returns></returns>
public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5) public static async Task<bool> AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5, Action<string, string> onRefreshing = null)
{ {
// 如果验证有效,则跳过刷新 // 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated) if (context.User.Identity.IsAuthenticated)
@@ -245,7 +246,11 @@ public class JWTEncryption
// 返回新的 Token // 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken; httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token // 返回新的 刷新Token
httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); var refreshAccessToken = GenerateRefreshToken(accessToken, refreshTokenExpiredTime); ;
httpContext.Response.Headers[xAccessTokenKey] = refreshAccessToken;
// 调用刷新后回调函数
onRefreshing?.Invoke(accessToken, refreshAccessToken);
// 处理 axios 问题 // 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs); httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);

View File

@@ -90,7 +90,8 @@ public sealed class ConsoleFormatterExtend : ConsoleFormatter, IDisposable
, true , true
, _disableColors , _disableColors
, _formatterOptions.WithTraceId , _formatterOptions.WithTraceId
, _formatterOptions.WithStackFrame); , _formatterOptions.WithStackFrame
, _formatterOptions.FormatProvider);
} }
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件 // 判断是否自定义了日志筛选器,如果是则检查是否符合条件

View File

@@ -12,6 +12,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -69,4 +71,10 @@ public sealed class ConsoleFormatterExtendOptions : ConsoleFormatterOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -20,7 +20,7 @@ namespace ThingsGateway.Logging;
/// </summary> /// </summary>
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks> /// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
[SuppressSniffer] [SuppressSniffer]
public sealed class DatabaseLogger : ILogger public sealed class DatabaseLogger : ILogger, IDisposable
{ {
/// <summary> /// <summary>
/// 记录器类别名称 /// 记录器类别名称
@@ -60,6 +60,11 @@ public sealed class DatabaseLogger : ILogger
return _databaseLoggerProvider.ScopeProvider?.Push(state); return _databaseLoggerProvider.ScopeProvider?.Push(state);
} }
public void Dispose()
{
_databaseLoggerProvider.RemoveCache(_logName);
}
/// <summary> /// <summary>
/// 检查是否已启用给定日志级别 /// 检查是否已启用给定日志级别
/// </summary> /// </summary>
@@ -118,7 +123,7 @@ public sealed class DatabaseLogger : ILogger
// 设置日志消息模板 // 设置日志消息模板
logMsg.Message = _options.MessageFormat != null logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg) ? _options.MessageFormat(logMsg)
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame); : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查 // 空检查
if (logMsg.Message is null) if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -80,4 +82,10 @@ public sealed class DatabaseLoggerOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -14,6 +14,8 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using ThingsGateway.Extension.Generic;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -54,6 +56,8 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
/// <remarks>实现不间断写入</remarks> /// <remarks>实现不间断写入</remarks>
private Task _processQueueTask; private Task _processQueueTask;
/// <summary> /// <summary>
/// 构造函数 /// 构造函数
/// </summary> /// </summary>
@@ -82,7 +86,10 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
{ {
return _databaseLoggers.GetOrAdd(categoryName, name => new DatabaseLogger(name, this)); return _databaseLoggers.GetOrAdd(categoryName, name => new DatabaseLogger(name, this));
} }
public void RemoveCache(string categoryName)
{
_databaseLoggers.Remove(categoryName);
}
/// <summary> /// <summary>
/// 设置作用域提供器 /// 设置作用域提供器
/// </summary> /// </summary>

View File

@@ -18,8 +18,17 @@ namespace ThingsGateway.Logging;
/// </summary> /// </summary>
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks> /// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
[SuppressSniffer] [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>
/// 开始逻辑操作范围 /// 开始逻辑操作范围
/// </summary> /// </summary>
@@ -31,6 +40,11 @@ public sealed class EmptyLogger : ILogger
return default; return default;
} }
public void Dispose()
{
_emptyLoggerProvider.RemoveCache(_logName);
}
/// <summary> /// <summary>
/// 检查是否已启用给定日志级别 /// 检查是否已启用给定日志级别
/// </summary> /// </summary>

View File

@@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using ThingsGateway.Extension.Generic;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -34,9 +36,12 @@ public sealed class EmptyLoggerProvider : ILoggerProvider
/// <returns><see cref="ILogger"/></returns> /// <returns><see cref="ILogger"/></returns>
public ILogger CreateLogger(string categoryName) 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>
/// 释放非托管资源 /// 释放非托管资源
/// </summary> /// </summary>

View File

@@ -18,7 +18,7 @@ namespace ThingsGateway.Logging;
/// </summary> /// </summary>
/// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks> /// <remarks>https://docs.microsoft.com/zh-cn/dotnet/core/extensions/custom-logging-provider</remarks>
[SuppressSniffer] [SuppressSniffer]
public sealed class FileLogger : ILogger public sealed class FileLogger : ILogger, IDisposable
{ {
/// <summary> /// <summary>
/// 记录器类别名称 /// 记录器类别名称
@@ -58,6 +58,11 @@ public sealed class FileLogger : ILogger
return _fileLoggerProvider.ScopeProvider?.Push(state); return _fileLoggerProvider.ScopeProvider?.Push(state);
} }
public void Dispose()
{
_fileLoggerProvider.RemoveCache(_logName);
}
/// <summary> /// <summary>
/// 检查是否已启用给定日志级别 /// 检查是否已启用给定日志级别
/// </summary> /// </summary>
@@ -116,7 +121,7 @@ public sealed class FileLogger : ILogger
// 设置日志消息模板 // 设置日志消息模板
logMsg.Message = _options.MessageFormat != null logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg) ? _options.MessageFormat(logMsg)
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame); : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查 // 空检查
if (logMsg.Message is null) if (logMsg.Message is null)

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -104,4 +106,10 @@ public sealed class FileLoggerOptions
/// 日志消息内容转换(如脱敏处理) /// 日志消息内容转换(如脱敏处理)
/// </summary> /// </summary>
public Func<string, string> MessageProcess { get; set; } public Func<string, string> MessageProcess { get; set; }
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using ThingsGateway.Extension.Generic;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -116,6 +118,10 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
{ {
return _fileLoggers.GetOrAdd(categoryName, name => new FileLogger(name, this)); return _fileLoggers.GetOrAdd(categoryName, name => new FileLogger(name, this));
} }
public void RemoveCache(string categoryName)
{
_fileLoggers.Remove(categoryName);
}
/// <summary> /// <summary>
/// 设置作用域提供器 /// 设置作用域提供器

View File

@@ -17,7 +17,6 @@ namespace ThingsGateway.Logging;
[SuppressSniffer] [SuppressSniffer]
public sealed class LogContext : IDisposable public sealed class LogContext : IDisposable
{ {
/// <summary> /// <summary>
/// 日志上下文数据 /// 日志上下文数据
/// </summary> /// </summary>

View File

@@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
namespace ThingsGateway.Logging; namespace ThingsGateway.Logging;
/// <summary> /// <summary>
@@ -120,6 +122,6 @@ public struct LogMessage
/// <returns><see cref="string"/></returns> /// <returns><see cref="string"/></returns>
public override readonly string ToString() public override readonly string ToString()
{ {
return Penetrates.OutputStandardMessage(this); return Penetrates.OutputStandardMessage(this, provider: CultureInfo.InvariantCulture);
} }
} }

View File

@@ -192,7 +192,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
/// <param name="claimsPrincipal"></param> /// <param name="claimsPrincipal"></param>
/// <param name="authorization"></param> /// <param name="authorization"></param>
/// <returns></returns> /// <returns></returns>
private static List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization) private List<string> GenerateAuthorizationTemplate(Utf8JsonWriter writer, ClaimsPrincipal claimsPrincipal, StringValues authorization)
{ {
var templates = new List<string>(); var templates = new List<string>();
@@ -219,7 +219,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var succeed = long.TryParse(value, out var seconds); var succeed = long.TryParse(value, out var seconds);
if (succeed) if (succeed)
{ {
value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime():yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd} L)"; value = $"{value} ({DateTimeOffset.FromUnixTimeSeconds(seconds).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss:ffff(zzz) dddd", Settings.FormatProvider)} L)";
} }
} }

View File

@@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
@@ -143,4 +144,11 @@ public sealed class LoggingMonitorSettings
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
SkipValidation = true SkipValidation = true
}; };
/// <summary>
/// 格式化提供器
/// </summary>
/// <remarks></remarks>
public IFormatProvider? FormatProvider { get; set; } = CultureInfo.InvariantCulture;
} }

View File

@@ -107,13 +107,15 @@ internal static class Penetrates
/// <param name="isConsole"></param> /// <param name="isConsole"></param>
/// <param name="withTraceId"></param> /// <param name="withTraceId"></param>
/// <param name="withStackFrame"></param> /// <param name="withStackFrame"></param>
/// <param name="provider"></param>
/// <returns></returns> /// <returns></returns>
internal static string OutputStandardMessage(LogMessage logMsg internal static string OutputStandardMessage(LogMessage logMsg
, string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd" , string dateFormat = "yyyy-MM-dd HH:mm:ss.fffffff zzz dddd"
, bool isConsole = false , bool isConsole = false
, bool disableColors = true , bool disableColors = true
, bool withTraceId = false , bool withTraceId = false
, bool withStackFrame = false) , bool withStackFrame = false
, IFormatProvider? provider = null)
{ {
// 空检查 // 空检查
if (logMsg.Message is null) return null; if (logMsg.Message is null) return null;
@@ -127,7 +129,7 @@ internal static class Penetrates
_ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors); _ = AppendWithColor(formatString, GetLogLevelString(logMsg.LogLevel), logLevelColors);
formatString.Append(": "); formatString.Append(": ");
formatString.Append(logMsg.LogDateTime.ToString(dateFormat)); formatString.Append(logMsg.LogDateTime.ToString(dateFormat, provider));
formatString.Append(' '); formatString.Append(' ');
formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L"); formatString.Append(logMsg.UseUtcTimestamp ? "U" : "L");
formatString.Append(' '); formatString.Append(' ');

View File

@@ -78,9 +78,9 @@ public partial interface ISchedulerFactory
/// <returns><see cref="IJob"/></returns> /// <returns><see cref="IJob"/></returns>
IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context); IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context);
/// <summary> ///// <summary>
/// GC 垃圾回收器回收处理 ///// GC 垃圾回收器回收处理
/// </summary> ///// </summary>
/// <remarks>避免频繁 GC 回收</remarks> ///// <remarks>避免频繁 GC 回收</remarks>
void GCCollect(); //void GCCollect();
} }

View File

@@ -183,9 +183,10 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
// 标记当前方法初始化完成 // 标记当前方法初始化完成
PreloadCompleted = true; PreloadCompleted = true;
// 释放引用内存并立即回收GC // 释放引用内存
_schedulerBuilders.Clear(); _schedulerBuilders.Clear();
GCCollect();
//GCCollect();
// 输出作业调度器初始化日志 // 输出作业调度器初始化日志
if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count); if (!preloadSucceed) _logger.LogWarning("Schedule hosted service preload completed, and a total of <{Count}> schedulers are appended.", _schedulers.Count);
@@ -393,22 +394,22 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
return jobHandler; return jobHandler;
} }
/// <summary> ///// <summary>
/// GC 垃圾回收器回收处理 ///// GC 垃圾回收器回收处理
/// </summary> ///// </summary>
/// <remarks>避免频繁 GC 回收</remarks> ///// <remarks>避免频繁 GC 回收</remarks>
public void GCCollect() //public void GCCollect()
{ //{
var nowTime = DateTime.UtcNow; // var nowTime = DateTime.UtcNow;
if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS)) // if ((LastGCCollectTime == null || (nowTime - LastGCCollectTime.Value).TotalMilliseconds > GC_COLLECT_INTERVAL_MILLISECONDS))
{ // {
LastGCCollectTime = nowTime; // LastGCCollectTime = nowTime;
// 通知 GC 垃圾回收器立即回收 // // 通知 GC 垃圾回收器立即回收
GC.Collect(); // GC.Collect();
GC.WaitForPendingFinalizers(); // GC.WaitForPendingFinalizers();
} // }
} //}
/// <summary> /// <summary>
/// 释放非托管资源 /// 释放非托管资源
@@ -535,7 +536,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
//_logger.LogWarning("Schedule hosted service cancels hibernation."); //_logger.LogWarning("Schedule hosted service cancels hibernation.");
// 通知 GC 垃圾回收器立即回收 // 通知 GC 垃圾回收器立即回收
GCCollect(); //GCCollect();
}); });
} }

View File

@@ -389,7 +389,7 @@ internal sealed class ScheduleHostedService : BackgroundService
_jobCancellationToken.Cancel(jobId, triggerId, false); _jobCancellationToken.Cancel(jobId, triggerId, false);
// 通知 GC 垃圾回收器回收 // 通知 GC 垃圾回收器回收
_schedulerFactory.GCCollect(); //_schedulerFactory.GCCollect();
} }
}, stoppingToken); }, stoppingToken);
}); });

View File

@@ -113,10 +113,8 @@ public static class SpecificationDocumentBuilder
} }
// 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口 // 处理贴有 [ApiExplorerSettings(IgnoreApi = true)] 或者 [ApiDescriptionSettings(false)] 特性的接口
var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true); var apiExplorerSettings = method.GetFoundAttribute<ApiExplorerSettingsAttribute>(true, true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true, true);
var apiDescriptionSettings = method.GetFoundAttribute<ApiDescriptionSettingsAttribute>(true);
if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false; if (apiExplorerSettings?.IgnoreApi == true || apiDescriptionSettings?.IgnoreApi == true) return false;
if (currentGroup == AllGroupsKey) if (currentGroup == AllGroupsKey)

View File

@@ -39,22 +39,22 @@
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" /> <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>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.16" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" /> <PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' "> <ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.5" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.4" /> <PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.5" />
<PackageReference Include="System.Text.Json" Version="9.0.4" /> <PackageReference Include="System.Text.Json" Version="9.0.5" />
</ItemGroup> </ItemGroup>

View File

@@ -433,10 +433,15 @@ public partial class Crontab
{ {
newValue = newValue.AddSeconds(-newValue.Second); newValue = newValue.AddSeconds(-newValue.Second);
} }
// 初始化是否存在随机 R 标识符
var randomSecond = false;
var randomMinute = false;
var randomHour = false;
// 获取分钟、小时所有字符解析器 // 获取分钟、小时所有字符解析器
var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList(); var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomMinute = minuteParsers.OfType<RandomParser>().Any();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList(); var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomHour = hourParsers.OfType<RandomParser>().Any();
// 获取秒、分钟、小时解析器中最小起始值 // 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取下一个发生值的输入参数 // 该值主要用来获取下一个发生值的输入参数
@@ -456,7 +461,7 @@ public partial class Crontab
{ {
// 获取秒所有字符解析器 // 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList(); var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomSecond = secondParsers.OfType<RandomParser>().Any();
// 获取秒解析器最小起始值 // 获取秒解析器最小起始值
firstSecondValue = secondParsers.Select(x => x.First()).Min(); firstSecondValue = secondParsers.Select(x => x.First()).Min();
@@ -519,8 +524,8 @@ public partial class Crontab
// 设置起始时间为下一个小时时间 // 设置起始时间为下一个小时时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours, newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
overflow ? firstMinuteValue : newMinutes, overflow && !randomMinute ? firstMinuteValue : newMinutes,
overflow ? firstSecondValue : newSeconds); overflow && !randomSecond ? firstSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器 // 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue)) if (!overflow && !IsMatch(newValue))
@@ -534,7 +539,7 @@ public partial class Crontab
} }
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间 // 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
if (!overflow) if (!randomHour && !overflow)
{ {
return MinDate(newValue, endTime); return MinDate(newValue, endTime);
} }
@@ -788,8 +793,15 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param> /// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param> /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns> /// <returns><see cref="int"/></returns>
private static int Increment(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow) private static int Increment(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{ {
// 检查是否是随机 R 字符解析器
if (parsers.Count == 1 && parsers.First() is RandomParser randomParser)
{
overflow = true;
return randomParser.Next(value).Value;
}
var nextValue = parsers.Select(x => x.Next(value)) var nextValue = parsers.Select(x => x.Next(value))
.Where(x => x > value) .Where(x => x > value)
.Min() .Min()
@@ -808,7 +820,7 @@ public partial class Crontab
/// <param name="defaultValue">默认值</param> /// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param> /// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns> /// <returns><see cref="int"/></returns>
private static int Decrement(IEnumerable<ITimeParser> parsers, int value, int defaultValue, out bool overflow) private static int Decrement(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{ {
var previousValue = parsers.Select(x => x.Previous(value)) var previousValue = parsers.Select(x => x.Previous(value))
.Where(x => x < value) .Where(x => x < value)

View File

@@ -69,7 +69,7 @@ internal sealed class RandomParser : ICronParser, ITimeParser
/// <returns><see cref="bool"/></returns> /// <returns><see cref="bool"/></returns>
public bool IsMatch(DateTime datetime) public bool IsMatch(DateTime datetime)
{ {
return true; return Kind is not CrontabFieldKind.Hour;
} }
/// <summary> /// <summary>

View File

@@ -168,7 +168,7 @@ public static class UnifyContext
if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null; if (context.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) return null;
// 获取序列化配置 // 获取序列化配置
var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true); var unifySerializerSettingAttribute = actionDescriptor.MethodInfo.GetFoundAttribute<UnifySerializerSettingAttribute>(true, true);
if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null; if (unifySerializerSettingAttribute == null || string.IsNullOrWhiteSpace(unifySerializerSettingAttribute.Name)) return null;
// 解析全局配置 // 解析全局配置
@@ -225,7 +225,8 @@ public static class UnifyContext
|| method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType) || method.GetRealReturnType().HasImplementedRawGeneric(unityMetadata.ResultType)
|| method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType)) || method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
|| method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) || method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData"); || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
|| method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
if (!isWebRequest) if (!isWebRequest)
{ {
@@ -255,7 +256,8 @@ public static class UnifyContext
!method.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType)) !method.CustomAttributes.Any(x => typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType))
&& method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) && method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true)
) )
|| method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData"); || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")
|| method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>);
unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider; unifyResult = isSkip ? null : App.RootServices.GetService(unityMetadata.ProviderType) as IUnifyResultProvider;
return unifyResult == null || isSkip; return unifyResult == null || isSkip;
@@ -347,7 +349,7 @@ public static class UnifyContext
/// <param name="result"></param> /// <param name="result"></param>
/// <param name="data"></param> /// <param name="data"></param>
/// <returns></returns> /// <returns></returns>
internal static bool CheckVaildResult(IActionResult result, out object data) public static bool CheckVaildResult(IActionResult result, out object data)
{ {
data = default; data = default;
@@ -398,7 +400,7 @@ public static class UnifyContext
{ {
if (method == default) return default; if (method == default) return default;
var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true); var unityProviderAttribute = method.GetFoundAttribute<UnifyProviderAttribute>(true, true);
// 获取元数据 // 获取元数据
var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata); var isExists = UnifyProviders.TryGetValue(unityProviderAttribute?.Name ?? string.Empty, out var metadata);

View File

@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="DateTime" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTime" /> 时间使用 <c>DateTime.Parse</c> 作为回退。</remarks>
public sealed class DateTimeConverterUsingDateTimeParseAsFallback : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 尝试获取 ISO 8601-1:2019 格式时间
if (!reader.TryGetDateTime(out var value))
{
value = DateTime.Parse(reader.GetString()!);
}
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -0,0 +1,38 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="DateTimeOffset" /> JSON 序列化转换器
/// </summary>
/// <remarks>在不符合 <c>ISO 8601-1:2019</c> 格式的 <see cref="DateTimeOffset" /> 时间使用 <c>DateTimeOffset.Parse</c> 作为回退。</remarks>
public sealed class DateTimeOffsetConverterUsingDateTimeOffsetParseAsFallback : JsonConverter<DateTimeOffset>
{
/// <inheritdoc />
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 尝试获取 ISO 8601-1:2019 格式时间
if (!reader.TryGetDateTimeOffset(out var value))
{
value = DateTimeOffset.Parse(reader.GetString()!);
}
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value);
}

View File

@@ -0,0 +1,37 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
using ThingsGateway.Extensions;
namespace ThingsGateway.Converters.Json;
/// <summary>
/// <see cref="string" /> JSON 序列化转换器
/// </summary>
/// <remarks>解决 Number 类型和 Boolean 类型转 String 类型时异常。</remarks>
public sealed class StringJsonConverter : JsonConverter<string>
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch
{
JsonTokenType.True or JsonTokenType.False => reader.GetBoolean().ToString(),
JsonTokenType.Number => reader.ConvertRawValueToString(),
_ => reader.GetString()
};
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteStringValue(value);
}

View File

@@ -10,6 +10,7 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
namespace ThingsGateway.Extensions; namespace ThingsGateway.Extensions;
@@ -43,7 +44,7 @@ internal static class LinqExpressionExtensions
} }
/// <summary> /// <summary>
/// 解析表达式属性名称 /// 解析表达式并获取属性的 <see cref="PropertyInfo" /> 实例
/// </summary> /// </summary>
/// <typeparam name="T">对象类型</typeparam> /// <typeparam name="T">对象类型</typeparam>
/// <typeparam name="TProperty">属性类型</typeparam> /// <typeparam name="TProperty">属性类型</typeparam>
@@ -51,48 +52,54 @@ internal static class LinqExpressionExtensions
/// <see cref="Expression{TDelegate}" /> /// <see cref="Expression{TDelegate}" />
/// </param> /// </param>
/// <returns> /// <returns>
/// <see cref="string" /> /// <see cref="PropertyInfo" />
/// </returns> /// </returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) => internal static PropertyInfo GetProperty<T, TProperty>(this Expression<Func<T, TProperty?>> propertySelector) =>
propertySelector.Body switch propertySelector.Body switch
{ {
// 检查 Lambda 表达式的主体是否是 MemberExpression 类型 // 检查 Lambda 表达式的主体是否是 MemberExpression 类型
MemberExpression memberExpression => GetPropertyName<T>(memberExpression), MemberExpression memberExpression => GetProperty<T>(memberExpression),
// 如果主体是 UnaryExpression 类型,则继续解析 // 如果主体是 UnaryExpression 类型,则继续解析
UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetPropertyName<T>( UnaryExpression { Operand: MemberExpression nestedMemberExpression } => GetProperty<T>(
nestedMemberExpression), nestedMemberExpression),
_ => throw new ArgumentException("Expression must be a simple member access (e.g. x => x.Property).",
_ => throw new ArgumentException("Expression is not valid for property selection.") nameof(propertySelector))
}; };
/// <summary> /// <summary>
/// 解析表达式属性名称 /// 从成员表达式中提取 <see cref="PropertyInfo" /> 实例
/// </summary> /// </summary>
/// <typeparam name="T">对象类型</typeparam>
/// <param name="memberExpression"> /// <param name="memberExpression">
/// <see cref="MemberExpression" /> /// <see cref="MemberExpression" />
/// </param> /// </param>
/// <typeparam name="T">对象类型</typeparam>
/// <returns> /// <returns>
/// <see cref="string" /> /// <see cref="PropertyInfo" />
/// </returns> /// </returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
internal static string GetPropertyName<T>(MemberExpression memberExpression) internal static PropertyInfo GetProperty<T>(MemberExpression memberExpression)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(memberExpression); ArgumentNullException.ThrowIfNull(memberExpression);
// 获取属性声明类型 // 确保表达式根是 T 类型的参数
var propertyType = memberExpression.Member.DeclaringType; if (memberExpression.Expression is not ParameterExpression parameterExpression ||
parameterExpression.Type != typeof(T))
// 检查是否越界访问属性
if (propertyType != typeof(T))
{ {
throw new ArgumentException("Invalid property selection."); throw new ArgumentException(
$"Expression '{memberExpression}' must refer to a member of type '{typeof(T)}'.",
nameof(memberExpression));
} }
// 返回属性名称 // 确保成员是属性(非字段)
return memberExpression.Member.Name; if (memberExpression.Member is not PropertyInfo propertyInfo)
{
throw new ArgumentException(
$"Expression '{memberExpression}' refers to a field. Only properties are supported.",
nameof(memberExpression));
}
return propertyInfo;
} }
} }

View File

@@ -11,6 +11,7 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@@ -149,7 +150,7 @@ internal static partial class StringExtensions
var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators); var pairs = (trimChar is null ? keyValueString : keyValueString.TrimStart(trimChar.Value)).Split(separators);
return (from pair in pairs return (from pair in pairs
select pair.Split('=') select pair.Split('=', 2) // 限制只分割一次
into keyValue into keyValue
where keyValue.Length == 2 where keyValue.Length == 2
select new KeyValuePair<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList(); select new KeyValuePair<string, string?>(keyValue[0].Trim(), keyValue[1])).ToList();
@@ -328,6 +329,18 @@ internal static partial class StringExtensions
}); });
} }
/// <summary>
/// 转换输入字符串中的任何转义字符
/// </summary>
/// <param name="input">
/// <see cref="string" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string? Unescape([NotNullIfNotNull(nameof(input))] this string? input) =>
string.IsNullOrWhiteSpace(input) ? input : Regex.Unescape(input);
/// <summary> /// <summary>
/// 占位符匹配正则表达式 /// 占位符匹配正则表达式
/// </summary> /// </summary>

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Buffers;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace ThingsGateway.Extensions; namespace ThingsGateway.Extensions;
@@ -34,4 +36,17 @@ internal static class Utf8JsonReaderExtensions
return jsonDocument.RootElement.Clone().GetRawText(); return jsonDocument.RootElement.Clone().GetRawText();
} }
/// <summary>
/// 从 <see cref="Utf8JsonReader" /> 中提取原始值,并将其转换为字符串
/// </summary>
/// <remarks>支持处理各种类型的原始值(例如数字、布尔值等)。</remarks>
/// <param name="reader">
/// <see cref="Utf8JsonReader" />
/// </param>
/// <returns>
/// <see cref="string" />
/// </returns>
internal static string ConvertRawValueToString(this Utf8JsonReader reader) =>
Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);
} }

View File

@@ -97,16 +97,45 @@ internal static class V5_ObjectExtensions
case ICollection collection: case ICollection collection:
count = collection.Count; count = collection.Count;
return true; return true;
// 检查对象是否实现了 IEnumerable 接口
case IEnumerable enumerable:
// 获取集合枚举数
var enumerator = enumerable.GetEnumerator();
try
{
// 检查枚举数是否可以推进到下一个元素
if (!enumerator.MoveNext())
{
count = 0;
return true;
}
// 枚举数循环推进到下一个元素并叠加推进次数
var c = 1;
while (enumerator.MoveNext())
{
c++;
}
count = c;
return true;
}
finally
{
// 检查枚举数是否实现了 IDisposable 接口
if (enumerator is IDisposable disposable)
{
disposable.Dispose();
}
}
} }
// 反射查找是否存在 Count 属性 // 反射查找是否存在 Count 属性
var runtimeProperty = obj.GetType() var runtimeProperty = obj.GetType().GetRuntimeProperty("Count");
.GetRuntimeProperty("Count");
// 反射获取 Count 属性值 // 反射获取 Count 属性值
if (runtimeProperty is not null if (runtimeProperty is not null && runtimeProperty.CanRead && runtimeProperty.PropertyType == typeof(int))
&& runtimeProperty.CanRead
&& runtimeProperty.PropertyType == typeof(int))
{ {
count = (int)runtimeProperty.GetValue(obj)!; count = (int)runtimeProperty.GetValue(obj)!;
return true; return true;

View File

@@ -38,7 +38,7 @@ public sealed class HttpContextForwardBuilder
/// <summary> /// <summary>
/// 忽略在转发时需要跳过的请求标头列表 /// 忽略在转发时需要跳过的请求标头列表
/// </summary> /// </summary>
internal static HashSet<string> _ignoreRequestHeaders = internal static readonly HashSet<string> _ignoreRequestHeaders =
[ [
Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding", Constants.X_FORWARD_TO_HEADER, "Host", "Accept", "Accept-CH", "Accept-Charset", "Accept-Encoding",
"Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges" "Accept-Language", "Accept-Patch", "Accept-Post", "Accept-Ranges"
@@ -356,8 +356,7 @@ public sealed class HttpContextForwardBuilder
if (multipartSection.AsFileSection() is not null) if (multipartSection.AsFileSection() is not null)
{ {
// 复制多部分表单内容文件节内容 // 复制多部分表单内容文件节内容
await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, httpRequestBuilder, await CopyFileMultipartSectionAsync(multipartSection, httpMultipartFormDataBuilder, cancellationToken).ConfigureAwait(false);
cancellationToken).ConfigureAwait(false);
} }
else else
{ {
@@ -410,15 +409,11 @@ public sealed class HttpContextForwardBuilder
/// <param name="httpMultipartFormDataBuilder"> /// <param name="httpMultipartFormDataBuilder">
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </param> /// </param>
/// <param name="httpRequestBuilder">
/// <see cref="HttpRequestBuilder" />
/// </param>
/// <param name="cancellationToken"> /// <param name="cancellationToken">
/// <see cref="CancellationToken" /> /// <see cref="CancellationToken" />
/// </param> /// </param>
internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection, internal static async Task CopyFileMultipartSectionAsync(MultipartSection multipartSection,
HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, HttpRequestBuilder httpRequestBuilder, HttpMultipartFormDataBuilder httpMultipartFormDataBuilder, CancellationToken cancellationToken)
CancellationToken cancellationToken)
{ {
// 初始化 MemoryStream 实例 // 初始化 MemoryStream 实例
var memoryStream = new MemoryStream(); var memoryStream = new MemoryStream();
@@ -433,10 +428,8 @@ public sealed class HttpContextForwardBuilder
var fileMultipartSection = multipartSection.AsFileSection()!; var fileMultipartSection = multipartSection.AsFileSection()!;
// 添加文件流 // 添加文件流
httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName); httpMultipartFormDataBuilder.AddStream(memoryStream, fileMultipartSection.Name, fileMultipartSection.FileName,
disposeStreamOnRequestCompletion: true);
// 添加文件流到请求结束时需要释放的集合中
httpRequestBuilder.AddDisposable(memoryStream);
} }
/// <summary> /// <summary>

View File

@@ -124,12 +124,9 @@ public sealed class HttpMultipartFormDataBuilder
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </returns> /// </returns>
/// <exception cref="JsonException"></exception> /// <exception cref="JsonException"></exception>
public HttpMultipartFormDataBuilder AddJson(object rawJson, string? name = null, Encoding? contentEncoding = null, public HttpMultipartFormDataBuilder AddJson(object? rawJson, string? name = null, Encoding? contentEncoding = null,
string? contentType = null) string? contentType = null)
{ {
// 空检查
ArgumentNullException.ThrowIfNull(rawJson);
// 检查是否配置表单名或不是字符串类型 // 检查是否配置表单名或不是字符串类型
if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString) if (!string.IsNullOrWhiteSpace(name) || rawJson is not string rawString)
{ {
@@ -292,10 +289,8 @@ public sealed class HttpMultipartFormDataBuilder
// 从互联网 URL 地址中加载流 // 从互联网 URL 地址中加载流
var fileStream = Helpers.GetStreamFromRemote(url); var fileStream = Helpers.GetStreamFromRemote(url);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(fileStream); true);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -365,10 +360,8 @@ public sealed class HttpMultipartFormDataBuilder
// 读取文件流(没有 using // 读取文件流(没有 using
var fileStream = File.OpenRead(filePath); var fileStream = File.OpenRead(filePath);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(fileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(fileStream); true);
return AddStream(fileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -407,10 +400,8 @@ public sealed class HttpMultipartFormDataBuilder
// 初始化带读写进度的文件流 // 初始化带读写进度的文件流
var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName); var progressFileStream = new ProgressFileStream(fileStream, filePath, progressChannel, newFileName);
// 添加文件流到请求结束时需要释放的集合中 return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding,
_httpRequestBuilder.AddDisposable(progressFileStream); true);
return AddStream(progressFileStream, name, newFileName, contentType, contentEncoding);
} }
/// <summary> /// <summary>
@@ -500,11 +491,12 @@ public sealed class HttpMultipartFormDataBuilder
/// <param name="fileName">文件的名称</param> /// <param name="fileName">文件的名称</param>
/// <param name="contentType">内容类型</param> /// <param name="contentType">内容类型</param>
/// <param name="contentEncoding">内容编码</param> /// <param name="contentEncoding">内容编码</param>
/// <param name="disposeStreamOnRequestCompletion">是否在请求结束后自动释放流。默认值为:<c>false</c></param>
/// <returns> /// <returns>
/// <see cref="HttpMultipartFormDataBuilder" /> /// <see cref="HttpMultipartFormDataBuilder" />
/// </returns> /// </returns>
public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null, public HttpMultipartFormDataBuilder AddStream(Stream stream, string name = "file", string? fileName = null,
string? contentType = null, Encoding? contentEncoding = null) string? contentType = null, Encoding? contentEncoding = null, bool disposeStreamOnRequestCompletion = false)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(stream);
@@ -529,6 +521,12 @@ public sealed class HttpMultipartFormDataBuilder
FileName = fileName FileName = fileName
}); });
// 是否在请求结束后自动释放流
if (disposeStreamOnRequestCompletion)
{
_httpRequestBuilder.AddDisposable(stream);
}
return this; return this;
} }
@@ -697,6 +695,20 @@ public sealed class HttpMultipartFormDataBuilder
return this; return this;
} }
/// <summary>
/// 设置是否移除默认的多部分内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpMultipartFormDataBuilder" />
/// </returns>
public HttpMultipartFormDataBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary> /// <summary>
/// 构建 <see cref="MultipartFormDataContent" /> 实例 /// 构建 <see cref="MultipartFormDataContent" /> 实例
/// </summary> /// </summary>

View File

@@ -327,23 +327,20 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param> /// <param name="key">键</param>
/// <param name="value">值</param> /// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithHeader(string key, object? value, bool escape = false, bool replace = false,
IEqualityComparer<string>? comparer = null, bool replace = false) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer, replace); return WithHeaders(new Dictionary<string, object?> { { key, value } }, escape, replace, culture);
} }
/// <summary> /// <summary>
@@ -352,25 +349,22 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="headers">请求标头集合</param> /// <param name="headers">请求标头集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false, public HttpRequestBuilder WithHeaders(IDictionary<string, object?> headers, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false) bool replace = false, CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(headers); ArgumentNullException.ThrowIfNull(headers);
// 初始化请求标头 // 初始化请求标头
Headers ??= new Dictionary<string, List<string?>>(comparer); Headers ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectHeaders = new Dictionary<string, List<object?>>(comparer); var objectHeaders = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加 // 存在则合并否则添加
objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false); objectHeaders.AddOrUpdate(Headers.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -380,7 +374,7 @@ public sealed partial class HttpRequestBuilder
Headers = objectHeaders.ToDictionary(kvp => kvp.Key, Headers = objectHeaders.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u => kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer); StringComparer.OrdinalIgnoreCase);
return this; return this;
} }
@@ -391,26 +385,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="headerSource">请求标头源对象</param> /// <param name="headerSource">请求标头源对象</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的请求标头。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithHeaders(object headerSource, bool escape = false, bool replace = false,
IEqualityComparer<string>? comparer = null, bool replace = false) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(headerSource); ArgumentNullException.ThrowIfNull(headerSource);
return WithHeaders( return WithHeaders(
headerSource.ObjectToDictionary()!.ToDictionary( headerSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, replace,
comparer, replace); culture);
} }
/// <summary> /// <summary>
@@ -474,6 +465,7 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder SetTimeout(TimeSpan timeout) public HttpRequestBuilder SetTimeout(TimeSpan timeout)
{ {
Timeout = timeout; Timeout = timeout;
TimeoutAction = null;
return this; return this;
} }
@@ -494,6 +486,43 @@ public sealed partial class HttpRequestBuilder
} }
Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds); Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
TimeoutAction = null;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeout">超时时间</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(TimeSpan timeout, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeout).TimeoutAction = onTimeout;
return this;
}
/// <summary>
/// 设置超时时间
/// </summary>
/// <param name="timeoutMilliseconds">超时时间(毫秒)</param>
/// <param name="onTimeout">超时发生时要执行的操作</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetTimeout(double timeoutMilliseconds, Action onTimeout)
{
// 空检查
ArgumentNullException.ThrowIfNull(onTimeout);
SetTimeout(timeoutMilliseconds).TimeoutAction = onTimeout;
return this; return this;
} }
@@ -570,26 +599,22 @@ public sealed partial class HttpRequestBuilder
/// <param name="key">键</param> /// <param name="key">键</param>
/// <param name="value">值</param> /// <param name="value">值</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, public HttpRequestBuilder WithQueryParameter(string key, object? value, bool escape = false, bool replace = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer, return WithQueryParameters(new Dictionary<string, object?> { { key, value } }, escape, replace,
replace, ignoreNullValues); ignoreNullValues, culture);
} }
/// <summary> /// <summary>
@@ -598,27 +623,23 @@ public sealed partial class HttpRequestBuilder
/// <remarks>支持多次调用。</remarks> /// <remarks>支持多次调用。</remarks>
/// <param name="parameters">查询参数集合</param> /// <param name="parameters">查询参数集合</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false, public HttpRequestBuilder WithQueryParameters(IDictionary<string, object?> parameters, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(parameters);
// 初始化查询参数 // 初始化查询参数
QueryParameters ??= new Dictionary<string, List<string?>>(comparer); QueryParameters ??= new Dictionary<string, List<string?>>(StringComparer.OrdinalIgnoreCase);
var objectQueryParameters = new Dictionary<string, List<object?>>(comparer); var objectQueryParameters = new Dictionary<string, List<object?>>(StringComparer.OrdinalIgnoreCase);
// 存在则合并否则添加 // 存在则合并否则添加
objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false); objectQueryParameters.AddOrUpdate(QueryParameters.ToDictionary(u => u.Key, object? (u) => u.Value), false);
@@ -629,7 +650,7 @@ public sealed partial class HttpRequestBuilder
QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key, QueryParameters = objectQueryParameters.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value.Select(u => kvp => kvp.Value.Select(u =>
u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(), u.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape)).ToList(),
comparer); StringComparer.OrdinalIgnoreCase);
return this; return this;
} }
@@ -641,20 +662,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="parameterSource">查询参数集合</param> /// <param name="parameterSource">查询参数集合</param>
/// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param> /// <param name="prefix">参数前缀。对于对象类型可生成如 <c>prefix.Name=furion</c> 与 <c>prefix.Age=30</c> 参数格式。</param>
/// <param name="escape">是否转义字符串,默认 <c>false</c></param> /// <param name="escape">是否转义字符串,默认 <c>false</c></param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c></param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c></param>
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <param name="replace">是否替换已存在的查询参数。默认值为 <c>false</c>。</param>
/// <param name="ignoreNullValues">是否忽略空值。默认值为 <c>false</c>。</param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false, public HttpRequestBuilder WithQueryParameters(object parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null, bool replace = false, bool replace = false, bool ignoreNullValues = false, CultureInfo? culture = null)
bool ignoreNullValues = false)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameterSource); ArgumentNullException.ThrowIfNull(parameterSource);
@@ -663,7 +680,7 @@ public sealed partial class HttpRequestBuilder
parameterSource.ObjectToDictionary()!.ToDictionary( parameterSource.ObjectToDictionary()!.ToDictionary(
u => u =>
$"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}", $"{(string.IsNullOrWhiteSpace(prefix) ? null : $"{prefix}.")}{u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!}",
u => u.Value), escape, culture, comparer, replace, ignoreNullValues); u => u.Value), escape, replace, ignoreNullValues, culture);
} }
/// <summary> /// <summary>
@@ -709,19 +726,16 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false, public HttpRequestBuilder WithPathParameter(string key, object? value, bool escape = false,
CultureInfo? culture = null, IEqualityComparer<string>? comparer = null) CultureInfo? culture = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer); return WithPathParameters(new Dictionary<string, object?> { { key, value } }, escape, culture);
} }
/// <summary> /// <summary>
@@ -733,26 +747,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, public HttpRequestBuilder WithPathParameters(IDictionary<string, object?> parameters, bool escape = false,
bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(parameters);
PathParameters ??= new Dictionary<string, string?>(comparer); PathParameters ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加 // 存在则更新否则添加
PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key, PathParameters.AddOrUpdate(parameters.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer)); StringComparer.OrdinalIgnoreCase));
return this; return this;
} }
@@ -767,15 +776,11 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false, public HttpRequestBuilder WithPathParameters(object? parameterSource, string? prefix = null, bool escape = false,
CultureInfo? culture = null, CultureInfo? culture = null)
IEqualityComparer<string>? comparer = null)
{ {
// 检查是否设置了模板字符串前缀 // 检查是否设置了模板字符串前缀
if (string.IsNullOrWhiteSpace(prefix)) if (string.IsNullOrWhiteSpace(prefix))
@@ -786,7 +791,7 @@ public sealed partial class HttpRequestBuilder
return WithPathParameters( return WithPathParameters(
parameterSource.ObjectToDictionary()!.ToDictionary( parameterSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape,
culture, comparer); culture);
} }
ObjectPathParameters ??= new Dictionary<string, object?>(); ObjectPathParameters ??= new Dictionary<string, object?>();
@@ -823,19 +828,15 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null, public HttpRequestBuilder WithCookie(string key, object? value, bool escape = false, CultureInfo? culture = null)
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(key);
return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture, comparer); return WithCookies(new Dictionary<string, object?> { { key, value } }, escape, culture);
} }
/// <summary> /// <summary>
@@ -847,26 +848,21 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, public HttpRequestBuilder WithCookies(IDictionary<string, object?> cookies, bool escape = false,
bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(cookies); ArgumentNullException.ThrowIfNull(cookies);
Cookies ??= new Dictionary<string, string?>(comparer); Cookies ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
// 存在则更新否则添加 // 存在则更新否则添加
Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key, Cookies.AddOrUpdate(cookies.ToDictionary(u => u.Key,
u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape), u => u.Value?.ToCultureString(culture ?? CultureInfo.InvariantCulture)?.EscapeDataString(escape),
comparer)); StringComparer.OrdinalIgnoreCase));
return this; return this;
} }
@@ -880,15 +876,10 @@ public sealed partial class HttpRequestBuilder
/// <param name="culture"> /// <param name="culture">
/// <see cref="CultureInfo" /> /// <see cref="CultureInfo" />
/// </param> /// </param>
/// <param name="comparer">
/// <see cref="IEqualityComparer{T}" />
/// </param>
/// <returns> /// <returns>
/// <see cref="HttpRequestBuilder" /> /// <see cref="HttpRequestBuilder" />
/// </returns> /// </returns>
public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, public HttpRequestBuilder WithCookies(object cookieSource, bool escape = false, CultureInfo? culture = null)
CultureInfo? culture = null,
IEqualityComparer<string>? comparer = null)
{ {
// 空检查 // 空检查
ArgumentNullException.ThrowIfNull(cookieSource); ArgumentNullException.ThrowIfNull(cookieSource);
@@ -896,8 +887,7 @@ public sealed partial class HttpRequestBuilder
// 存在则更新否则添加 // 存在则更新否则添加
return WithCookies( return WithCookies(
cookieSource.ObjectToDictionary()!.ToDictionary( cookieSource.ObjectToDictionary()!.ToDictionary(
u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture, u => u.Key.ToCultureString(culture ?? CultureInfo.InvariantCulture)!, u => u.Value), escape, culture);
comparer);
} }
/// <summary> /// <summary>
@@ -1193,6 +1183,17 @@ public sealed partial class HttpRequestBuilder
return this; return this;
} }
/// <summary>
/// 设置身份验证凭据请求授权标头
/// </summary>
/// <param name="scheme">身份验证的方案</param>
/// <param name="parameter">身份验证的凭证</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder AddAuthentication(string scheme, string? parameter) =>
AddAuthentication(new AuthenticationHeaderValue(scheme, parameter));
/// <summary> /// <summary>
/// 设置身份验证凭据请求授权标头 /// 设置身份验证凭据请求授权标头
/// </summary> /// </summary>
@@ -1328,6 +1329,17 @@ public sealed partial class HttpRequestBuilder
ReleaseDisposables(); ReleaseDisposables();
} }
/// <summary>
/// 设置请求来源地址
/// </summary>
/// <remarks>设置此配置后,将在单次请求标头中添加 <c>Referer</c> 标头。</remarks>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetReferer(string? referer) =>
WithHeader(HeaderNames.Referer, referer, replace: true);
/// <summary> /// <summary>
/// 设置模拟浏览器环境 /// 设置模拟浏览器环境
/// </summary> /// </summary>
@@ -1364,6 +1376,17 @@ public sealed partial class HttpRequestBuilder
public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) => public HttpRequestBuilder WithAnyStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler(["*"], handler); WithStatusCodeHandler(["*"], handler);
/// <summary>
/// 添加请求成功200-299状态码处理程序
/// </summary>
/// <param name="handler">自定义处理程序</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder
WithSuccessStatusCodeHandler(Func<HttpResponseMessage, CancellationToken, Task> handler) =>
WithStatusCodeHandler("200-299", handler);
/// <summary> /// <summary>
/// 添加状态码处理程序 /// 添加状态码处理程序
/// </summary> /// </summary>
@@ -1590,6 +1613,107 @@ public sealed partial class HttpRequestBuilder
? null ? null
: new Uri(baseAddress, UriKind.RelativeOrAbsolute)); : new Uri(baseAddress, UriKind.RelativeOrAbsolute));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">版本号</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(string? version) =>
SetVersion(string.IsNullOrWhiteSpace(version) ? null : new Version(version));
/// <summary>
/// 设置 HTTP 版本
/// </summary>
/// <param name="version">
/// <see cref="Version" />
/// </param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetVersion(Version? version)
{
Version = version;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>抑制所有异常。重复调用仅最后一次调用生效。</remarks>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions() => SuppressExceptions(true);
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="enable">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SuppressExceptions(bool enable) => SuppressExceptions(enable ? [typeof(Exception)] : []);
/// <summary>
/// 设置是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <param name="omit">如果为 <c>true</c> 则移除,默认为 <c>false</c></param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
public HttpRequestBuilder SetOmitContentType(bool omit)
{
OmitContentType = omit;
return this;
}
/// <summary>
/// 设置异常抑制
/// </summary>
/// <remarks>重复调用仅最后一次调用生效。</remarks>
/// <param name="exceptionTypes">异常抑制类型集合</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
public HttpRequestBuilder SuppressExceptions(Type[] exceptionTypes)
{
// 空检查
ArgumentNullException.ThrowIfNull(exceptionTypes);
// 检查是否包含 null 或者不是 Exception 类型的元素
if (exceptionTypes.Any(u => (Type?)u is null || !typeof(Exception).IsAssignableFrom(u)))
{
throw new ArgumentException(
"All elements in exceptionTypes must be non-null and assignable to System.Exception.");
}
// 释放引用(无关紧要)
SuppressExceptionTypes = null;
// 空检查
if (exceptionTypes.Length == 0)
{
return this;
}
// 确保每次都能覆盖
SuppressExceptionTypes = [];
// 遍历异常抑制类型集合逐条追加
foreach (var exceptionType in exceptionTypes)
{
SuppressExceptionTypes.Add(exceptionType);
}
return this;
}
/// <summary> /// <summary>
/// 释放可释放的对象集合 /// 释放可释放的对象集合
/// </summary> /// </summary>

View File

@@ -157,6 +157,11 @@ public sealed partial class HttpRequestBuilder
/// </summary> /// </summary>
public Uri? BaseAddress { get; private set; } public Uri? BaseAddress { get; private set; }
/// <summary>
/// HTTP 版本
/// </summary>
public Version? Version { get; private set; }
/// <summary> /// <summary>
/// <see cref="HttpClient" /> 实例提供器 /// <see cref="HttpClient" /> 实例提供器
/// </summary> /// </summary>
@@ -181,7 +186,7 @@ public sealed partial class HttpRequestBuilder
/// <summary> /// <summary>
/// 用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作 /// 用于处理在设置 <see cref="HttpRequestMessage" /> 的请求消息的内容时的操作
/// </summary> /// </summary>
public Action<HttpContent?>? OnPreSetContent { get; private set; } public Action<HttpContent>? OnPreSetContent { get; private set; }
/// <summary> /// <summary>
/// 用于处理在发送 HTTP 请求之前的操作 /// 用于处理在发送 HTTP 请求之前的操作
@@ -201,7 +206,13 @@ public sealed partial class HttpRequestBuilder
/// <summary> /// <summary>
/// <inheritdoc cref="HttpMultipartFormDataBuilder" /> /// <inheritdoc cref="HttpMultipartFormDataBuilder" />
/// </summary> /// </summary>
internal HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; } public HttpMultipartFormDataBuilder? MultipartFormDataBuilder { get; private set; }
/// <summary>
/// 是否移除默认的内容的 <c>Content-Type</c>
/// </summary>
/// <remarks>默认值为:<c>false</c>。</remarks>
public bool OmitContentType { get; private set; }
/// <summary> /// <summary>
/// 如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。 /// 如果 HTTP 响应的 <c>IsSuccessStatusCode</c> 属性是 <c>false</c>,则引发异常。
@@ -273,4 +284,15 @@ public sealed partial class HttpRequestBuilder
get; get;
private set; private set;
} }
/// <summary>
/// 异常抑制类型集合
/// </summary>
/// <remarks>当配置了异常抑制类型集合后,框架将抑制(即不抛出)该集合中匹配的异常类型。</remarks>
internal HashSet<Type>? SuppressExceptionTypes { get; private set; }
/// <summary>
/// 超时发生时要执行的操作
/// </summary>
internal Action? TimeoutAction { get; private set; }
} }

View File

@@ -10,6 +10,9 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
using System.Reflection; using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
@@ -614,4 +617,116 @@ public sealed partial class HttpRequestBuilder
/// </returns> /// </returns>
public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) => public static HttpDeclarativeBuilder Declarative(MethodInfo method, object?[] args) =>
new(method, args); new(method, args);
/// <summary>
/// 从 JSON 中创建 <see cref="HttpRequestBuilder" /> 实例
/// </summary>
/// <param name="json">JSON 字符串</param>
/// <param name="configure">自定义配置委托</param>
/// <returns>
/// <see cref="HttpRequestBuilder" />
/// </returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static HttpRequestBuilder FromJson(string json, Action<HttpRequestBuilder>? configure = null)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(json);
/*
* 手动解析 JSON 字符串
*
* 不采用 JSON 反序列化的原因如下:
* 1. HttpRequestBuilder 的属性设计为只读,无法直接通过反序列化赋值。
* 2. 避免引入 [JsonInclude] 特性对 System.Text.Json 的强耦合,保持依赖解耦。
* 3. 简化 JSON 字符串的结构定义,无需严格遵循 HttpRequestBuilder 的属性定义,从而省略 [JsonPropertyName] 等自定义映射。
* 4. 精确控制需要解析的键,减少不必要的自定义 JsonConverter 操作,提升性能与可维护性。
*/
var jsonObject = JsonNode.Parse(json, new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { AllowTrailingCommas = true })?.AsObject();
// 空检查
ArgumentNullException.ThrowIfNull(jsonObject);
// 验证必填字段
if (!jsonObject.TryGetPropertyValue("method", out var methodNode) || methodNode is not JsonValue methodValue)
{
throw new ArgumentException("Missing required `method` in JSON.");
}
// 允许 "url" 为 null但必须定义
if (!jsonObject.ContainsKey("url"))
{
throw new ArgumentException("Missing required `url` in JSON.");
}
// 初始化 HttpRequestBuilder 实例
var httpRequestBuilder = Create(methodValue.ToString(), jsonObject["url"]?.GetValue<string?>());
// 处理可选字段
HandleJsonNode(jsonObject, "baseAddress", node => httpRequestBuilder.SetBaseAddress(node.GetValue<string>()));
HandleJsonNode(jsonObject, "headers", node => httpRequestBuilder.WithHeaders(node));
HandleJsonNode(jsonObject, "queries", node => httpRequestBuilder.WithQueryParameters(node));
HandleJsonNode(jsonObject, "cookies", node => httpRequestBuilder.WithCookies(node));
HandleJsonNode(jsonObject, "timeout", node => httpRequestBuilder.SetTimeout(node.GetValue<double>()));
HandleJsonNode(jsonObject, "client", node => httpRequestBuilder.SetHttpClientName(node.GetValue<string?>()));
HandleJsonNode(jsonObject, "profiler", node => httpRequestBuilder.Profiler(node.GetValue<bool>()));
// 处理请求内容
if (jsonObject.TryGetPropertyValue("data", out var dataNode))
{
// "data" 和 "contentType" 必须同时存在或同时不存在
if (!jsonObject.TryGetPropertyValue("contentType", out var contentTypeNode) ||
contentTypeNode is not JsonValue contentTypeValue)
{
throw new InvalidOperationException("The `contentType` key is required when `data` is present.");
}
// 设置请求内容
httpRequestBuilder
.SetContent(
dataNode?.ToJsonString(new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}), contentTypeValue.ToString()).AddStringContentForFormUrlEncodedContentProcessor();
// 设置内容编码
HandleJsonNode(jsonObject, "encoding",
node => httpRequestBuilder.SetContentEncoding(node.GetValue<string>()));
}
// 处理多部分表单
if (jsonObject.TryGetPropertyValue("multipart", out var multipartNode))
{
// 设置多部分表单内容
httpRequestBuilder.SetMultipartContent(multipart => multipart.AddJson(multipartNode?.AsObject()
.ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })));
}
// 调用自定义配置委托
configure?.Invoke(httpRequestBuilder);
return httpRequestBuilder;
}
/// <summary>
/// 处理 <see cref="JsonNode" />
/// </summary>
/// <param name="jsonObject">
/// <see cref="JsonObject" />
/// </param>
/// <param name="propertyName">属性名</param>
/// <param name="action">自定义操作</param>
internal static void HandleJsonNode(JsonObject jsonObject, string propertyName, Action<JsonNode> action)
{
// 空检查
ArgumentNullException.ThrowIfNull(jsonObject);
ArgumentNullException.ThrowIfNull(propertyName);
ArgumentNullException.ThrowIfNull(action);
if (jsonObject.TryGetPropertyValue(propertyName, out var node) && node is not null)
{
action(node);
}
}
} }

View File

@@ -76,6 +76,12 @@ public sealed partial class HttpRequestBuilder
// 初始化 HttpRequestMessage 实例 // 初始化 HttpRequestMessage 实例
var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri); var httpRequestMessage = new HttpRequestMessage(HttpMethod, finalRequestUri);
// 设置 HTTP 版本
if (Version is not null)
{
httpRequestMessage.Version = Version;
}
// 启用性能优化 // 启用性能优化
EnablePerformanceOptimization(httpRequestMessage); EnablePerformanceOptimization(httpRequestMessage);
@@ -160,18 +166,44 @@ public sealed partial class HttpRequestBuilder
/// </param> /// </param>
internal void AppendPathSegments(UriBuilder uriBuilder) internal void AppendPathSegments(UriBuilder uriBuilder)
{ {
// 空检查
if ((PathSegments == null || PathSegments.Count == 0) &&
(PathSegmentsToRemove == null || PathSegmentsToRemove.Count == 0))
{
return;
}
// 记录原路径是否以斜杠结尾(修复核心逻辑)
var originalPath = uriBuilder.Uri.AbsolutePath;
var endsWithSlash = originalPath.Length > 1 && originalPath.EndsWith('/');
// 解析 URL 中的路径片段列表 // 解析 URL 中的路径片段列表
var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Concat([]); var pathSegments = uriBuilder.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
// 追加路径片段 // 追加并处理新路径片段
pathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([]).Where(u => !string.IsNullOrWhiteSpace(u)) var newPathSegments = pathSegments.Concat(PathSegments.ConcatIgnoreNull([])
.Select(u => u.TrimStart('/').TrimEnd('/'))); .Where(u => !string.IsNullOrWhiteSpace(u)).Select(u => u.TrimStart('/').TrimEnd('/')));
// 构建路径片段赋值给 UriBuilder 的 Path 属性 // 过滤需要移除的路径片段
uriBuilder.Path = '/' + string.Join('/', var filteredSegments = newPathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
// 过滤已标记为移除的路径片段 u => PathSegmentsToRemove?.Contains(u) == false).ToArray();
pathSegments.WhereIf(PathSegmentsToRemove is { Count: > 0 },
u => PathSegmentsToRemove?.TryGetValue(u, out _) == false)); // 构建最终路径
if (filteredSegments.Length != 0)
{
uriBuilder.Path = $"/{string.Join('/', filteredSegments)}";
// 恢复原路径的结尾斜杠(当存在路径片段时)
if (endsWithSlash)
{
uriBuilder.Path += "/";
}
}
// 没有路径片段时设置为根路径
else
{
uriBuilder.Path = "/";
}
} }
/// <summary> /// <summary>
@@ -182,6 +214,13 @@ public sealed partial class HttpRequestBuilder
/// </param> /// </param>
internal void AppendQueryParameters(UriBuilder uriBuilder) internal void AppendQueryParameters(UriBuilder uriBuilder)
{ {
// 空检查
if ((QueryParameters is null || QueryParameters.Count == 0) &&
(QueryParametersToRemove is null || QueryParametersToRemove.Count == 0))
{
return;
}
// 解析 URL 中的查询字符串为键值对列表 // 解析 URL 中的查询字符串为键值对列表
var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?'); var queryParameters = uriBuilder.Query.ParseFormatKeyValueString(['&'], '?');
@@ -300,6 +339,16 @@ public sealed partial class HttpRequestBuilder
// 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中 // 遍历请求标头集合并追加到 HttpRequestMessage.Headers 中
foreach (var (key, values) in Headers) foreach (var (key, values) in Headers)
{ {
// 替换 Referer 标头的 "{BASE_ADDRESS}" 模板字符串
if (key.IsIn([HeaderNames.Referer], StringComparer.OrdinalIgnoreCase) &&
values.FirstOrDefault() == Constants.REFERER_HEADER_BASE_ADDRESS_TEMPLATE)
{
httpRequestMessage.Headers.Referrer = new Uri(
$"{httpRequestMessage.RequestUri?.Scheme}://{httpRequestMessage.RequestUri?.Host}{(httpRequestMessage.RequestUri?.IsDefaultPort != true ? $":{httpRequestMessage.RequestUri?.Port}" : string.Empty)}",
UriKind.RelativeOrAbsolute);
continue;
}
httpRequestMessage.Headers.TryAddWithoutValidation(key, values); httpRequestMessage.Headers.TryAddWithoutValidation(key, values);
} }
} }
@@ -486,6 +535,18 @@ public sealed partial class HttpRequestBuilder
// 构建 HttpContent 实例 // 构建 HttpContent 实例
var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors); var httpContent = httpContentProcessorFactory.Build(RawContent, ContentType!, ContentEncoding, processors);
// 空检查
if (httpContent is null)
{
return;
}
// 检查是否移除默认的内容的 Content-Type解决对接 Java 程序时可能出现失败问题
if (OmitContentType)
{
httpContent.Headers.ContentType = null;
}
// 调用用于处理在设置请求消息的内容时的操作 // 调用用于处理在设置请求消息的内容时的操作
OnPreSetContent?.Invoke(httpContent); OnPreSetContent?.Invoke(httpContent);
@@ -513,6 +574,9 @@ public sealed partial class HttpRequestBuilder
{ {
httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE"); httpRequestMessage.Options.AddOrUpdate(Constants.DISABLED_PROFILER_KEY, "TRUE");
} }
// 添加 HttpClient 实例的配置名称
httpRequestMessage.Options.AddOrUpdate(Constants.HTTP_CLIENT_NAME, HttpClientName ?? string.Empty);
} }
/// <summary> /// <summary>

View File

@@ -88,15 +88,26 @@ internal static class Constants
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks> /// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__"; internal const string DECLARATIVE_METHOD_KEY = "__DECLARATIVE_METHOD__";
/// <summary>
/// HTTP 请求 <see cref="HttpClient" /> 实例的配置名称键
/// </summary>
/// <remarks>被用于从 <see cref="HttpRequestMessage" /> 的 <c>Options</c> 属性中读取。</remarks>
internal const string HTTP_CLIENT_NAME = "__HTTP_CLIENT_NAME__";
/// <summary> /// <summary>
/// 浏览器的 <c>User-Agent</c> 标头值 /// 浏览器的 <c>User-Agent</c> 标头值
/// </summary> /// </summary>
internal const string USER_AGENT_OF_BROWSER = internal const string USER_AGENT_OF_BROWSER =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"; "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0";
/// <summary> /// <summary>
/// 移动端浏览器的 <c>User-Agent</c> 标头值 /// 移动端浏览器的 <c>User-Agent</c> 标头值
/// </summary> /// </summary>
internal const string USER_AGENT_OF_MOBILE_BROWSER = internal const string USER_AGENT_OF_MOBILE_BROWSER =
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Mobile Safari/537.36 Edg/133.0.0.0"; "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 Edg/135.0.0.0";
/// <summary>
/// <c>Referer</c> 标头请求基地址模板
/// </summary>
internal const string REFERER_HEADER_BASE_ADDRESS_TEMPLATE = "{BASE_ADDRESS}";
} }

View File

@@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
namespace ThingsGateway.HttpRemote; namespace ThingsGateway.HttpRemote;
@@ -27,16 +28,45 @@ public class ObjectContentConverter : IHttpContentConverter
/// <inheritdoc /> /// <inheritdoc />
public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage, public virtual object? Read(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync(resultType, httpResponseMessage.Content
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? .ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage), cancellationToken)
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); .GetAwaiter().GetResult();
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage, public virtual async Task<object?> ReadAsync(Type resultType, HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync(resultType, await httpResponseMessage.Content.ReadFromJsonAsync(resultType, GetJsonSerializerOptions(httpResponseMessage),
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? cancellationToken).ConfigureAwait(false);
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
/// <summary>
/// 获取 JSON 序列化选项实例
/// </summary>
/// <param name="httpResponseMessage">
/// <see cref="HttpResponseMessage" />
/// </param>
/// <returns>
/// <see cref="JsonSerializerOptions" />
/// </returns>
protected virtual JsonSerializerOptions GetJsonSerializerOptions(HttpResponseMessage httpResponseMessage)
{
// 空检查
ArgumentNullException.ThrowIfNull(httpResponseMessage);
// 获取 HttpClient 实例的配置名称
if (httpResponseMessage.RequestMessage?.Options.TryGetValue(
new HttpRequestOptionsKey<string>(Constants.HTTP_CLIENT_NAME), out var httpClientName) != true)
{
httpClientName = string.Empty;
}
// 获取 HttpClientOptions 实例
var httpClientOptions = ServiceProvider?.GetService<IOptionsMonitor<HttpClientOptions>>()?.Get(httpClientName);
// 优先级:指定名称的 HttpClientOptions -> HttpRemoteOptions -> 默认值
return (httpClientOptions?.IsDefault != false ? null : httpClientOptions.JsonSerializerOptions) ??
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault;
}
} }
/// <summary> /// <summary>
@@ -48,14 +78,13 @@ public class ObjectContentConverter<TResult> : ObjectContentConverter, IHttpCont
/// <inheritdoc /> /// <inheritdoc />
public virtual TResult? Read(HttpResponseMessage httpResponseMessage, public virtual TResult? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
httpResponseMessage.Content.ReadFromJsonAsync<TResult>( httpResponseMessage.Content
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? .ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage), cancellationToken).GetAwaiter()
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).GetAwaiter().GetResult(); .GetResult();
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage, public virtual async Task<TResult?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await httpResponseMessage.Content.ReadFromJsonAsync<TResult>( await httpResponseMessage.Content.ReadFromJsonAsync<TResult>(GetJsonSerializerOptions(httpResponseMessage),
ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ?? cancellationToken).ConfigureAwait(false);
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false);
} }

View File

@@ -18,10 +18,10 @@ public class VoidContentConverter : HttpContentConverterBase<VoidContent>
{ {
/// <inheritdoc /> /// <inheritdoc />
public override VoidContent? Read(HttpResponseMessage httpResponseMessage, public override VoidContent? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => default; CancellationToken cancellationToken = default) => null;
/// <inheritdoc /> /// <inheritdoc />
public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage, public override Task<VoidContent?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
Task.FromResult<VoidContent?>(default); Task.FromResult<VoidContent?>(null);
} }

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 HTTP 版本特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class HttpVersionAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="HttpVersionAttribute" />
/// </summary>
/// <param name="version">HTTP 版本</param>
public HttpVersionAttribute(string? version) => Version = version;
/// <summary>
/// HTTP 版本
/// </summary>
public string? Version { get; set; }
}

View File

@@ -0,0 +1,30 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式请求来源地址特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class RefererAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="RefererAttribute" />
/// </summary>
/// <param name="referer">请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址</param>
public RefererAttribute(string? referer) => Referer = referer;
/// <summary>
/// 请求来源地址,当设置为 <c>"{BASE_ADDRESS}"</c> 时将替换为基地址
/// </summary>
public string? Referer { get; set; }
}

View File

@@ -0,0 +1,45 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式异常抑制特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public sealed class SuppressExceptionsAttribute : Attribute
{
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <remarks>抑制所有异常。</remarks>
public SuppressExceptionsAttribute()
: this(true)
{
}
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="enabled">是否启用异常抑制。当设置为 <c>false</c> 时,将禁用异常抑制机制。</param>
public SuppressExceptionsAttribute(bool enabled) => Types = enabled ? [typeof(Exception)] : [];
/// <summary>
/// <inheritdoc cref="SuppressExceptionsAttribute" />
/// </summary>
/// <param name="types">异常抑制类型集合</param>
public SuppressExceptionsAttribute(params Type[] types) => Types = types;
/// <summary>
/// 异常抑制类型集合
/// </summary>
public Type[] Types { get; set; }
}

View File

@@ -44,8 +44,11 @@ public sealed class HttpDeclarativeBuilder
new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()), new(typeof(QueryDeclarativeExtractor), new QueryDeclarativeExtractor()),
new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()), new(typeof(PathDeclarativeExtractor), new PathDeclarativeExtractor()),
new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()), new(typeof(CookieDeclarativeExtractor), new CookieDeclarativeExtractor()),
new(typeof(RefererDeclarativeExtractor), new RefererDeclarativeExtractor()),
new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()), new(typeof(HeaderDeclarativeExtractor), new HeaderDeclarativeExtractor()),
new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()), new(typeof(PropertyDeclarativeExtractor), new PropertyDeclarativeExtractor()),
new(typeof(HttpVersionDeclarativeExtractor), new HttpVersionDeclarativeExtractor()),
new(typeof(SuppressExceptionsDeclarativeExtractor), new SuppressExceptionsDeclarativeExtractor()),
new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor()) new(typeof(BodyDeclarativeExtractor), new BodyDeclarativeExtractor())
]); ]);

View File

@@ -45,7 +45,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (headerAttribute.HasSetValue) if (headerAttribute.HasSetValue)
{ {
httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape, httpRequestBuilder.WithHeader(headerName, headerAttribute.Value, headerAttribute.Escape,
replace: headerAttribute.Replace); headerAttribute.Replace);
} }
// 移除请求标头 // 移除请求标头
else else
@@ -91,7 +91,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection()) if (parameter.ParameterType.IsBaseTypeOrEnumOrCollection())
{ {
httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value, httpRequestBuilder.WithHeader(parameterName, value ?? headerAttribute.Value,
headerAttribute.Escape, replace: headerAttribute.Replace); headerAttribute.Escape, headerAttribute.Replace);
continue; continue;
} }
@@ -99,7 +99,7 @@ internal sealed class HeaderDeclarativeExtractor : IHttpDeclarativeExtractor
// 空检查 // 空检查
if (value is not null) if (value is not null)
{ {
httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, replace: headerAttribute.Replace); httpRequestBuilder.WithHeaders(value, headerAttribute.Escape, headerAttribute.Replace);
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More