Compare commits

...

70 Commits

Author SHA1 Message Date
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
Diego
f73245e650 build: 10.5.10 2025-04-30 15:31:39 +08:00
2248356998 qq.com
484461fa05 更新依赖 2025-04-30 15:29:19 +08:00
Diego
7e0b7aff2a feat: sqldb支持数组 2025-04-28 15:52:32 +08:00
Diego
6c450dcb09 feat: sqldb支持数组类型 2025-04-28 15:52:11 +08:00
Diego
227f44283f build: 10.5.8 2025-04-28 15:30:10 +08:00
Diego
74f6e79625 build: 10.5.7
优化opcua变量缓存
修复cron表达式间隔
2025-04-27 16:35:58 +08:00
Diego
cec43e2ce8 feat: 防呆设计,强制设置通道的最大并发数 2025-04-27 10:13:35 +08:00
Diego
7553b258bb build: 10.5.5 2025-04-26 17:46:31 +08:00
Diego
8bdbdc117e 支持bind链路设置多个通道 2025-04-26 17:10:46 +08:00
Diego
0e206be296 更新依赖 2025-04-26 16:17:02 +08:00
Diego
00b7353433 兼容性增强 2025-04-26 15:53:20 +08:00
Diego
44e7a83593 更新依赖 2025-04-26 15:02:57 +08:00
Diego
dd68d555d4 feat: 支持字节数组上传 2025-04-24 10:25:36 +08:00
2248356998 qq.com
0456296103 更新依赖包 2025-04-23 23:07:27 +08:00
Diego
a1b66277ff nuget 2025-04-23 15:28:53 +08:00
Diego
50758b79bc 更新依赖包 2025-04-23 11:48:45 +08:00
Diego
06a1f902ad 修改Startup方法名称 2025-04-23 10:56:03 +08:00
Diego
58f8b23b7c build:10.5.1
fix: 反向代理正确获取客户端IP

refactor: 添加demo站点域名
2025-04-22 11:45:14 +08:00
Diego
9e7c348b15 添加大数据写入方法 2025-04-21 23:05:10 +08:00
Diego
5f5ff8b43b 删除实体部分特性 2025-04-21 17:53:33 +08:00
Diego
f626b4e5fc sql插件增加分表模式 属性 2025-04-21 11:00:05 +08:00
2248356998 qq.com
bc23200e66 更新依赖 2025-04-20 20:32:41 +08:00
Diego
95ab59fd5a build: 10.4.21 2025-04-18 11:27:48 +08:00
Diego
0bbee003b0 app.json 2025-04-18 09:21:31 +08:00
431 changed files with 11548 additions and 6200 deletions

View File

@@ -20,11 +20,14 @@ A cross-platform, high-performance edge data collection gateway based on net9.
## Demo ## Demo
 
[Demo](http://47.119.161.158:5000/) [Demo](https://demo.thingsgateway.cn/)
 
Account: **SuperAdmin** Account: **SuperAdmin**
 
Password: **111111** Password: **111111**
 
**In the upper-right corner, switch to the IoT Gateway module in the personal popup box** **In the upper-right corner, switch to the IoT Gateway module in the personal popup box**

View File

@@ -13,7 +13,7 @@
## 演示 ## 演示
[ThingsGateway演示地址](http://47.119.161.158:5000/) [ThingsGateway演示地址](https://demo.thingsgateway.cn/)
账户 : **SuperAdmin** 账户 : **SuperAdmin**

View File

@@ -115,7 +115,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,19 +127,19 @@ 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
{ {
Name = (localizerType == null ? App.CreateLocalizerByType(typeof(OperDescAttribute)) : App.CreateLocalizerByType(localizerType))![Description], Name = (localizerType == null ? App.CreateLocalizerByType(typeof(OperDescAttribute)) : App.CreateLocalizerByType(localizerType))![Description],
Category = LogCateGoryEnum.Operate, Category = LogCateGoryEnum.Operate,
ExeStatus = true, ExeStatus = true,
OpIp = AppService?.RemoteIpAddress?.MapToIPv4()?.ToString() ?? 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

@@ -26,6 +26,7 @@ namespace ThingsGateway.Admin.Application;
[Route("openapi/auth")] [Route("openapi/auth")]
[Authorize(AuthenticationSchemes = "Bearer")] [Authorize(AuthenticationSchemes = "Bearer")]
[LoggingMonitor] [LoggingMonitor]
[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.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
} }
return Task.CompletedTask;
}
} }

View File

@@ -23,7 +23,7 @@ public class SysDict : BaseEntity
/// <summary> /// <summary>
/// 类型 /// 类型
///</summary> ///</summary>
[SugarColumn(ColumnDescription = "类型", Length = 200)] [SugarColumn(ColumnDescription = "类型")]
[AutoGenerateColumn(Ignore = true, Filterable = true, Sortable = true)] [AutoGenerateColumn(Ignore = true, Filterable = true, Sortable = true)]
public virtual DictTypeEnum DictType { get; set; } public virtual DictTypeEnum DictType { get; set; }

View File

@@ -24,7 +24,7 @@ public class SysOperateLog
/// <summary> /// <summary>
/// 日志分类 /// 日志分类
///</summary> ///</summary>
[SugarColumn(ColumnDescription = "日志分类", Length = 200)] [SugarColumn(ColumnDescription = "日志分类")]
[AutoGenerateColumn(Order = 1, Filterable = true, Sortable = true)] [AutoGenerateColumn(Order = 1, Filterable = true, Sortable = true)]
public LogCateGoryEnum Category { get; set; } public LogCateGoryEnum Category { get; set; }

View File

@@ -54,7 +54,7 @@ public class SysPosition : BaseEntity
/// <summary> /// <summary>
/// 分类 /// 分类
///</summary> ///</summary>
[SugarColumn(ColumnName = "Category", ColumnDescription = "分类", Length = 200)] [SugarColumn(ColumnName = "Category", ColumnDescription = "分类")]
[AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)] [AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
public virtual PositionCategoryEnum Category { get; set; } public virtual PositionCategoryEnum Category { get; set; }

View File

@@ -22,7 +22,7 @@ public class SysRelation : PrimaryKeyEntity
/// <summary> /// <summary>
/// 分类 /// 分类
///</summary> ///</summary>
[SugarColumn(ColumnDescription = "分类", Length = 200)] [SugarColumn(ColumnDescription = "分类")]
public RelationCategoryEnum Category { get; set; } public RelationCategoryEnum Category { get; set; }
/// <summary> /// <summary>

View File

@@ -41,7 +41,7 @@ public class SysRole : BaseEntity
/// <summary> /// <summary>
/// 分类 /// 分类
///</summary> ///</summary>
[SugarColumn(ColumnDescription = "分类", Length = 200, IsNullable = false)] [SugarColumn(ColumnDescription = "分类", IsNullable = false)]
[AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)] [AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
public virtual RoleCategoryEnum Category { get; set; } public virtual RoleCategoryEnum Category { get; set; }

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()
{
var historyHardwareInfos = MemoryCache.Get<List<HistoryHardwareInfo>>(CacheKey);
if (historyHardwareInfos == null)
{ {
using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew(); using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
return await db.Queryable<HistoryHardwareInfo>().ToListAsync().ConfigureAwait(false); 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,4 +1,15 @@
{ {
"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": "用户登录已过期,请重新登录"
}, },
@@ -24,9 +35,6 @@
"LatestLoginTime": "最新登录时间", "LatestLoginTime": "最新登录时间",
"LatestLoginDevice": "最新登录设备", "LatestLoginDevice": "最新登录设备",
"LatestLoginAddress": "最新登录地点", "LatestLoginAddress": "最新登录地点",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"OrgNames": "机构名称", "OrgNames": "机构名称",
"PositionName": "职位名称", "PositionName": "职位名称",
"OrgId": "机构", "OrgId": "机构",
@@ -60,12 +68,9 @@
"Name": "名称", "Name": "名称",
"Name.Required": " {0} 是必填项", "Name.Required": " {0} 是必填项",
"Category": "分类", "Category": "分类",
"SortCode": "排序",
"Global": "全局", "Global": "全局",
"Status": "状态", "Status": "状态",
"OrgId": "机构", "OrgId": "机构",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"CanotDeleteAdmin": "不可删除系统内置超管角色", "CanotDeleteAdmin": "不可删除系统内置超管角色",
"CanotEditAdmin": "不可编辑超管角色", "CanotEditAdmin": "不可编辑超管角色",
@@ -103,10 +108,7 @@
"Category": "分类", "Category": "分类",
"Target": "跳转类型", "Target": "跳转类型",
"NavLinkMatch": "匹配类型", "NavLinkMatch": "匹配类型",
"SortCode": "排序",
"ParentId": "上级菜单", "ParentId": "上级菜单",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"ResourceDup": "存在重复的名称 {0}", "ResourceDup": "存在重复的名称 {0}",
"ResourceParentChoiceSelf": "父级不能选择自己", "ResourceParentChoiceSelf": "父级不能选择自己",
"ResourceParentNull": "父级不存在 {0}", "ResourceParentNull": "父级不存在 {0}",
@@ -132,9 +134,6 @@
"Status": "状态", "Status": "状态",
"OrgId": "机构", "OrgId": "机构",
"Remark": "备注", "Remark": "备注",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"Dup": "存在重复的岗位 分类 {0} 名称 {1}", "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}", "CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}", "NameDup": "存在重复的名称 {0}",
@@ -158,9 +157,6 @@
"Names": "机构全称", "Names": "机构全称",
"Remark": "备注", "Remark": "备注",
"DirectorId": "主管", "DirectorId": "主管",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"Dup": "存在重复的机构 分类 {0} 名称 {1}", "Dup": "存在重复的机构 分类 {0} 名称 {1}",
"CodeDup": "存在重复的编码 {0}", "CodeDup": "存在重复的编码 {0}",
"NameDup": "存在重复的名称 {0}", "NameDup": "存在重复的名称 {0}",
@@ -357,9 +353,6 @@
"Name": "名称", "Name": "名称",
"Code": "代码", "Code": "代码",
"Remark": "备注", "Remark": "备注",
"SortCode": "排序",
"CreateTime": "创建时间",
"UpdateTime": "更新时间",
"DictDup": "存在重复的配置 分类 {0} 名称 {1}", "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
"DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置" "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
}, },
@@ -466,4 +459,4 @@
"SUCCESS": "成功", "SUCCESS": "成功",
"FAIL": "失败" "FAIL": "失败"
} }
} }

View File

@@ -18,8 +18,6 @@ using ThingsGateway.Logging;
using ThingsGateway.NewLife.Json.Extension; using ThingsGateway.NewLife.Json.Extension;
using ThingsGateway.Razor; using ThingsGateway.Razor;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
/// <summary> /// <summary>
@@ -53,7 +51,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
if (loggingMonitor.Validation == null) if (loggingMonitor.Validation == null)
{ {
var operation = logMsg.Context.Get(LoggingConst.Operation).ToString();//获取操作名称 var operation = logMsg.Context.Get(LoggingConst.Operation).ToString();//获取操作名称
var client = (ClientInfo)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息 var client = (UserAgent)logMsg.Context.Get(LoggingConst.Client);//获取客户端信息
var path = logMsg.Context.Get(LoggingConst.Path).ToString();//获取操作名称 var path = logMsg.Context.Get(LoggingConst.Path).ToString();//获取操作名称
var method = logMsg.Context.Get(LoggingConst.Method).ToString();//获取方法 var method = logMsg.Context.Get(LoggingConst.Method).ToString();//获取方法
//表示访问日志 //表示访问日志
@@ -92,16 +90,16 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
/// <param name="operation">操作名称</param> /// <param name="operation">操作名称</param>
/// <param name="path">请求地址</param> /// <param name="path">请求地址</param>
/// <param name="loggingMonitor">loggingMonitor</param> /// <param name="loggingMonitor">loggingMonitor</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, LoggingMonitorJson loggingMonitor, UserAgent userAgent, bool flush)
{ {
//账号 //账号
var opAccount = loggingMonitor.AuthorizationClaims?.Where(it => it.Type == ClaimConst.Account).Select(it => it.Value).FirstOrDefault(); var opAccount = loggingMonitor.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 = loggingMonitor.Parameters == null || loggingMonitor.Parameters.Count == 0 ? null : loggingMonitor.Parameters[0].Value.ToSystemTextJsonString();
//获取结果json字符串 //获取结果json字符串
var resultJson = string.Empty; var resultJson = string.Empty;
@@ -109,7 +107,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
{ {
if (loggingMonitor.ReturnInformation.Value != null)//如果返回值不为空 if (loggingMonitor.ReturnInformation.Value != null)//如果返回值不为空
{ {
resultJson = loggingMonitor.ReturnInformation.Value.ToJsonNetString(); resultJson = loggingMonitor.ReturnInformation.Value.ToSystemTextJsonString();
} }
} }
@@ -120,8 +118,8 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
Category = LogCateGoryEnum.Operate, Category = LogCateGoryEnum.Operate,
ExeStatus = true, ExeStatus = true,
OpIp = loggingMonitor.RemoteIPv4, OpIp = loggingMonitor.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 = loggingMonitor.LogDateTime.LocalDateTime,
OpAccount = opAccount, OpAccount = opAccount,
ReqMethod = loggingMonitor.HttpMethod, ReqMethod = loggingMonitor.HttpMethod,
@@ -161,16 +159,16 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
/// <param name="operation">访问类型</param> /// <param name="operation">访问类型</param>
/// <param name="path"></param> /// <param name="path"></param>
/// <param name="loggingMonitor">loggingMonitor</param> /// <param name="loggingMonitor">loggingMonitor</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, LoggingMonitorJson loggingMonitor, 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 var result = loggingMonitor.ReturnInformation?.Value?.ToSystemTextJsonString();//返回值转json
var userInfo = result.FromJsonNetString<UnifyResult<LoginOutput>>();//格式化成user表 var userInfo = result.FromJsonNetString<UnifyResult<LoginOutput>>();//格式化成user表
opAccount = userInfo.Data.Account;//赋值账号 opAccount = userInfo.Data.Account;//赋值账号
verificatId = userInfo.Data.VerificatId; verificatId = userInfo.Data.VerificatId;
@@ -188,18 +186,18 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
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 = loggingMonitor.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 = loggingMonitor.LogDateTime.LocalDateTime,
VerificatId = verificatId, VerificatId = verificatId,
OpAccount = opAccount, OpAccount = opAccount,
ReqMethod = loggingMonitor.HttpMethod, ReqMethod = loggingMonitor.HttpMethod,
ReqUrl = path, ReqUrl = path,
ResultJson = loggingMonitor.ReturnInformation?.Value?.ToJsonNetString(), ResultJson = loggingMonitor.ReturnInformation?.Value?.ToSystemTextJsonString(),
ClassName = loggingMonitor.DisplayName, ClassName = loggingMonitor.DisplayName,
MethodName = loggingMonitor.ActionName, MethodName = loggingMonitor.ActionName,
ParamJson = loggingMonitor.Parameters?.ToJsonNetString(), ParamJson = loggingMonitor.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())
var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items;
foreach (var item1 in apiDescriptions)
{ {
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = item ?? "Default" }; foreach (var item in item1.Items)
{
if (item.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = controllerActionDescriptor.ControllerName ?? "Default" };
groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector); groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
} }
var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items; }
}
// 获取所有需要数据权限的控制器 // 获取所有需要数据权限的控制器
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

@@ -13,15 +13,17 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using System.Net;
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?>
@@ -42,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;
} }
} }
@@ -72,7 +72,7 @@ public class AppService : IAppService
} }
public ClaimsPrincipal? User => App.User; public ClaimsPrincipal? User => App.User;
public IPAddress? RemoteIpAddress => App.HttpContext?.Connection?.RemoteIpAddress; public string? RemoteIpAddress => App.HttpContext?.GetRemoteIpAddressToIPv4();
public int LocalPort => App.HttpContext.Connection.LocalPort; public int LocalPort => App.HttpContext.Connection.LocalPort;
} }

View File

@@ -11,22 +11,19 @@
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Net;
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 = IPAddress.Parse("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
@@ -56,7 +53,7 @@ public class HybridAppService : IAppService
} }
} }
public IPAddress? RemoteIpAddress { get; } public string? RemoteIpAddress { get; }
public string GetReturnUrl(string returnUrl) public string GetReturnUrl(string returnUrl)
{ {

View File

@@ -9,11 +9,8 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
using System.Net;
using System.Security.Claims; using System.Security.Claims;
using UAParser;
namespace ThingsGateway.Admin.Application; namespace ThingsGateway.Admin.Application;
public interface IAppService public interface IAppService
@@ -21,7 +18,7 @@ public interface IAppService
/// <summary> /// <summary>
/// ClientInfo /// ClientInfo
/// </summary> /// </summary>
public ClientInfo? ClientInfo { get; } public UserAgent? UserAgent { get; }
/// <summary> /// <summary>
/// ClaimsPrincipal /// ClaimsPrincipal
@@ -31,7 +28,7 @@ public interface IAppService
/// <summary> /// <summary>
/// RemoteIpAddress /// RemoteIpAddress
/// </summary> /// </summary>
public IPAddress? RemoteIpAddress { get; } public string? RemoteIpAddress { get; }
/// <summary> /// <summary>
/// GetReturnUrl /// GetReturnUrl

View File

@@ -96,16 +96,16 @@ 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)
{ {
var loginEvent = new LoginEvent var loginEvent = new LoginEvent
{ {
Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(), Ip = _appService.RemoteIpAddress,
SysUser = userinfo, SysUser = userinfo,
VerificatId = verificatId VerificatId = verificatId
}; };
@@ -236,8 +236,8 @@ public class AuthService : IAuthService
//登录事件参数 //登录事件参数
var logingEvent = new LoginEvent var logingEvent = new LoginEvent
{ {
Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(), 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

@@ -47,12 +47,10 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<bool> DeleteAsync(IEnumerable<T> models) public async Task<bool> DeleteAsync(IEnumerable<T> models)
{ {
if (models.FirstOrDefault() is IPrimaryIdEntity) using var db = GetDB();
return DeleteAsync(models.Select(a => ((IPrimaryIdEntity)a).Id)); return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
else
return Task.FromResult(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -165,4 +163,6 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
{ {
return DbContext.Db.GetConnectionScopeWithAttr<T>().CopyNew(); return DbContext.Db.GetConnectionScopeWithAttr<T>().CopyNew();
} }
} }

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
}; };
} }
@@ -103,4 +105,46 @@ public static class DbContext
Console.WriteLine("【Sql执行时间】" + DateTime.Now.ToDefaultDateTimeFormat()); Console.WriteLine("【Sql执行时间】" + DateTime.Now.ToDefaultDateTimeFormat());
Console.WriteLine("【Sql语句】" + msg + Environment.NewLine); Console.WriteLine("【Sql语句】" + msg + Environment.NewLine);
} }
public static async Task BulkCopyAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
{
switch (db.CurrentConnectionConfig.DbType)
{
case DbType.MySql:
case DbType.SqlServer:
case DbType.Sqlite:
case DbType.Oracle:
case DbType.PostgreSQL:
case DbType.Dm:
case DbType.MySqlConnector:
case DbType.Kdbndp:
await db.Fastest<TITEM>().PageSize(size).BulkCopyAsync(datas).ConfigureAwait(false);
break;
default:
await db.Insertable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
break;
}
}
public static async Task BulkUpdateAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
{
switch (db.CurrentConnectionConfig.DbType)
{
case DbType.MySql:
case DbType.SqlServer:
case DbType.Sqlite:
case DbType.Oracle:
case DbType.PostgreSQL:
case DbType.Dm:
case DbType.MySqlConnector:
case DbType.Kdbndp:
await db.Fastest<TITEM>().PageSize(size).BulkUpdateAsync(datas).ConfigureAwait(false);
break;
default:
await db.Updateable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
break;
}
}
} }

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

@@ -10,6 +10,7 @@
using BootstrapBlazor.Components; using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SqlSugar; using SqlSugar;
@@ -23,7 +24,7 @@ namespace ThingsGateway.Admin.Application;
[AppStartup(1000000000)] [AppStartup(1000000000)]
public class Startup : AppStartup public class Startup : AppStartup
{ {
public void ConfigureAdminApp(IServiceCollection services) public void Configure(IServiceCollection services)
{ {
Directory.CreateDirectory("DB"); Directory.CreateDirectory("DB");
@@ -35,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;
@@ -75,7 +77,7 @@ public class Startup : AppStartup
} }
public void UseAdminCore(IServiceProvider serviceProvider) public void Use(IApplicationBuilder applicationBuilder)
{ {
//检查ConfigId //检查ConfigId
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId); var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
@@ -88,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,11 +18,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.2" /> <PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
<!--<PackageReference Include="MiniExcel" Version="1.39.0" />-->
<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.188" /> <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" />
@@ -31,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

@@ -15,7 +15,7 @@ namespace ThingsGateway.Admin.Razor;
[AppStartup(-1)] [AppStartup(-1)]
public class Startup : AppStartup public class Startup : AppStartup
{ {
public void ConfigureAdminApp(IServiceCollection services) public void Configure(IServiceCollection services)
{ {
services.AddScoped<IMenuService, MenuService>(); services.AddScoped<IMenuService, MenuService>();
services.AddBootstrapBlazorTableExportService(); services.AddBootstrapBlazorTableExportService();

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,6 +10,9 @@
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.Mvc.Controllers;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
@@ -18,6 +21,7 @@ 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;
@@ -216,7 +220,7 @@ public class Startup : AppStartup
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特性
@@ -291,6 +295,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
});
} }
@@ -298,9 +317,9 @@ public class Startup : AppStartup
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env) public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
{ {
var app = (WebApplication)applicationBuilder; var app = (WebApplication)applicationBuilder;
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
app.UseBootstrapBlazor(); app.UseBootstrapBlazor();
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
// 启用本地化 // 启用本地化
var option = app.Services.GetService<IOptions<RequestLocalizationOptions>>(); var option = app.Services.GetService<IOptions<RequestLocalizationOptions>>();

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

@@ -106,7 +106,7 @@ public static class HttpContextExtensions
/// <param name="context"></param> /// <param name="context"></param>
/// <param name="xff">是否优先取 X-Forwarded-For</param> /// <param name="xff">是否优先取 X-Forwarded-For</param>
/// <returns></returns> /// <returns></returns>
public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = false) public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = true)
{ {
var ipv4 = context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString(); var ipv4 = context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();

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);
aesAlg.IV = iv;
fullCipher = cipher; 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.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock(); cryptoStream.FlushFinalBlock();
}
// 如果是 CBC 模式,将 IV 和密文拼接在一起 var encryptedContent = memoryStream.ToArray();
if (mode != CipherMode.ECB)
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{ {
var result = new byte[aesAlg.IV.Length + memoryStream.ToArray().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);
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

@@ -118,7 +118,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

@@ -116,7 +116,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

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

@@ -147,7 +147,7 @@ internal sealed class ScheduleHostedService : BackgroundService
await BackgroundProcessing(stoppingToken).ConfigureAwait(false); await BackgroundProcessing(stoppingToken).ConfigureAwait(false);
} }
_logger.LogCritical($"Schedule hosted service is stopped."); _logger.LogInformation($"Schedule hosted service is stopped.");
} }
/// <summary> /// <summary>
@@ -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;
@@ -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),
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 ?? ServiceProvider?.GetRequiredService<IOptions<HttpRemoteOptions>>().Value.JsonSerializerOptions ??
HttpRemoteOptions.JsonSerializerOptionsDefault, cancellationToken).ConfigureAwait(false); 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);
} }
} }
} }

View File

@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.HttpRemote;
/// <summary>
/// HTTP 声明式 <see cref="HttpVersionAttribute" /> 特性提取器
/// </summary>
internal sealed class HttpVersionDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 检查方法或接口是否贴有 [HttpVersion] 特性
if (!context.IsMethodDefined<HttpVersionAttribute>(out var versionAttribute, true))
{
return;
}
// 设置 HTTP 版本
httpRequestBuilder.SetVersion(versionAttribute.Version);
}
}

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