Compare commits
112 Commits
10.10.15.0
...
00c24d06a3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
00c24d06a3 | ||
![]() |
3461f34240 | ||
![]() |
aa1ce08c02 | ||
![]() |
9c230c2da9 | ||
![]() |
21215d0379 | ||
![]() |
7448183791 | ||
![]() |
35edd7dc43 | ||
![]() |
bd178831e3 | ||
![]() |
fe9ec6ad10 | ||
![]() |
6f9ec2e24b | ||
![]() |
c0337e2b19 | ||
![]() |
8a95f48f5a | ||
![]() |
14f3c31265 | ||
![]() |
1bad65378f | ||
![]() |
db3affc67e | ||
![]() |
5ee8b50a92 | ||
![]() |
301beda2a2 | ||
![]() |
628b51a353 | ||
![]() |
f03445bc83 | ||
![]() |
55a2ff5487 | ||
![]() |
0fef7dcf3b | ||
![]() |
19d9702606 | ||
![]() |
a8a9774932 | ||
![]() |
aad0f0e8c3 | ||
![]() |
e74eae50a7 | ||
![]() |
3b16d7019f | ||
![]() |
3e038028c2 | ||
![]() |
b1d8041f7e | ||
![]() |
53a98b26cd | ||
![]() |
42c740fa1b | ||
![]() |
556819c90c | ||
![]() |
2522333a9c | ||
![]() |
bd4ce7c09b | ||
![]() |
156ed88bd6 | ||
![]() |
2416226eb0 | ||
![]() |
976323a716 | ||
![]() |
3c9e397403 | ||
![]() |
79406ad4a0 | ||
![]() |
20c44f10ca | ||
![]() |
31d6b2a9e6 | ||
![]() |
68e5a9c546 | ||
![]() |
b2ea9f99b9 | ||
![]() |
6d7d0e468a | ||
![]() |
ff1f632de2 | ||
![]() |
fc3d7015ee | ||
![]() |
40c5acb522 | ||
![]() |
f9cc1cbb05 | ||
![]() |
cf6e8b58f0 | ||
![]() |
615e3bb24c | ||
![]() |
4a7534b210 | ||
![]() |
58e099cb93 | ||
![]() |
a94a9c953c | ||
![]() |
35e1ffa3e9 | ||
![]() |
4921642151 | ||
![]() |
d71ee29da8 | ||
![]() |
901aa2d59f | ||
![]() |
764957c014 | ||
![]() |
0e3898218b | ||
![]() |
61f13cef3c | ||
![]() |
0b663d9e01 | ||
![]() |
6c95c6209f | ||
![]() |
4d223d2622 | ||
![]() |
e8d7e91b64 | ||
![]() |
8175f541ec | ||
![]() |
0adbdb926b | ||
![]() |
42adee9980 | ||
![]() |
427a7404bc | ||
![]() |
3658199e0a | ||
![]() |
82eedee50a | ||
![]() |
6a18fc3e06 | ||
![]() |
c37e314ed6 | ||
![]() |
a937a85d90 | ||
![]() |
35dd4ae9d3 | ||
![]() |
0b829ac85c | ||
![]() |
aa247422d2 | ||
![]() |
2e00e8c135 | ||
![]() |
34dd2cf0a7 | ||
![]() |
8404e20c5e | ||
![]() |
662aa162e9 | ||
![]() |
5927738c32 | ||
![]() |
3c9f97a5c3 | ||
![]() |
179ca0aa0e | ||
![]() |
fc09a52da1 | ||
![]() |
5436b91c89 | ||
![]() |
d1c46f51a6 | ||
![]() |
4539d8d198 | ||
![]() |
9ea9529a5f | ||
![]() |
4e6be23aac | ||
![]() |
2fabbd236b | ||
![]() |
163a66530e | ||
![]() |
29073a00c4 | ||
![]() |
c6d4d1ecfa | ||
![]() |
ba16889cad | ||
![]() |
5aaed35b0f | ||
![]() |
df067c91eb | ||
![]() |
2078b4a60b | ||
![]() |
20a2e3ff8e | ||
![]() |
61a973b1b5 | ||
![]() |
cbd72e2081 | ||
![]() |
4e0377b20c | ||
![]() |
fd318d3cdc | ||
![]() |
515bdb9700 | ||
![]() |
46c1780017 | ||
![]() |
fe78a4c3ca | ||
![]() |
2d7effadf9 | ||
![]() |
346c560f8b | ||
![]() |
8e3bd89f61 | ||
![]() |
6da142d080 | ||
![]() |
ff7d029e6f | ||
![]() |
21b4695683 | ||
![]() |
02ad494a26 | ||
![]() |
280366e1b2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -364,7 +364,5 @@ FodyWeavers.xsd
|
||||
|
||||
/src/*Pro*/
|
||||
/src/*Pro*
|
||||
/src/*pro*
|
||||
/src/*pro*/
|
||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
|
||||
/src/.idea/
|
||||
|
@@ -11,6 +11,7 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<!-- 避免 DLL 被打包到 lib/ -->
|
||||
<EnableSourceGenerator>true</EnableSourceGenerator>
|
||||
|
||||
<!-- 可选 -->
|
||||
|
||||
|
||||
@@ -26,6 +27,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" Private="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -37,9 +37,8 @@ public class FileController : ControllerBase
|
||||
var root = Directory.GetCurrentDirectory();
|
||||
var wwwroot = Path.Combine(root, "wwwroot");
|
||||
var filePath = Path.Combine(wwwroot, fileName);
|
||||
// 防止路径穿越攻击
|
||||
#pragma warning disable CA3003
|
||||
if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
|
||||
if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
@@ -49,6 +48,6 @@ public class FileController : ControllerBase
|
||||
|
||||
Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
|
||||
|
||||
return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
|
||||
return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,7 @@ public class VerificatInfo : PrimaryIdEntity
|
||||
/// 登录IP
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 200)]
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string LoginIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -78,5 +79,6 @@ public class VerificatInfo : PrimaryIdEntity
|
||||
/// 登录设备
|
||||
/// </summary>
|
||||
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 100)]
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public string Device { get; set; }
|
||||
}
|
||||
|
@@ -251,11 +251,13 @@ public class RequestAuditFilter : IAsyncActionFilter, IOrderedFilter
|
||||
|
||||
if (exception == null)
|
||||
{
|
||||
logger.Log(LogLevel.Information, $"{logData.Method}:{logData.Path}-{logData.Operation}");
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
logger.Log(LogLevel.Information, $"{logData.Method}:{logData.Path}-{logData.Operation}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception?.ToSystemTextJsonString()}{Environment.NewLine}{logData.Validation?.ToSystemTextJsonString()}");
|
||||
if (logger.IsEnabled(LogLevel.Warning))
|
||||
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception?.ToSystemTextJsonString()}{Environment.NewLine}{logData.Validation?.ToSystemTextJsonString()}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "This account has not been assigned a module. Please contact the administrator",
|
||||
"UserNull": "User {0} does not exist"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "User expired, please login again"
|
||||
},
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "该账号未分配模块,请联系管理员",
|
||||
"UserNull": "用户 {0} 不存在"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "用户登录已过期,请重新登录"
|
||||
},
|
||||
|
@@ -11,6 +11,7 @@
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
|
||||
public static partial class AdminMapper
|
||||
{
|
||||
|
@@ -145,7 +145,7 @@ public class AdminOAuthHandler<TOptions>(
|
||||
var loginEvent = new LoginEvent
|
||||
{
|
||||
Ip = appService.RemoteIpAddress,
|
||||
Device = appService.UserAgent?.Platform,
|
||||
Device = appService.UserAgent?.Platform ?? "Unknown",
|
||||
Expire = expire,
|
||||
SysUser = sysUser,
|
||||
VerificatId = CommonUtils.GetSingleId()
|
||||
@@ -156,7 +156,7 @@ public class AdminOAuthHandler<TOptions>(
|
||||
//生成verificat信息
|
||||
var verificatInfo = new VerificatInfo
|
||||
{
|
||||
Device = loginEvent.Device,
|
||||
Device = loginEvent.Device ?? "Unknown",
|
||||
Expire = loginEvent.Expire,
|
||||
VerificatTimeout = tokenTimeout,
|
||||
Id = loginEvent.VerificatId,
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"Module": 2,
|
||||
"Title": "权限管理",
|
||||
"Code": "System",
|
||||
"NavLinkMatch": "All",
|
||||
"NavLinkMatch": "Prefix",
|
||||
"Category": "MENU",
|
||||
"Target": "_self",
|
||||
"Href": null,
|
||||
@@ -47,7 +47,7 @@
|
||||
"ParentId": 0,
|
||||
"Module": 2,
|
||||
"Title": "系统运维",
|
||||
"NavLinkMatch": "All",
|
||||
"NavLinkMatch": "Prefix",
|
||||
"Code": "System",
|
||||
"Category": "MENU",
|
||||
"Target": "_self",
|
||||
|
@@ -235,7 +235,7 @@ public class AuthService : IAuthService
|
||||
var logingEvent = new LoginEvent
|
||||
{
|
||||
Ip = _appService.RemoteIpAddress,
|
||||
Device = _appService.UserAgent?.Platform,
|
||||
Device = _appService.UserAgent?.Platform ?? "Unknown",
|
||||
Expire = expire,
|
||||
SysUser = sysUser,
|
||||
VerificatId = verificatId
|
||||
@@ -344,7 +344,7 @@ public class AuthService : IAuthService
|
||||
//生成verificat信息
|
||||
var verificatInfo = new VerificatInfo
|
||||
{
|
||||
Device = loginEvent.Device,
|
||||
Device = loginEvent.Device ?? "Unknown",
|
||||
Expire = loginEvent.Expire,
|
||||
VerificatTimeout = tokenTimeout,
|
||||
Id = loginEvent.VerificatId,
|
||||
|
@@ -20,7 +20,7 @@ namespace ThingsGateway.Admin.Application;
|
||||
/// <typeparam name="TEntry"></typeparam>
|
||||
public class EventService<TEntry> : IEventService<TEntry>, IDisposable
|
||||
{
|
||||
private ConcurrentDictionary<string, Func<TEntry, Task>> Cache = new();
|
||||
private NonBlockingDictionary<string, Func<TEntry, Task>> Cache = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
@@ -9,6 +9,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
internal sealed class NoticeService : INoticeService
|
||||
{
|
||||
private IEventService<AppMessage>? MessageDispatchService { get; set; }
|
||||
|
@@ -282,7 +282,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
|
||||
if (sysRole != null)
|
||||
{
|
||||
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
|
||||
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
|
||||
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
|
||||
|
||||
#region 角色模块处理
|
||||
|
||||
|
@@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
|
||||
/// 获取用户拥有的资源
|
||||
/// </summary>
|
||||
/// <param name="id">用户id</param>
|
||||
public async Task<GrantResourceData> OwnResourceAsync(long id)
|
||||
public Task<GrantResourceData> OwnResourceAsync(long id)
|
||||
{
|
||||
return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false);
|
||||
return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
|
||||
var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下
|
||||
using var db = GetDB();
|
||||
//重置密码
|
||||
if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
{
|
||||
Password = password
|
||||
}, it => it.Id == id).ConfigureAwait(false))
|
||||
}, it => it.Id == id).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
DeleteUserFromCache(id);//从cache删除用户信息
|
||||
var verificatInfoIds = _verificatInfoService.GetListByUserId(id);
|
||||
@@ -550,7 +550,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
|
||||
if (sysUser != null)
|
||||
{
|
||||
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
|
||||
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
|
||||
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
|
||||
|
||||
#region 用户模块处理
|
||||
|
||||
|
@@ -185,12 +185,12 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
|
||||
using var db = GetDB();
|
||||
|
||||
//更新指定字段
|
||||
var result = await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
var result = (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
{
|
||||
Email = input.Email,
|
||||
Phone = input.Phone,
|
||||
Avatar = input.Avatar,
|
||||
}, it => it.Id == UserManager.UserId).ConfigureAwait(false);
|
||||
}, it => it.Id == UserManager.UserId).ConfigureAwait(false)) > 0;
|
||||
if (result)
|
||||
_userService.DeleteUserFromCache(UserManager.UserId);//cache删除用户数据
|
||||
}
|
||||
|
@@ -119,7 +119,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
|
||||
public void Add(VerificatInfo verificatInfo)
|
||||
{
|
||||
using var db = GetDB();
|
||||
db.Insertable<VerificatInfo>(verificatInfo).ExecuteCommand();
|
||||
db.InsertableT<VerificatInfo>(verificatInfo).ExecuteCommand();
|
||||
VerificatInfoService.RemoveCache(verificatInfo.Id);
|
||||
if (verificatInfo != null)
|
||||
VerificatInfoService.SetCahce(verificatInfo);
|
||||
@@ -132,7 +132,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
|
||||
public void Update(VerificatInfo verificatInfo)
|
||||
{
|
||||
using var db = GetDB();
|
||||
db.Updateable<VerificatInfo>(verificatInfo).ExecuteCommand();
|
||||
db.UpdateableT<VerificatInfo>(verificatInfo).ExecuteCommand();
|
||||
VerificatInfoService.RemoveCache(verificatInfo.Id);
|
||||
if (verificatInfo != null)
|
||||
VerificatInfoService.SetCahce(verificatInfo);
|
||||
|
@@ -5,9 +5,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
|
||||
<PackageReference Include="Rougamo.Fody" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
@@ -27,10 +28,10 @@
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" />
|
||||
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET9Version)" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="$(NET9Version)" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET9Version)" />
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET10Version)" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="$(NET10Version)" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET10Version)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Remove="SeedData\Admin\*.json" />
|
||||
|
@@ -4,9 +4,9 @@
|
||||
|
||||
<div class="tg-table h-100">
|
||||
|
||||
<Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
|
||||
DataService="DataService" CreateItemCallback="CreateItemCallback!"
|
||||
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" BeforeShowEditDialogCallback="BeforeShowEditDialogCallback!"
|
||||
<Table Id=@Id TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
|
||||
DataService="DataService" CreateItemCallback="CreateItemCallback!" RenderMode=RenderMode OnColumnCreating=OnColumnCreating
|
||||
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" DisableEditButtonCallback="DisableEditButtonCallback" DisableDeleteButtonCallback="DisableDeleteButtonCallback" BeforeShowEditDialogCallback=" BeforeShowEditDialogCallback!"
|
||||
IsTree="IsTree" OnTreeExpand="OnTreeExpand!" TreeNodeConverter="TreeNodeConverter!" TreeIcon="fa-solid fa-circle-chevron-right" TreeExpandIcon="fa-solid fa-circle-chevron-right fa-rotate-90" IsAutoQueryFirstRender=IsAutoQueryFirstRender
|
||||
ShowDefaultButtons="ShowDefaultButtons" ShowAdvancedSearch="ShowAdvancedSearch" ShowResetButton=ShowResetButton
|
||||
ShowEmpty="ShowEmpty" EmptyText="@EmptyText" EmptyImage="@($"{WebsiteConst.DefaultResourceUrl}images/empty.svg")" SortString="@SortString" EditDialogSize="EditDialogSize"
|
||||
@@ -14,7 +14,7 @@
|
||||
ShowSkeleton="true" ShowLoading="ShowLoading" ShowSearch="ShowSearch" SearchModel=@SearchModel ShowLineNo
|
||||
SearchMode=SearchMode ShowExportPdfButton=ShowExportPdfButton ExportButtonText=@ExportButtonText
|
||||
ShowExportButton=@ShowExportButton Items=Items ClickToSelect=ClickToSelect ScrollMode=ScrollMode
|
||||
ShowExportCsvButton=@ShowExportCsvButton SelectedRowsChanged=SelectedRowsChanged ShowCardView=ShowCardView
|
||||
ShowExportCsvButton=@ShowExportCsvButton ShowCardView=ShowCardView OnColumnVisibleChanged=OnColumnVisibleChanged
|
||||
FixedExtendButtonsColumn=FixedExtendButtonsColumn FixedMultipleColumn=FixedMultipleColumn FixedDetailRowHeaderColumn=FixedDetailRowHeaderColumn FixedLineNoColumn=FixedLineNoColumn
|
||||
IsAutoRefresh=IsAutoRefresh AutoRefreshInterval=AutoRefreshInterval
|
||||
AllowDragColumn=@AllowDragColumn Height=@Height ShowRefresh=ShowRefresh
|
||||
@@ -29,7 +29,7 @@
|
||||
ShowMultiFilterHeader=ShowMultiFilterHeader
|
||||
ShowFilterHeader=ShowFilterHeader
|
||||
ShowColumnList=ShowColumnList ExtendButtonColumnWidth="@ExtendButtonColumnWidth"
|
||||
CustomerSearchModel="CustomerSearchModel" SelectedRows="SelectedRows" ModelEqualityComparer="ModelEqualityComparer!"
|
||||
CustomerSearchModel="CustomerSearchModel" ModelEqualityComparer="ModelEqualityComparer!"
|
||||
ShowExtendEditButtonCallback="ShowExtendEditButtonCallback!" ShowExtendDeleteButtonCallback="ShowExtendDeleteButtonCallback!"
|
||||
DisableExtendEditButton="DisableExtendEditButton!" DisableExtendDeleteButton="DisableExtendDeleteButton!"
|
||||
DisableExtendEditButtonCallback="DisableExtendEditButtonCallback!" DisableExtendDeleteButtonCallback="DisableExtendDeleteButtonCallback!"
|
||||
@@ -41,6 +41,7 @@
|
||||
DoubleClickToEdit="DoubleClickToEdit"
|
||||
OnDoubleClickCellCallback="OnDoubleClickCellCallback"
|
||||
OnDoubleClickRowCallback="OnDoubleClickRowCallback"
|
||||
RowContentTemplate="RowContentTemplate"
|
||||
OnClickRowCallback="OnClickRowCallback">
|
||||
</Table>
|
||||
</div>
|
||||
|
@@ -13,6 +13,41 @@ namespace ThingsGateway.Admin.Razor;
|
||||
[CascadingTypeParameter(nameof(TItem))]
|
||||
public partial class AdminTable<TItem> where TItem : class, new()
|
||||
{
|
||||
/// <inheritdoc cref="Table{TItem}.OnColumnVisibleChanged"/>
|
||||
[Parameter]
|
||||
public Func<string,bool, Task> OnColumnVisibleChanged { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.OnColumnCreating"/>
|
||||
[Parameter]
|
||||
public Func<List<ITableColumn>,Task> OnColumnCreating { get; set; }
|
||||
/// <inheritdoc cref="Table{TItem}.RenderMode"/>
|
||||
[Parameter]
|
||||
public TableRenderMode RenderMode { get; set; }
|
||||
|
||||
public List<ITableColumn> Columns => Instance?.Columns;
|
||||
|
||||
public IEnumerable<ITableColumn> GetVisibleColumns => Instance?.GetVisibleColumns();
|
||||
public List<TItem> Rows => Instance?.Rows;
|
||||
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
|
||||
[Parameter]
|
||||
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
|
||||
[Parameter]
|
||||
public List<TItem> SelectedRows { get; set; } = new();
|
||||
|
||||
private async Task privateSelectedRowsChanged(List<TItem> items)
|
||||
{
|
||||
SelectedRows = items;
|
||||
if (SelectedRowsChanged.HasDelegate)
|
||||
await SelectedRowsChanged.InvokeAsync(items);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.DoubleClickToEdit"/>
|
||||
[Parameter]
|
||||
public bool DoubleClickToEdit { get; set; } = false;
|
||||
@@ -22,6 +57,10 @@ public partial class AdminTable<TItem> where TItem : class, new()
|
||||
/// <inheritdoc cref="Table{TItem}.OnDoubleClickRowCallback"/>
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? OnDoubleClickRowCallback { get; set; }
|
||||
/// <inheritdoc cref="Table{TItem}.RowContentTemplate"/>
|
||||
[Parameter]
|
||||
public RenderFragment<TableRowContext<TItem>>? RowContentTemplate { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.OnClickRowCallback"/>
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? OnClickRowCallback { get; set; }
|
||||
@@ -128,6 +167,9 @@ public partial class AdminTable<TItem> where TItem : class, new()
|
||||
[Parameter]
|
||||
public IDataService<TItem> DataService { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.CreateItemCallback"/>
|
||||
[Parameter]
|
||||
public Func<TItem> CreateItemCallback { get; set; }
|
||||
@@ -210,14 +252,6 @@ public partial class AdminTable<TItem> where TItem : class, new()
|
||||
[Parameter]
|
||||
public RenderFragment<TItem>? SearchTemplate { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
|
||||
[Parameter]
|
||||
public List<TItem>? SelectedRows { get; set; } = new List<TItem>();
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
|
||||
[Parameter]
|
||||
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.SetRowClassFormatter"/>
|
||||
[Parameter]
|
||||
public Func<TItem, string?>? SetRowClassFormatter { get; set; }
|
||||
@@ -266,6 +300,15 @@ public partial class AdminTable<TItem> where TItem : class, new()
|
||||
[Parameter]
|
||||
public bool ShowExportButton { get; set; } = false;
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.DisableEditButtonCallback"/>
|
||||
public Func<List<TItem>, bool> DisableEditButtonCallback { get; set; } = (list) =>
|
||||
list.Count != 1;
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.DisableDeleteButtonCallback"/>
|
||||
[Parameter]
|
||||
public Func<List<TItem>, bool> DisableDeleteButtonCallback { get; set; } = (list) =>
|
||||
list.Count <= 0;
|
||||
|
||||
/// <inheritdoc cref="Table{TItem}.ShowExportCsvButton"/>
|
||||
[Parameter]
|
||||
public bool ShowExportCsvButton { get; set; } = false;
|
||||
|
@@ -30,7 +30,7 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 全部菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> AllMenus { get; private set; }
|
||||
public List<SysResource> AllMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户
|
||||
@@ -42,22 +42,22 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 不同模块的菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
public List<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,多个模块
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> OwnMenus { get; private set; }
|
||||
public List<SysResource> OwnMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,非树形
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个人工作台
|
||||
@@ -67,9 +67,9 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人快捷方式菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
public List<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
|
||||
public IEnumerable<SysResource> AllResource { get; private set; }
|
||||
public List<SysResource> AllResource { get; private set; }
|
||||
|
||||
private ISysResourceService ResourceService { get; }
|
||||
private ISysUserService SysUserService { get; }
|
||||
@@ -93,7 +93,7 @@ public class BlazorAppContext
|
||||
AllResource = sysResources;
|
||||
var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
|
||||
CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
|
||||
|
||||
if (moduleId == null)
|
||||
{
|
||||
@@ -123,17 +123,26 @@ public class BlazorAppContext
|
||||
}
|
||||
}
|
||||
var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
|
||||
OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
|
||||
OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item =>
|
||||
{
|
||||
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
|
||||
Text = item.Title,
|
||||
Icon = item.Icon,
|
||||
Url = item.Href,
|
||||
Target = item.Target.ToString(),
|
||||
});
|
||||
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
|
||||
var menu = new MenuItem()
|
||||
{
|
||||
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
|
||||
Text = item.Title,
|
||||
Icon = item.Icon,
|
||||
Url = item.Href,
|
||||
Target = item.Target.ToString(),
|
||||
};
|
||||
if (menu.Url.IsNullOrEmpty())
|
||||
{
|
||||
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
|
||||
}
|
||||
return menu;
|
||||
|
||||
}).ToList();
|
||||
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ public partial class EditPagePolicy
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
ShortcutsSearchText = searchText;
|
||||
return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
}
|
||||
}
|
||||
|
@@ -72,7 +72,7 @@ public partial class HardwareInfoPage : IDisposable
|
||||
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.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd-HH:mm"));
|
||||
ChartDataSource.Data.Add(new ChartDataset()
|
||||
{
|
||||
Tension = 0.4f,
|
||||
@@ -116,7 +116,7 @@ public partial class HardwareInfoPage : IDisposable
|
||||
else
|
||||
{
|
||||
var hisHardwareInfos = await HardwareJob.GetHistoryHardwareInfos();
|
||||
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd HH:mm zz"));
|
||||
ChartDataSource.Labels = hisHardwareInfos.Select(a => a.Date.ToString("dd-HH:mm"));
|
||||
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);
|
||||
|
@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
|
||||
var all = (await SysResourceService.GetAllAsync());
|
||||
var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
|
||||
ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
|
||||
Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
|
@@ -26,12 +26,12 @@
|
||||
OnQueryAsync="OnQueryAsync" CustomerSearchModel="@CustomerSearchModel"
|
||||
OnSaveAsync="Save" OnDeleteAsync="Delete">
|
||||
<TableToolbarTemplate>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count<=0||!AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count <= 0 || !AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<BodyTemplate>
|
||||
<Select Items="ModuleSelectedItems" @bind-Value=CopyModule ShowLabel="false" />
|
||||
</BodyTemplate>
|
||||
</PopConfirmButton>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count != 1 || !AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<BodyTemplate>
|
||||
<div class="overflow-y-auto" style="height:500px">
|
||||
<TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />
|
||||
|
@@ -39,8 +39,8 @@ public partial class SysResourcePage
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public partial class SysResourcePage
|
||||
|
||||
private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
|
||||
{
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
|
||||
var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
|
||||
return data;
|
||||
@@ -136,14 +136,14 @@ public partial class SysResourcePage
|
||||
private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
|
||||
{
|
||||
var sysResources = await SysResourceService.GetAllAsync();
|
||||
var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
var result = ResourceUtil.BuildTableTrees(items, 0);
|
||||
var result = AdminResourceUtil.BuildTableTrees(items, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
|
||||
{
|
||||
var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
|
||||
|
||||
Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ public partial class SysUserEdit
|
||||
BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
|
||||
var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
|
||||
Items = PositionUtil.BuildCascaderItemList(items);
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-sm-10 h-100">
|
||||
<div class="col-12 col-sm-10 h-100 ps-2">
|
||||
<AdminTable @ref=table TItem="SysUser"
|
||||
AutoGenerateColumns="true"
|
||||
ShowAdvancedSearch=false
|
||||
|
@@ -20,5 +20,6 @@ public class Startup : AppStartup
|
||||
services.AddScoped<IMenuService, MenuService>();
|
||||
services.AddScoped<IAuthRazorService, AuthRazorService>();
|
||||
services.AddBootstrapBlazorTableExportService();
|
||||
services.AddBootstrapBlazorWinBoxService();
|
||||
}
|
||||
}
|
||||
|
@@ -5,19 +5,22 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
|
||||
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
|
||||
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.4" />
|
||||
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
|
||||
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.7" />
|
||||
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET8Version)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET9Version)" />
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net10.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET10Version)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
|
||||
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
|
||||
public static class ResourceUtil
|
||||
public static class AdminResourceUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造选择项,ID/TITLE
|
||||
@@ -41,15 +41,22 @@ public static class ResourceUtil
|
||||
return items
|
||||
.Where(it => it.ParentId == parentId)
|
||||
.Select((item, index) =>
|
||||
new MenuItem()
|
||||
{
|
||||
var menu = new MenuItem()
|
||||
{
|
||||
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
|
||||
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
|
||||
Text = item.Title,
|
||||
Icon = item.Icon,
|
||||
Url = item.Href,
|
||||
Target = item.Target.ToString(),
|
||||
Items = BuildMenuTrees(items, item.Id).ToList()
|
||||
};
|
||||
if (menu.Url.IsNullOrEmpty())
|
||||
{
|
||||
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<Target Name="AdminPostPublish" AfterTargets="Publish">
|
||||
<ItemGroup>
|
||||
<!-- setting up the variable for convenience -->
|
||||
<AdminFiles Include="bin\$(Configuration)\$(TargetFramework)\SeedData\**" />
|
||||
<AdminFiles Include="$(OutputPath)\$(TargetFramework)\SeedData\**" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
</PropertyGroup>
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -21,11 +21,13 @@
|
||||
<link rel="apple-touch-icon" href="favicon.png">
|
||||
<base href="/" />
|
||||
<title>ThingsGateway</title>
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css?v={this.GetType().Assembly.GetName().Version}") />
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css?v={this.GetType().Assembly.GetName().Version}") />
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css?v={this.GetType().Assembly.GetName().Version}") />
|
||||
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css?v={this.GetType().Assembly.GetName().Version}") />
|
||||
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css?v={this.GetType().Assembly.GetName().Version}") />
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css") />
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css") />
|
||||
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css") />
|
||||
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css") />
|
||||
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css") />
|
||||
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/devui.css") />
|
||||
|
||||
@* <script src=@($"{WebsiteConst.DefaultResourceUrl}js/theme.js") type="module"></script><!-- 初始主题 --> *@
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
@@ -38,12 +40,13 @@
|
||||
|
||||
<BlazorReconnector @rendermode="new InteractiveServerRenderMode(false)" />
|
||||
|
||||
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js?v={this.GetType().Assembly.GetName().Version}")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></script>
|
||||
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<!-- PWA Service Worker -->
|
||||
<script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
|
||||
|
||||
<script src="pwa-install.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@@ -70,7 +70,7 @@
|
||||
<Button @onclick="ShowAbout" class="layout-header-bar d-none d-lg-flex px-2" Icon="fa fa-info" Color="Color.None" TooltipText="@Localizer[nameof(About)]" />
|
||||
}
|
||||
@* 版本号 *@
|
||||
<div class="px-1 navbar-header-text d-none d-lg-block">@_versionString</div>
|
||||
<div class="px-1 navbar-header-text text-nowrap d-none d-lg-block">@_versionString</div>
|
||||
|
||||
@* 主题切换 *@
|
||||
@* <ThemeToggle /> *@
|
||||
@@ -89,12 +89,19 @@
|
||||
</div>
|
||||
</Side>
|
||||
<Main>
|
||||
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowContextMenuFullScreen="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
|
||||
AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
|
||||
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
|
||||
{
|
||||
return Task.FromResult(!(a.Url=="/"||a.Url.IsNullOrEmpty()));
|
||||
})>
|
||||
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
|
||||
ShowFullscreenToolbarButton=false ShowContextMenuFullScreen=false ShowFullScreen=false AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
|
||||
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
|
||||
{
|
||||
return Task.FromResult(!(a.Url == "/" || a.Url.IsNullOrEmpty()));
|
||||
})
|
||||
>
|
||||
<BeforeContextMenuTemplate>
|
||||
|
||||
<ContextMenuItem Icon="fa fa-window-restore" Text="@Localizer["WindowRestore"]" OnClick="WinboxRender"></ContextMenuItem>
|
||||
<ContextMenuDivider></ContextMenuDivider>
|
||||
|
||||
</BeforeContextMenuTemplate>
|
||||
</Tab>
|
||||
</Main>
|
||||
<NotAuthorized>
|
||||
|
@@ -120,6 +120,38 @@ public partial class MainLayout : IDisposable
|
||||
|
||||
#endregion 注销
|
||||
|
||||
|
||||
private async Task WinboxRender(ContextMenuItem item, object? context)
|
||||
{
|
||||
if (context is TabItem tabItem)
|
||||
{
|
||||
await WinboxRender(tabItem.ChildContent, tabItem.Text);
|
||||
//await _tab.RemoveTab(tabItem);
|
||||
}
|
||||
}
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private WinBoxService? WinBoxService { get; set; }
|
||||
|
||||
private async Task WinboxRender(RenderFragment item, string title)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
var option = new WinBoxOption()
|
||||
{
|
||||
Title = title,
|
||||
ContentTemplate = item,
|
||||
Max = false,
|
||||
Width = "80%",
|
||||
Height = "80%",
|
||||
Top = "0%",
|
||||
Left = "10%",
|
||||
Background = "var(--bb-primary-color)",
|
||||
Overflow = true
|
||||
};
|
||||
await WinBoxService.Show(option);
|
||||
}
|
||||
}
|
||||
private string _versionString = string.Empty;
|
||||
[Inject]
|
||||
[NotNull]
|
||||
|
@@ -15,6 +15,7 @@ using System.Text;
|
||||
|
||||
using ThingsGateway.Admin.Application;
|
||||
using ThingsGateway.DB;
|
||||
using ThingsGateway.NewLife;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.AdminServer;
|
||||
@@ -64,7 +65,7 @@ public class Program
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
builder.Host.UseSystemd();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
if (Runtime.IsLegacyWindows)
|
||||
builder.Logging.ClearProviders(); //去除默认的事件日志提供者,某些情况下会日志输出异常,导致程序崩溃
|
||||
}).ConfigureBuilder(builder =>
|
||||
{
|
||||
|
@@ -132,7 +132,11 @@ public class Startup : AppStartup
|
||||
services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||
#if NET10_0_OR_GREATER
|
||||
options.KnownIPNetworks.Clear();
|
||||
#else
|
||||
options.KnownNetworks.Clear();
|
||||
#endif
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
@@ -183,25 +187,37 @@ public class Startup : AppStartup
|
||||
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
|
||||
|
||||
if (!NewLife.Runtime.IsLegacyWindows)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
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
|
||||
});
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
|
||||
{
|
||||
var app = (WebApplication)applicationBuilder;
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.All,
|
||||
#if NET10_0_OR_GREATER
|
||||
KnownIPNetworks = { },
|
||||
#else
|
||||
KnownNetworks = { },
|
||||
#endif
|
||||
KnownProxies = { }
|
||||
});
|
||||
app.UseBootstrapBlazor();
|
||||
|
||||
// 启用本地化
|
||||
|
@@ -4,7 +4,8 @@
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
|
||||
@@ -52,9 +53,9 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET9Version)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET9Version)" />
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET10Version)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET10Version)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
34
src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
Normal file
34
src/Admin/ThingsGateway.AdminServer/wwwroot/pwa-install.js
Normal file
@@ -0,0 +1,34 @@
|
||||
let installPromptTriggered = false;
|
||||
|
||||
function getCookie(name) {
|
||||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? match[2] : null;
|
||||
}
|
||||
|
||||
function hasShownInstallPrompt() {
|
||||
return getCookie("tgPWAInstallPromptShown") === "true";
|
||||
}
|
||||
|
||||
function markInstallPromptShown() {
|
||||
document.cookie = "tgPWAInstallPromptShown=true; max-age=31536000; path=/";
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!hasShownInstallPrompt() && !installPromptTriggered) {
|
||||
installPromptTriggered = true;
|
||||
setTimeout(() => {
|
||||
e.prompt()
|
||||
.then(() => e.userChoice)
|
||||
.then(choiceResult => {
|
||||
markInstallPromptShown();
|
||||
})
|
||||
.catch(err => {
|
||||
// 可选错误处理
|
||||
});
|
||||
}, 2000); // 延迟 2 秒提示
|
||||
} else {
|
||||
// console.log("已提示过安装,不再弹出");
|
||||
}
|
||||
});
|
@@ -1,4 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
@@ -477,7 +477,7 @@ public class ConcurrentList<T> : IList<T>, IReadOnlyList<T>
|
||||
{
|
||||
lock (((ICollection)m_list).SyncRoot)
|
||||
{
|
||||
return m_list.IndexOf(item);
|
||||
return m_list.LastIndexOf(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,9 +30,27 @@ public class ImportPreviewOutputBase
|
||||
/// <summary>
|
||||
/// 返回状态
|
||||
/// </summary>
|
||||
public ConcurrentList<(int Row, bool Success, string? ErrorMessage)> Results { get; set; } = new();
|
||||
public ConcurrentList<ImportPreviewResult> Results { get; set; } = new();
|
||||
}
|
||||
public class ImportPreviewResult
|
||||
{
|
||||
public ImportPreviewResult()
|
||||
{
|
||||
|
||||
}
|
||||
public ImportPreviewResult(int row, bool success, string error)
|
||||
{
|
||||
this.Row = row;
|
||||
this.Success = success;
|
||||
this.ErrorMessage = error;
|
||||
}
|
||||
|
||||
public int Row { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 导入预览
|
||||
/// </summary>
|
||||
|
@@ -31,8 +31,8 @@ public static class GenericExtensions
|
||||
|
||||
// 比较oldModel和model的属性,找出差异
|
||||
var differences = properties
|
||||
.Where(prop => prop.CanRead && prop.CanWrite) // 确保属性可读可写
|
||||
.Where(prop => !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 找出值不同的属性
|
||||
.Where(prop => prop.CanRead && prop.CanWrite && !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 确保属性可读可写
|
||||
// 找出值不同的属性
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(model)); // 将属性名和新值存储到字典中
|
||||
|
||||
// 应用差异到channels列表中的每个Channel对象
|
||||
|
@@ -16,6 +16,8 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.Common.Extension;
|
||||
/// <summary>
|
||||
/// 对象拓展类
|
||||
@@ -48,113 +50,7 @@ public static class ObjectExtensions
|
||||
bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTimeOffset 转换成本地 DateTime
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
|
||||
{
|
||||
if (dateTime.Offset.Equals(TimeSpan.Zero))
|
||||
return dateTime.UtcDateTime;
|
||||
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
|
||||
return dateTime.ToLocalTime().DateTime;
|
||||
else
|
||||
return dateTime.DateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTimeOffset? 转换成本地 DateTime?
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
|
||||
{
|
||||
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTime 转换成 DateTimeOffset
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
|
||||
{
|
||||
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTime? 转换成 DateTimeOffset?
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
|
||||
{
|
||||
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将流保存到本地磁盘
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static void CopyToSave(this Stream stream, string path)
|
||||
{
|
||||
// 空检查
|
||||
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
||||
|
||||
using var fileStream = File.Create(path);
|
||||
stream.CopyTo(fileStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字节数组保存到本地磁盘
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static void CopyToSave(this byte[] bytes, string path)
|
||||
{
|
||||
using var stream = new MemoryStream(bytes);
|
||||
stream.CopyToSave(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将流保存到本地磁盘
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="path">需包含文件名完整路径</param>
|
||||
/// <returns></returns>
|
||||
public static async Task CopyToSaveAsync(this Stream stream, string path)
|
||||
{
|
||||
// 空检查
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
// 文件名判断
|
||||
if (string.IsNullOrWhiteSpace(Path.GetFileName(path)))
|
||||
{
|
||||
throw new ArgumentException("The parameter of <path> parameter must include the complete file name.");
|
||||
}
|
||||
|
||||
using var fileStream = File.Create(path);
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字节数组保存到本地磁盘
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task CopyToSaveAsync(this byte[] bytes, string path)
|
||||
{
|
||||
using var stream = new MemoryStream(bytes);
|
||||
await stream.CopyToSaveAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并两个字典
|
||||
@@ -186,7 +82,7 @@ public static class ObjectExtensions
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="dic">字典</param>
|
||||
/// <param name="newDic">新字典</param>
|
||||
internal static void AddOrUpdate<T>(this ConcurrentDictionary<string, T> dic, Dictionary<string, T> newDic)
|
||||
internal static void AddOrUpdate<T>(this NonBlockingDictionary<string, T> dic, Dictionary<string, T> newDic)
|
||||
{
|
||||
foreach (var (key, value) in newDic)
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ public static class ParallelExtensions
|
||||
/// <typeparam name="T">集合元素类型</typeparam>
|
||||
/// <param name="source">要操作的集合</param>
|
||||
/// <param name="body">要执行的操作</param>
|
||||
public static void ParallelForEach<T>(this IList<T> source, Action<T> body)
|
||||
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body)
|
||||
{
|
||||
ParallelOptions options = new();
|
||||
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
|
||||
@@ -38,7 +38,7 @@ public static class ParallelExtensions
|
||||
/// <typeparam name="T">集合元素类型</typeparam>
|
||||
/// <param name="source">要操作的集合</param>
|
||||
/// <param name="body">要执行的操作</param>
|
||||
public static void ParallelForEach<T>(this IList<T> source, Action<T, ParallelLoopState, long> body)
|
||||
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T, ParallelLoopState, long> body)
|
||||
{
|
||||
ParallelOptions options = new();
|
||||
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
|
||||
@@ -53,7 +53,7 @@ public static class ParallelExtensions
|
||||
/// <param name="source">要操作的集合</param>
|
||||
/// <param name="body">要执行的操作</param>
|
||||
/// <param name="parallelCount">最大并行度</param>
|
||||
public static void ParallelForEach<T>(this IList<T> source, Action<T> body, int parallelCount)
|
||||
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body, int parallelCount)
|
||||
{
|
||||
// 创建并行操作的选项对象,设置最大并行度为指定的值
|
||||
var options = new ParallelOptions();
|
||||
@@ -109,7 +109,7 @@ public static class ParallelExtensions
|
||||
/// <param name="parallelCount">最大并行度</param>
|
||||
/// <param name="cancellationToken">取消操作的标志</param>
|
||||
/// <returns>表示异步操作的任务</returns>
|
||||
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
|
||||
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 创建并行操作的选项对象,设置最大并行度和取消标志
|
||||
var options = new ParallelOptions();
|
||||
@@ -126,7 +126,7 @@ public static class ParallelExtensions
|
||||
/// <param name="body">异步执行的操作</param>
|
||||
/// <param name="cancellationToken">取消操作的标志</param>
|
||||
/// <returns>表示异步操作的任务</returns>
|
||||
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
|
||||
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ParallelForEachAsync(source, body, Environment.ProcessorCount, cancellationToken);
|
||||
}
|
||||
|
@@ -66,7 +66,8 @@ internal class JsonStringLocalizer(Assembly assembly, string typeName, string ba
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "{JsonStringLocalizerName} searched for '{Name}' in '{typeName}' with culture '{CultureName}' throw exception.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
|
||||
if (Logger?.IsEnabled(LogLevel.Error) == true)
|
||||
Logger.LogError(ex, "{JsonStringLocalizerName} searched for '{Name}' in '{typeName}' with culture '{CultureName}' throw exception.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -176,7 +177,8 @@ internal class JsonStringLocalizer(Assembly assembly, string typeName, string ba
|
||||
localizationMissingItemHandler.HandleMissingItem(name, typeName, CultureInfo.CurrentUICulture.Name);
|
||||
if (jsonLocalizationOptions.IgnoreLocalizerMissing == false)
|
||||
{
|
||||
Logger.LogInformation("{JsonStringLocalizerName} searched for '{Name}' in '{TypeName}' with culture '{CultureName}' not found.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
|
||||
if (Logger?.IsEnabled(LogLevel.Information) == true)
|
||||
Logger.LogInformation("{JsonStringLocalizerName} searched for '{Name}' in '{TypeName}' with culture '{CultureName}' not found.", nameof(JsonStringLocalizer), name, typeName, CultureInfo.CurrentUICulture.Name);
|
||||
}
|
||||
_missingManifestCache.TryAdd($"name={name}&culture={CultureInfo.CurrentUICulture.Name}");
|
||||
}
|
||||
|
@@ -5,15 +5,16 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
|
||||
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.7" />
|
||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.9.1" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.11.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -27,10 +27,15 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
|
||||
public virtual long Id { get; set; }
|
||||
}
|
||||
|
||||
public interface IPrimaryKeyEntity
|
||||
{
|
||||
string ExtJson { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主键实体基类
|
||||
/// </summary>
|
||||
public abstract class PrimaryKeyEntity : PrimaryIdEntity
|
||||
public abstract class PrimaryKeyEntity : PrimaryIdEntity, IPrimaryKeyEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 拓展信息
|
||||
|
@@ -9,6 +9,7 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace ThingsGateway.DB;
|
||||
|
||||
@@ -41,4 +42,31 @@ public static class FileExtensions
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
/// <summary>
|
||||
/// 存储本地文件
|
||||
/// </summary>
|
||||
/// <param name="pPath">存储的第一层目录</param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns>文件全路径</returns>
|
||||
public static async Task<string> StorageLocal(this IFormFile file, string pPath = "imports")
|
||||
{
|
||||
string uploadFileFolder = App.WebHostEnvironment?.WebRootPath ?? "wwwroot"!;//赋值路径
|
||||
var now = CommonUtils.GetSingleId();
|
||||
var filePath = Path.Combine(uploadFileFolder, pPath);
|
||||
if (!Directory.Exists(filePath))//如果不存在就创建文件夹
|
||||
Directory.CreateDirectory(filePath);
|
||||
//var fileSuffix = Path.GetExtension(file.Name).ToLower();// 文件后缀
|
||||
var fileObjectName = $"{now}{file.Name}";//存储后的文件名
|
||||
var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路径
|
||||
fileName = fileName.Replace("\\", "/");//格式化一系
|
||||
//存储文件
|
||||
using (var stream = File.Create(Path.Combine(filePath, fileObjectName)))
|
||||
{
|
||||
await file.CopyToAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
|
||||
return datas;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
|
||||
{
|
||||
if (queryFunc != null)
|
||||
@@ -135,4 +137,24 @@ public static class QueryPageOptionsExtensions
|
||||
ret.Items = items.ToList();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据查询条件返回QueryData
|
||||
/// </summary>
|
||||
public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
|
||||
{
|
||||
var ret = new QueryData<SelectedItem>()
|
||||
{
|
||||
IsSorted = false,
|
||||
IsFiltered = false,
|
||||
IsAdvanceSearch = false,
|
||||
IsSearch = !option.SearchText.IsNullOrWhiteSpace()
|
||||
};
|
||||
|
||||
var items = datas.Skip((option.StartIndex)).Take(option.Count);
|
||||
ret.TotalCount = datas.Count();
|
||||
|
||||
ret.Items = func(items).ToList();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@@ -209,16 +209,10 @@ public static class SqlSugarExtensions
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new()
|
||||
public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
|
||||
{
|
||||
return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
|
||||
{
|
||||
return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
|
||||
.ExecuteCommandAsync().ConfigureAwait(false) > 0;
|
||||
return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput)
|
||||
|
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
}
|
||||
}
|
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.DB.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.DB.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
}
|
||||
}
|
@@ -5,9 +5,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -18,6 +19,12 @@
|
||||
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Locales\en-US.json" />
|
||||
<EmbeddedResource Include="Locales\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
|
||||
<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->
|
||||
|
@@ -1 +1 @@
|
||||
https://gitee.com/dotnetchina/Furion/commit/8bf85f6908c1630268e45eeec607267a03947d2b
|
||||
https://gitee.com/dotnetchina/Furion/commit/f1c07d65cccb623aca9d1905bf2e1ac6e4f4b714
|
@@ -554,10 +554,9 @@ public static class App
|
||||
{
|
||||
types = ass.GetTypes();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
|
||||
Console.WriteLine($"Error load `{ass.FullName}` assembly.");
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
|
||||
}
|
||||
|
||||
return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));
|
||||
|
@@ -12,7 +12,7 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Reflection;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting;
|
||||
@@ -44,7 +44,7 @@ public static class HostBuilderExtensions
|
||||
|
||||
hostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, combineAssembliesName);
|
||||
|
||||
// 实现假的 Starup,解决泛型主机启动问题
|
||||
// 实现假的 Startup,解决泛型主机启动问题
|
||||
hostBuilder.UseStartup<FakeStartup>();
|
||||
return hostBuilder;
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
namespace ThingsGateway.Extensions;
|
||||
namespace ThingsGateway.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// 对象拓展类
|
||||
@@ -28,70 +28,10 @@ namespace ThingsGateway.Extensions;
|
||||
[SuppressSniffer]
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 DateTimeOffset 转换成本地 DateTime
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
|
||||
{
|
||||
if (dateTime.Offset.Equals(TimeSpan.Zero))
|
||||
return dateTime.UtcDateTime;
|
||||
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
|
||||
return dateTime.ToLocalTime().DateTime;
|
||||
else
|
||||
return dateTime.DateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTimeOffset? 转换成本地 DateTime?
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
|
||||
{
|
||||
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTime 转换成 DateTimeOffset
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
|
||||
{
|
||||
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 DateTime? 转换成 DateTimeOffset?
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
|
||||
{
|
||||
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将时间戳转换为 DateTime
|
||||
/// </summary>
|
||||
/// <param name="timestamp"></param>
|
||||
/// <returns></returns>
|
||||
internal static DateTime ConvertToDateTime(this long timestamp)
|
||||
{
|
||||
var timeStampDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var digitCount = (int)Math.Floor(Math.Log10(timestamp) + 1);
|
||||
|
||||
if (digitCount != 13 && digitCount != 10)
|
||||
{
|
||||
throw new ArgumentException("Data is not a valid timestamp format.");
|
||||
}
|
||||
|
||||
return (digitCount == 13
|
||||
? timeStampDateTime.AddMilliseconds(timestamp) // 13 位时间戳
|
||||
: timeStampDateTime.AddSeconds(timestamp)).ToLocalTime(); // 10 位时间戳
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 IFormFile 转换成 byte[]
|
||||
@@ -265,7 +205,7 @@ public static class ObjectExtensions
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="dic">字典</param>
|
||||
/// <param name="newDic">新字典</param>
|
||||
internal static void AddOrUpdate<T>(this ConcurrentDictionary<string, T> dic, Dictionary<string, T> newDic)
|
||||
internal static void AddOrUpdate<T>(this NonBlockingDictionary<string, T> dic, Dictionary<string, T> newDic)
|
||||
{
|
||||
foreach (var (key, value) in newDic)
|
||||
{
|
||||
|
@@ -47,7 +47,7 @@ public class StartupFilter : IStartupFilter
|
||||
else
|
||||
{
|
||||
// 输出当前环境标识
|
||||
context.Response.Headers["environment"] = envName;
|
||||
context.Response.Headers["Environment"] = envName;
|
||||
|
||||
// 输出框架版本
|
||||
context.Response.Headers[nameof(ThingsGateway)] = version;
|
||||
|
@@ -215,6 +215,10 @@ internal static class InternalApp
|
||||
// 获取环境变量名,如果没找到,则读取 NETCORE_ENVIRONMENT 环境变量信息识别(用于非 Web 环境)
|
||||
var envName = hostEnvironment?.EnvironmentName ?? Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT") ?? "Unknown";
|
||||
|
||||
// 获取 JSON 文件扫描配置(2025.07.25),修复 docker 中挂载大文件数据卷导致启动缓慢的问题
|
||||
var jsonFileScanner = configuration.GetSection("AppSettings:JsonFileScanner")
|
||||
.Get<JsonFileScanner>() ?? new JsonFileScanner();
|
||||
|
||||
// 读取忽略的配置文件
|
||||
var ignoreConfigurationFiles = (configuration.GetSection("IgnoreConfigurationFiles")
|
||||
.Get<string[]>()
|
||||
@@ -240,7 +244,7 @@ internal static class InternalApp
|
||||
// 循环加载
|
||||
foreach (var jsonFile in files)
|
||||
{
|
||||
configurationBuilder.AddJsonFile(jsonFile, optional: true, reloadOnChange: true);
|
||||
configurationBuilder.AddJsonFile(jsonFile, optional: jsonFileScanner.Optional, reloadOnChange: jsonFileScanner.ReloadOnChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,10 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
|
||||
/// 【部署】二级虚拟目录
|
||||
/// </summary>
|
||||
public string VirtualPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON 文件扫描配置
|
||||
/// </summary>
|
||||
public JsonFileScanner JsonFileScanner { get; set; }
|
||||
/// <summary>
|
||||
/// 后期配置
|
||||
/// </summary>
|
||||
@@ -67,3 +70,20 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
|
||||
options.VirtualPath ??= string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 文件扫描配置
|
||||
/// </summary>
|
||||
/// <remarks>修复 docker 中挂载大文件数据卷导致启动缓慢的问题。</remarks>
|
||||
public class JsonFileScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否可选
|
||||
/// </summary>
|
||||
public bool Optional { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否改变的时候重载
|
||||
/// </summary>
|
||||
public bool ReloadOnChange { get; set; } = true;
|
||||
}
|
@@ -701,7 +701,7 @@ public static class Serve
|
||||
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
|
||||
|
||||
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
|
||||
// 配置所有 Starup Configure
|
||||
// 配置所有 Startup Configure
|
||||
UseStartups(app);
|
||||
UseStartups(app.Services);
|
||||
|
||||
@@ -820,7 +820,7 @@ public static class Serve
|
||||
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
|
||||
|
||||
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
|
||||
// 配置所有 Starup Configure
|
||||
// 配置所有 Startup Configure
|
||||
UseStartups(app.Services);
|
||||
// 释放内存
|
||||
App.AppStartups.Clear();
|
||||
@@ -943,7 +943,7 @@ public static class Serve
|
||||
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
|
||||
|
||||
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
|
||||
// 配置所有 Starup Configure
|
||||
// 配置所有 Startup Configure
|
||||
UseStartups(app.Services);
|
||||
// 释放内存
|
||||
App.AppStartups.Clear();
|
||||
@@ -1005,7 +1005,7 @@ public static class Serve
|
||||
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
|
||||
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
|
||||
|
||||
// 配置所有 Starup Configure
|
||||
// 配置所有 Startup Configure
|
||||
UseStartups(app.Services);
|
||||
// 释放内存
|
||||
App.AppStartups.Clear();
|
||||
|
@@ -94,7 +94,7 @@ public static class AspNetCoreBuilderServiceCollectionExtensions
|
||||
/// <param name="mvcBuilder"></param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns></returns>
|
||||
public static IMvcBuilder AddFromConvertBinding(this IMvcBuilder mvcBuilder, Action<ConcurrentDictionary<Type, Type>> configure = default)
|
||||
public static IMvcBuilder AddFromConvertBinding(this IMvcBuilder mvcBuilder, Action<NonBlockingDictionary<Type, Type>> configure = default)
|
||||
{
|
||||
mvcBuilder.Services.AddFromConvertBinding(configure);
|
||||
|
||||
@@ -107,13 +107,13 @@ public static class AspNetCoreBuilderServiceCollectionExtensions
|
||||
/// <param name="services"></param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddFromConvertBinding(this IServiceCollection services, Action<ConcurrentDictionary<Type, Type>> configure = default)
|
||||
public static IServiceCollection AddFromConvertBinding(this IServiceCollection services, Action<NonBlockingDictionary<Type, Type>> configure = default)
|
||||
{
|
||||
// 非 Web 环境跳过注册
|
||||
if (App.WebHostEnvironment == default) return services;
|
||||
|
||||
// 定义模型绑定转换器集合
|
||||
var modelBinderConverts = new ConcurrentDictionary<Type, Type>();
|
||||
var modelBinderConverts = new NonBlockingDictionary<Type, Type>();
|
||||
modelBinderConverts.TryAdd(typeof(DateTime), typeof(DateTimeModelConvertBinder));
|
||||
modelBinderConverts.TryAdd(typeof(DateTimeOffset), typeof(DateTimeOffsetModelConvertBinder));
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -27,13 +27,13 @@ public class FromConvertBinder : IModelBinder
|
||||
/// <summary>
|
||||
/// 定义模型绑定转换器集合
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<Type, Type> _modelBinderConverts;
|
||||
private readonly NonBlockingDictionary<Type, Type> _modelBinderConverts;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="modelBinderConverts">定义模型绑定转换器集合</param>
|
||||
public FromConvertBinder(ConcurrentDictionary<Type, Type> modelBinderConverts)
|
||||
public FromConvertBinder(NonBlockingDictionary<Type, Type> modelBinderConverts)
|
||||
{
|
||||
_modelBinderConverts = modelBinderConverts;
|
||||
}
|
||||
|
@@ -28,13 +28,13 @@ public class FromConvertBinderProvider : IModelBinderProvider
|
||||
/// <summary>
|
||||
/// 定义模型绑定转换器集合
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<Type, Type> _modelBinderConverts;
|
||||
private readonly NonBlockingDictionary<Type, Type> _modelBinderConverts;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="modelBinderConverts">定义模型绑定转换器集合</param>
|
||||
public FromConvertBinderProvider(ConcurrentDictionary<Type, Type> modelBinderConverts)
|
||||
public FromConvertBinderProvider(NonBlockingDictionary<Type, Type> modelBinderConverts)
|
||||
{
|
||||
_modelBinderConverts = modelBinderConverts;
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -12,7 +12,7 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.AspNetCore;
|
||||
|
||||
|
@@ -18,7 +18,7 @@ using System.Reflection;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.ConfigurableOptions;
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
@@ -34,9 +34,12 @@ public static class PBKDF2Encryption
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
var salt = new byte[saltSize];
|
||||
rng.GetBytes(salt);
|
||||
|
||||
#if NET10_0_OR_GREATER
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), salt, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
|
||||
#else
|
||||
using var pbkdf2 = new Rfc2898DeriveBytes(text, salt, iterationCount, HashAlgorithmName.SHA256);
|
||||
var hash = pbkdf2.GetBytes(derivedKeyLength);
|
||||
#endif
|
||||
|
||||
// 分别编码盐和哈希,并用分隔符拼接
|
||||
return Convert.ToBase64String(salt) + SaltHashSeparator + Convert.ToBase64String(hash);
|
||||
@@ -65,8 +68,12 @@ public static class PBKDF2Encryption
|
||||
if (saltBytes.Length != saltSize || storedHashBytes.Length != derivedKeyLength)
|
||||
return false;
|
||||
|
||||
#if NET10_0_OR_GREATER
|
||||
var computedHash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), saltBytes, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
|
||||
#else
|
||||
using var pbkdf2 = new Rfc2898DeriveBytes(text, saltBytes, iterationCount, HashAlgorithmName.SHA256);
|
||||
var computedHash = pbkdf2.GetBytes(derivedKeyLength);
|
||||
#endif
|
||||
|
||||
return computedHash.SequenceEqual(storedHashBytes);
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Templates.Extensions;
|
||||
|
||||
namespace ThingsGateway.DataValidation;
|
||||
@@ -40,7 +40,7 @@ public static class DataValidator
|
||||
/// <summary>
|
||||
/// 验证类型正则表达式
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, ValidationItemMetadataAttribute> ValidationItemMetadatas;
|
||||
private static readonly NonBlockingDictionary<string, ValidationItemMetadataAttribute> ValidationItemMetadatas;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
@@ -57,7 +57,7 @@ public static class DataValidator
|
||||
ValidationItemMetadatas = GetValidationValidationItemMetadatas();
|
||||
|
||||
// 缓存所有正则表达式
|
||||
GetValidationTypeValidationItemMetadataCached = new ConcurrentDictionary<object, (string, ValidationItemMetadataAttribute)>();
|
||||
GetValidationTypeValidationItemMetadataCached = new NonBlockingDictionary<object, (string, ValidationItemMetadataAttribute)>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,7 +203,7 @@ public static class DataValidator
|
||||
/// <summary>
|
||||
/// 获取验证类型验证Item集合
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<object, (string, ValidationItemMetadataAttribute)> GetValidationTypeValidationItemMetadataCached;
|
||||
private static readonly NonBlockingDictionary<object, (string, ValidationItemMetadataAttribute)> GetValidationTypeValidationItemMetadataCached;
|
||||
|
||||
/// <summary>
|
||||
/// 获取验证类型正则表达式(需要缓存)
|
||||
@@ -267,9 +267,9 @@ public static class DataValidator
|
||||
/// 获取验证类型所有有效的正则表达式
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static ConcurrentDictionary<string, ValidationItemMetadataAttribute> GetValidationValidationItemMetadatas()
|
||||
private static NonBlockingDictionary<string, ValidationItemMetadataAttribute> GetValidationValidationItemMetadatas()
|
||||
{
|
||||
var vaidationItems = new ConcurrentDictionary<string, ValidationItemMetadataAttribute>();
|
||||
var vaidationItems = new NonBlockingDictionary<string, ValidationItemMetadataAttribute>();
|
||||
|
||||
// 查找所有 [ValidationMessageType] 类型中的 [ValidationMessage] 消息定义
|
||||
var customErrorMessages = ValidationMessageTypes.SelectMany(u => u.GetFields()
|
||||
|
@@ -353,7 +353,7 @@ public static class DependencyInjectionServiceCollectionExtensions
|
||||
/// <summary>
|
||||
/// 类型名称集合
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, Type> TypeNamedCollection;
|
||||
private static readonly NonBlockingDictionary<string, Type> TypeNamedCollection;
|
||||
|
||||
/// <summary>
|
||||
/// 创建代理方法
|
||||
@@ -374,7 +374,7 @@ public static class DependencyInjectionServiceCollectionExtensions
|
||||
GlobalServiceProxyType = App.EffectiveTypes
|
||||
.FirstOrDefault(u => typeof(AspectDispatchProxy).IsAssignableFrom(u) && typeof(IGlobalDispatchProxy).IsAssignableFrom(u) && u.IsClass && !u.IsInterface && !u.IsAbstract);
|
||||
|
||||
TypeNamedCollection = new ConcurrentDictionary<string, Type>();
|
||||
TypeNamedCollection = new NonBlockingDictionary<string, Type>();
|
||||
DispatchCreateMethod = typeof(AspectDispatchProxy).GetMethod(nameof(AspectDispatchProxy.Create));
|
||||
}
|
||||
}
|
||||
|
@@ -50,9 +50,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
|
||||
#pragma warning disable CA1851
|
||||
if (services
|
||||
.OfType<AspectDispatchProxy>()
|
||||
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
|
||||
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
|
||||
{
|
||||
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
|
||||
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
|
||||
}
|
||||
#pragma warning restore CA1851
|
||||
|
||||
@@ -85,9 +85,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
|
||||
#pragma warning disable CA1851
|
||||
if (services
|
||||
.OfType<AspectDispatchProxy>()
|
||||
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
|
||||
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
|
||||
{
|
||||
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
|
||||
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
|
||||
}
|
||||
#pragma warning restore CA1851
|
||||
|
||||
@@ -116,7 +116,7 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
private static string ResovleServiceName(Type type)
|
||||
private static string ResolveServiceName(Type type)
|
||||
{
|
||||
if (type.IsDefined(typeof(InjectionAttribute)))
|
||||
{
|
||||
|
@@ -21,7 +21,7 @@ using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.UnifyResult;
|
||||
|
||||
namespace ThingsGateway.DynamicApiController;
|
||||
|
@@ -28,21 +28,21 @@ internal static class Penetrates
|
||||
/// <summary>
|
||||
/// 请求动词映射字典
|
||||
/// </summary>
|
||||
internal static ConcurrentDictionary<string, string> VerbToHttpMethods { get; private set; }
|
||||
internal static NonBlockingDictionary<string, string> VerbToHttpMethods { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 控制器排序集合
|
||||
/// </summary>
|
||||
internal static ConcurrentDictionary<string, (string, int, Type)> ControllerOrderCollection { get; set; }
|
||||
internal static NonBlockingDictionary<string, (string, int, Type)> ControllerOrderCollection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
static Penetrates()
|
||||
{
|
||||
ControllerOrderCollection = new ConcurrentDictionary<string, (string, int, Type)>();
|
||||
ControllerOrderCollection = new NonBlockingDictionary<string, (string, int, Type)>();
|
||||
|
||||
VerbToHttpMethods = new ConcurrentDictionary<string, string>
|
||||
VerbToHttpMethods = new NonBlockingDictionary<string, string>
|
||||
{
|
||||
["post"] = "POST",
|
||||
["add"] = "POST",
|
||||
@@ -67,13 +67,13 @@ internal static class Penetrates
|
||||
["patch"] = "PATCH"
|
||||
};
|
||||
|
||||
//IsApiControllerCached = new ConcurrentDictionary<Type, bool>();
|
||||
//IsApiControllerCached = new NonBlockingDictionary<Type, bool>();
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// <see cref="IsApiController(Type)"/> 缓存集合
|
||||
///// </summary>
|
||||
//private static readonly ConcurrentDictionary<Type, bool> IsApiControllerCached;
|
||||
//private static readonly NonBlockingDictionary<Type, bool> IsApiControllerCached;
|
||||
|
||||
/// <summary>
|
||||
/// 是否是Api控制器
|
||||
|
@@ -61,7 +61,7 @@ internal sealed class EventBusHostedService : BackgroundService
|
||||
/// <summary>
|
||||
/// 事件处理程序集合
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<EventHandlerWrapper, EventHandlerWrapper> _eventHandlers = new();
|
||||
private readonly NonBlockingDictionary<EventHandlerWrapper, EventHandlerWrapper> _eventHandlers = new();
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
@@ -295,7 +295,8 @@ internal sealed class EventBusHostedService : BackgroundService
|
||||
, retryAction: (total, times) =>
|
||||
{
|
||||
// 输出重试日志
|
||||
_logger.LogWarning("Retrying {times}/{total} times for {EventId}", times, total, eventSource.EventId);
|
||||
if (_logger?.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Warning) == true)
|
||||
_logger.LogWarning("Retrying {times}/{total} times for {EventId}", times, total, eventSource.EventId);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
|
@@ -17,7 +17,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Templates.Extensions;
|
||||
|
||||
namespace ThingsGateway.FriendlyException;
|
||||
@@ -31,7 +31,7 @@ public static class Oops
|
||||
/// <summary>
|
||||
/// 方法错误异常特性
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<MethodBase, MethodIfException> _errorMethods;
|
||||
private static readonly NonBlockingDictionary<MethodBase, MethodIfException> _errorMethods;
|
||||
|
||||
/// <summary>
|
||||
/// 错误代码类型
|
||||
@@ -41,7 +41,7 @@ public static class Oops
|
||||
/// <summary>
|
||||
/// 错误消息字典
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, string> _errorCodeMessages;
|
||||
private static readonly NonBlockingDictionary<string, string> _errorCodeMessages;
|
||||
|
||||
/// <summary>
|
||||
/// 友好异常设置
|
||||
@@ -53,7 +53,7 @@ public static class Oops
|
||||
/// </summary>
|
||||
static Oops()
|
||||
{
|
||||
_errorMethods = new ConcurrentDictionary<MethodBase, MethodIfException>();
|
||||
_errorMethods = new NonBlockingDictionary<MethodBase, MethodIfException>();
|
||||
_friendlyExceptionSettings = App.GetConfig<FriendlyExceptionSettingsOptions>("FriendlyExceptionSettings", true);
|
||||
_errorCodeTypes = GetErrorCodeTypes();
|
||||
_errorCodeMessages = GetErrorCodeMessages();
|
||||
@@ -258,9 +258,9 @@ public static class Oops
|
||||
/// 获取所有错误消息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static ConcurrentDictionary<string, string> GetErrorCodeMessages()
|
||||
private static NonBlockingDictionary<string, string> GetErrorCodeMessages()
|
||||
{
|
||||
var defaultErrorCodeMessages = new ConcurrentDictionary<string, string>();
|
||||
var defaultErrorCodeMessages = new NonBlockingDictionary<string, string>();
|
||||
|
||||
// 查找所有 [ErrorCodeType] 类型中的 [ErrorCodeMetadata] 元数据定义
|
||||
var errorCodeMessages = _errorCodeTypes.SelectMany(u => u.GetFields().Where(u => u.IsDefined(typeof(ErrorCodeItemMetadataAttribute))))
|
||||
|
@@ -27,13 +27,15 @@ public sealed class Retry
|
||||
/// <param name="exceptionTypes">异常类型,可多个</param>
|
||||
/// <param name="fallbackPolicy">重试失败回调</param>
|
||||
/// <param name="retryAction">重试时调用方法</param>
|
||||
/// <param name="shouldExit">退出条件</param>
|
||||
public static void Invoke(Action action
|
||||
, int numRetries
|
||||
, int retryTimeout = 1000
|
||||
, bool finalThrow = true
|
||||
, Type[] exceptionTypes = default
|
||||
, Action<Exception> fallbackPolicy = default
|
||||
, Action<int, int> retryAction = default)
|
||||
, Action<int, int> retryAction = default
|
||||
, Func<bool> shouldExit = default)
|
||||
{
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
|
||||
@@ -46,7 +48,7 @@ public sealed class Retry
|
||||
{
|
||||
fallbackPolicy?.Invoke(ex);
|
||||
return Task.CompletedTask;
|
||||
}, retryAction).GetAwaiter().GetResult();
|
||||
}, retryAction, shouldExit).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,6 +61,7 @@ public sealed class Retry
|
||||
/// <param name="exceptionTypes">异常类型,可多个</param>
|
||||
/// <param name="fallbackPolicy">重试失败回调</param>
|
||||
/// <param name="retryAction">重试时调用方法</param>
|
||||
/// <param name="shouldExit">退出条件</param>
|
||||
/// <returns><see cref="Task"/></returns>
|
||||
public static async Task InvokeAsync(Func<Task> action
|
||||
, int numRetries
|
||||
@@ -66,7 +69,8 @@ public sealed class Retry
|
||||
, bool finalThrow = true
|
||||
, Type[] exceptionTypes = default
|
||||
, Func<Exception, Task> fallbackPolicy = default
|
||||
, Action<int, int> retryAction = default)
|
||||
, Action<int, int> retryAction = default
|
||||
, Func<bool> shouldExit = default)
|
||||
{
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
|
||||
@@ -117,6 +121,12 @@ public sealed class Retry
|
||||
|
||||
// 如果可重试异常数大于 0,则间隔指定时间后继续执行
|
||||
if (retryTimeout > 0) await Task.Delay(retryTimeout).ConfigureAwait(false);
|
||||
|
||||
// 处理退出机制
|
||||
if (shouldExit != null && shouldExit())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.InstantMessaging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder;
|
||||
|
@@ -23,7 +23,7 @@ public static class ILoggerExtensions
|
||||
/// 设置日志上下文
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
|
||||
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
|
||||
/// <returns></returns>
|
||||
public static IDisposable ScopeContext(this ILogger logger, IDictionary<string, object> properties)
|
||||
{
|
||||
|
@@ -9,7 +9,7 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
|
||||
namespace ThingsGateway.Logging;
|
||||
|
||||
|
@@ -82,7 +82,7 @@ public static class StringLoggingExtensions
|
||||
/// 配置日志上下文
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
|
||||
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
|
||||
/// <returns></returns>
|
||||
public static StringLoggingPart ScopeContext(this string message, IDictionary<string, object> properties)
|
||||
{
|
||||
|
@@ -44,7 +44,7 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
|
||||
/// 记录日志所有滚动文件名
|
||||
/// </summary>
|
||||
/// <remarks>只有 MaxRollingFiles 和 FileSizeLimitBytes 大于 0 有效</remarks>
|
||||
internal readonly ConcurrentDictionary<string, FileInfo> _rollingFileNames = new();
|
||||
internal readonly NonBlockingDictionary<string, FileInfo> _rollingFileNames = new();
|
||||
|
||||
/// <summary>
|
||||
/// 文件日志写入器
|
||||
|
@@ -36,8 +36,9 @@ using System.Text.Json;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.DataValidation;
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.FriendlyException;
|
||||
using ThingsGateway.JsonSerialization;
|
||||
using ThingsGateway.Logging;
|
||||
using ThingsGateway.Templates;
|
||||
using ThingsGateway.UnifyResult;
|
||||
@@ -150,7 +151,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
return;
|
||||
}
|
||||
|
||||
await MonitorAsync(actionMethod, context.ActionArguments, context, next).ConfigureAwait(false);
|
||||
await MonitorAsync(actionMethod, context.ActionArguments, context, () => next()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,7 +183,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
return;
|
||||
}
|
||||
|
||||
await MonitorAsync(actionMethod, context.HandlerArguments, context, next).ConfigureAwait(false);
|
||||
await MonitorAsync(actionMethod, context.HandlerArguments, context, () => next()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -639,6 +640,11 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
// 解决 JsonElement 序列化问题
|
||||
jsonSerializerSettings.Converters.Add(new JsonElementConverter());
|
||||
|
||||
// 解决 JsonObject 和 JsonArray 序列化问题
|
||||
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonObjectJsonConverter());
|
||||
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonArrayJsonConverter());
|
||||
|
||||
|
||||
// 解决 DateTimeOffset 序列化/反序列化问题
|
||||
if (obj is DateTimeOffset)
|
||||
{
|
||||
@@ -783,12 +789,12 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
return typeName;
|
||||
}
|
||||
|
||||
private async Task MonitorAsync(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, dynamic next)
|
||||
private async Task MonitorAsync<T>(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, Func<Task<T>> next)
|
||||
{
|
||||
// 排除 WebSocket 请求处理
|
||||
if (context.HttpContext.IsWebSocketRequest())
|
||||
{
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -799,14 +805,14 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
if (actionMethod.IsDefined(typeof(SuppressMonitorAttribute), true)
|
||||
|| actionMethod.DeclaringType.IsDefined(typeof(SuppressMonitorAttribute), true))
|
||||
{
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件
|
||||
if (LoggingMonitorSettings.InternalWriteFilter?.Invoke(context) == false)
|
||||
{
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -819,7 +825,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
// 解决局部和全局触发器同时配置触发两次问题
|
||||
if (isDefinedScopedAttribute && Settings.FromGlobalFilter == true)
|
||||
{
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -833,7 +839,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
&& !Settings.IncludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
// 查找是否包含匹配,忽略大小写
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -841,7 +847,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
if (Settings.GlobalEnabled
|
||||
&& Settings.ExcludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = await next();
|
||||
_ = await next().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -952,7 +958,8 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
|
||||
// 计算接口执行时间
|
||||
var timeOperation = Stopwatch.StartNew();
|
||||
var resultContext = await next();
|
||||
|
||||
var resultContext = await next().ConfigureAwait(false);
|
||||
timeOperation.Stop();
|
||||
writer.WriteNumber("timeOperationElapsedMilliseconds", timeOperation.ElapsedMilliseconds);
|
||||
|
||||
@@ -1008,8 +1015,13 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
|
||||
var environment = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().EnvironmentName;
|
||||
writer.WriteString(nameof(environment), environment);
|
||||
|
||||
Exception exception = null;
|
||||
// 获取异常对象情况
|
||||
Exception exception = resultContext.Exception;
|
||||
if (resultContext is PageHandlerExecutedContext pageHandlerExecutedContext)
|
||||
exception = pageHandlerExecutedContext.Exception;
|
||||
else if (resultContext is ActionExecutedContext actionExecutedContext)
|
||||
exception = actionExecutedContext.Exception;
|
||||
|
||||
if (exception == null)
|
||||
{
|
||||
// 解析存储的验证信息
|
||||
|
@@ -94,7 +94,7 @@ public sealed partial class StringLoggingPart
|
||||
/// <summary>
|
||||
/// 配置日志上下文
|
||||
/// </summary>
|
||||
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
|
||||
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
|
||||
/// <returns></returns>
|
||||
public StringLoggingPart ScopeContext(IDictionary<string, object> properties)
|
||||
{
|
||||
|
@@ -57,7 +57,7 @@ public static class Log
|
||||
/// <summary>
|
||||
/// 配置日志上下文
|
||||
/// </summary>
|
||||
/// <param name="properties">建议使用 ConcurrentDictionary 类型</param>
|
||||
/// <param name="properties">建议使用 NonBlockingDictionary 类型</param>
|
||||
/// <returns></returns>
|
||||
public static (ILogger logger, IDisposable scope) ScopeContext(IDictionary<string, object> properties)
|
||||
{
|
||||
|
@@ -16,7 +16,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.Extensions;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.Options;
|
||||
|
||||
namespace Microsoft.Extensions.Options;
|
||||
|
@@ -20,9 +20,10 @@ public sealed class DailyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public DailyAtAttribute(params object[] fields)
|
||||
: base("@daily", fields)
|
||||
public DailyAtAttribute(object field, params object[] fields)
|
||||
: base("@daily", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -20,9 +20,10 @@ public sealed class HourlyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public HourlyAtAttribute(params object[] fields)
|
||||
: base("@hourly", fields)
|
||||
public HourlyAtAttribute(object field, params object[] fields)
|
||||
: base("@hourly", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -20,9 +20,10 @@ public sealed class MinutelyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public MinutelyAtAttribute(params object[] fields)
|
||||
: base("@minutely", fields)
|
||||
public MinutelyAtAttribute(object field, params object[] fields)
|
||||
: base("@minutely", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -20,9 +20,10 @@ public sealed class MonthlyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public MonthlyAtAttribute(params object[] fields)
|
||||
: base("@monthly", fields)
|
||||
public MonthlyAtAttribute(object field, params object[] fields)
|
||||
: base("@monthly", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -14,15 +14,16 @@ namespace ThingsGateway.Schedule;
|
||||
/// <summary>
|
||||
/// 特定秒开始作业触发器特性
|
||||
/// </summary>
|
||||
[SecondlyAtAttribute, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
[SuppressSniffer, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public sealed class SecondlyAtAttribute : CronAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public SecondlyAtAttribute(params object[] fields)
|
||||
: base("@secondly", fields)
|
||||
public SecondlyAtAttribute(object field, params object[] fields)
|
||||
: base("@secondly", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -20,9 +20,10 @@ public sealed class WeeklyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public WeeklyAtAttribute(params object[] fields)
|
||||
: base("@weekly", fields)
|
||||
public WeeklyAtAttribute(object field, params object[] fields)
|
||||
: base("@weekly", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -20,9 +20,10 @@ public sealed class YearlyAtAttribute : CronAttribute
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="field">字段值</param>
|
||||
/// <param name="fields">字段值</param>
|
||||
public YearlyAtAttribute(params object[] fields)
|
||||
: base("@yearly", fields)
|
||||
public YearlyAtAttribute(object field, params object[] fields)
|
||||
: base("@yearly", new[] { field }.Concat(fields).ToArray())
|
||||
{
|
||||
}
|
||||
}
|
@@ -21,7 +21,7 @@ internal sealed class JobCancellationToken : IJobCancellationToken
|
||||
/// <summary>
|
||||
/// 取消作业执行 Token 集合
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokenSources;
|
||||
private readonly NonBlockingDictionary<string, CancellationTokenSource> _cancellationTokenSources;
|
||||
|
||||
/// <summary>
|
||||
/// 作业调度器日志服务
|
||||
@@ -93,12 +93,14 @@ internal sealed class JobCancellationToken : IJobCancellationToken
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex) when (!(ex is OperationCanceledException ||
|
||||
ex is ObjectDisposedException ||
|
||||
(ex is AggregateException aggEx && aggEx.InnerExceptions.All(e => e is OperationCanceledException || e is ObjectDisposedException))))
|
||||
{ }
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 输出非任务取消异常日志
|
||||
if (!(ex is OperationCanceledException || (ex is AggregateException aggEx && aggEx.InnerExceptions.Count == 1 && aggEx.InnerExceptions[0] is TaskCanceledException)))
|
||||
{
|
||||
// 待输出
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,8 @@
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ThingsGateway.Schedule;
|
||||
|
||||
/// <summary>
|
||||
@@ -152,7 +154,31 @@ public abstract class JobExecutionContext
|
||||
writer.WriteEndObject();
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// 检查作业任务是否处于正常状态
|
||||
/// </summary>
|
||||
/// <param name="schedulerFactory"><see cref="ISchedulerFactory"/></param>
|
||||
/// <returns><see cref="bool"/></returns>
|
||||
public bool IsNormalStatus(ISchedulerFactory schedulerFactory = null)
|
||||
{
|
||||
// 解析作业计划工厂服务
|
||||
schedulerFactory ??= ServiceProvider.GetRequiredService<ISchedulerFactory>();
|
||||
|
||||
// 情况 1:检查作业是否存在
|
||||
if (schedulerFactory.TryGetJob(JobId, out var scheduler) != ScheduleResult.Succeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 情况 2:检查作业触发器是否存在
|
||||
if (scheduler.TryGetTrigger(TriggerId, out var trigger) != ScheduleResult.Succeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 情况 3:检查作业触发器是否正常运行
|
||||
return trigger.IsNormalStatus();
|
||||
}
|
||||
/// <summary>
|
||||
/// 作业执行上下文转字符串输出输出
|
||||
/// </summary>
|
||||
|
@@ -209,7 +209,7 @@ public sealed class ScheduleUIMiddleware
|
||||
case "remove":
|
||||
_schedulerFactory.RemoveJob(jobId);
|
||||
break;
|
||||
// 立即执行
|
||||
// 手动执行
|
||||
case "run":
|
||||
_schedulerFactory.RunJob(jobId);
|
||||
break;
|
||||
@@ -264,7 +264,7 @@ public sealed class ScheduleUIMiddleware
|
||||
case "remove":
|
||||
scheduler1?.RemoveTrigger(triggerId);
|
||||
break;
|
||||
// 立即执行
|
||||
// 手动执行
|
||||
case "run":
|
||||
scheduler1?.Run(triggerId);
|
||||
break;
|
||||
|
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/__schedule__/static/css/main.fbe5db1c.css",
|
||||
"main.js": "/__schedule__/static/js/main.851eb0b3.js",
|
||||
"main.css": "/__schedule__/static/css/main.765127e9.css",
|
||||
"main.js": "/__schedule__/static/js/main.326c761f.js",
|
||||
"index.html": "/__schedule__/index.html",
|
||||
"main.fbe5db1c.css.map": "/__schedule__/static/css/main.fbe5db1c.css.map",
|
||||
"main.851eb0b3.js.map": "/__schedule__/static/js/main.851eb0b3.js.map"
|
||||
"main.765127e9.css.map": "/__schedule__/static/css/main.765127e9.css.map",
|
||||
"main.326c761f.js.map": "/__schedule__/static/js/main.326c761f.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.fbe5db1c.css",
|
||||
"static/js/main.851eb0b3.js"
|
||||
"static/css/main.765127e9.css",
|
||||
"static/js/main.326c761f.js"
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user