Compare commits
	
		
			134 Commits
		
	
	
		
			10.10.5.0
			...
			10.11.117.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					18d1cffb2d | ||
| 
						 | 
					516fd7f235 | ||
| 
						 | 
					2ee16c3533 | ||
| 
						 | 
					7d22f5c78e | ||
| 
						 | 
					3e604ee2fd | ||
| 
						 | 
					47e442874c | ||
| 
						 | 
					2a8c0cbab1 | ||
| 
						 | 
					c26898b49d | ||
| 
						 | 
					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 | ||
| 
						 | 
					6660ce3e34 | ||
| 
						 | 
					7499162c1a | ||
| 
						 | 
					40208a5cd6 | ||
| 
						 | 
					fa347f4f68 | ||
| 
						 | 
					d7df6fc605 | ||
| 
						 | 
					eb4bb2fd48 | ||
| 
						 | 
					faa9858974 | ||
| 
						 | 
					1b3d2dda49 | ||
| 
						 | 
					a8a9453611 | ||
| 
						 | 
					e84f42ce14 | ||
| 
						 | 
					6f814cf6b8 | ||
| 
						 | 
					e36432e4e9 | ||
| 
						 | 
					ebd71e807b | ||
| 
						 | 
					34000d8d7d | 
							
								
								
									
										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('/', '_')));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,12 +38,14 @@ public class VerificatInfo : PrimaryIdEntity
 | 
			
		||||
    [AutoGenerateColumn(Filterable = true, Sortable = true)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
 | 
			
		||||
    [IgnoreExcel]
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public override long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 登录IP
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [AutoGenerateColumn(Filterable = true, Sortable = true, Width = 200)]
 | 
			
		||||
    [SugarColumn(IsNullable = true)]
 | 
			
		||||
    public string LoginIp { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -77,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
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class USheetDatas
 | 
			
		||||
{
 | 
			
		||||
@@ -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 角色模块处理
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主键Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public override long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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" />
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public static class USheetDataHelpers
 | 
			
		||||
{
 | 
			
		||||
    public static USheetDatas GetUSheetDatas(Dictionary<string, object> data)
 | 
			
		||||
    {
 | 
			
		||||
        var uSheetDatas = new USheetDatas();
 | 
			
		||||
 | 
			
		||||
        foreach (var a in data)
 | 
			
		||||
        {
 | 
			
		||||
            var value = (a.Value as IEnumerable<Dictionary<string, object>>).ToList();
 | 
			
		||||
 | 
			
		||||
            var uSheetData = new USheetData();
 | 
			
		||||
            uSheetData.id = a.Key;
 | 
			
		||||
            uSheetData.name = a.Key;
 | 
			
		||||
 | 
			
		||||
            for (int row1 = 0; row1 < value.Count; row1++)
 | 
			
		||||
            {
 | 
			
		||||
                if (row1 == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    Dictionary<int, USheetCelldata> usheetColldata = new();
 | 
			
		||||
                    int col = 0;
 | 
			
		||||
                    foreach (var colData in value[row1])
 | 
			
		||||
                    {
 | 
			
		||||
                        usheetColldata.Add(col, new USheetCelldata() { v = colData.Key });
 | 
			
		||||
                        col++;
 | 
			
		||||
                    }
 | 
			
		||||
                    uSheetData.cellData.Add(row1, usheetColldata);
 | 
			
		||||
                }
 | 
			
		||||
                {
 | 
			
		||||
                    Dictionary<int, USheetCelldata> usheetColldata = new();
 | 
			
		||||
                    int col = 0;
 | 
			
		||||
                    foreach (var colData in value[row1])
 | 
			
		||||
                    {
 | 
			
		||||
                        usheetColldata.Add(col, new USheetCelldata() { v = colData.Value });
 | 
			
		||||
                        col++;
 | 
			
		||||
                    }
 | 
			
		||||
                    uSheetData.cellData.Add(row1 + 1, usheetColldata);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            uSheetData.rowCount = uSheetData.cellData.Count + 100;
 | 
			
		||||
            uSheetData.columnCount = uSheetData.cellData.FirstOrDefault().Value?.Count ?? 0;
 | 
			
		||||
            uSheetDatas.sheets.Add(a.Key, uSheetData);
 | 
			
		||||
        }
 | 
			
		||||
        return uSheetDatas;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
@namespace ThingsGateway.Gateway.Razor
 | 
			
		||||
@namespace ThingsGateway.Admin.Razor
 | 
			
		||||
@using ThingsGateway.Admin.Application
 | 
			
		||||
@using ThingsGateway.Admin.Razor
 | 
			
		||||
@using ThingsGateway.Gateway.Application
 | 
			
		||||
 | 
			
		||||
<div class="h-600px">
 | 
			
		||||
    <UniverSheet @ref="_sheetExcel" OnReadyAsync="OnReadyAsync"></UniverSheet>
 | 
			
		||||
@@ -8,9 +8,10 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Admin.Application;
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Razor;
 | 
			
		||||
namespace ThingsGateway.Admin.Razor;
 | 
			
		||||
 | 
			
		||||
public partial class USheet
 | 
			
		||||
{
 | 
			
		||||
@@ -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,18 +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/culture.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]
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
    </app>
 | 
			
		||||
    
 | 
			
		||||
    <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
 | 
			
		||||
    <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
 | 
			
		||||
    <script src="_framework/blazor.server.js"></script>
 | 
			
		||||
 | 
			
		||||
       <!-- PWA Service Worker -->
 | 
			
		||||
 
 | 
			
		||||
@@ -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 =>
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -45,11 +45,11 @@ public class Startup : AppStartup
 | 
			
		||||
            options.ServicesStopConcurrently = true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //// 事件总线
 | 
			
		||||
        //services.AddEventBus(options =>
 | 
			
		||||
        //{
 | 
			
		||||
        // 事件总线
 | 
			
		||||
        services.AddEventBus(options =>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        //});
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 任务调度
 | 
			
		||||
        services.AddSchedule(options => options.AddPersistence<JobPersistence>());
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Common;
 | 
			
		||||
 | 
			
		||||
public class SmartTriggerScheduler
 | 
			
		||||
{
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class StringOrdinalIgnoreCaseEqualityComparer : EqualityComparer<string>
 | 
			
		||||
{
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ internal class CacheManager
 | 
			
		||||
{
 | 
			
		||||
    private IMemoryCache Cache { get; set; }
 | 
			
		||||
 | 
			
		||||
    private IServiceProvider Provider { get; set; }
 | 
			
		||||
    private static IServiceProvider Provider => App.RootServices;
 | 
			
		||||
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    private static CacheManager? Instance { get; set; }
 | 
			
		||||
@@ -40,8 +40,7 @@ internal class CacheManager
 | 
			
		||||
    static CacheManager()
 | 
			
		||||
    {
 | 
			
		||||
        Instance = new();
 | 
			
		||||
        Instance.Provider = App.RootServices;
 | 
			
		||||
        Instance.Cache = Instance.Provider.GetRequiredService<IMemoryCache>();
 | 
			
		||||
        Instance.Cache = Provider.GetRequiredService<IMemoryCache>();
 | 
			
		||||
        Options = App.RootServices.GetRequiredService<IOptions<BootstrapBlazorOptions>>().Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -236,7 +235,7 @@ internal class CacheManager
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static IStringLocalizer? CreateLocalizerByType(Type resourceSource) => resourceSource.Assembly.IsDynamic
 | 
			
		||||
        ? null
 | 
			
		||||
        : Instance.Provider.GetRequiredService<IStringLocalizerFactory>().Create(resourceSource);
 | 
			
		||||
        : Provider.GetRequiredService<IStringLocalizerFactory>().Create(resourceSource);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获得 <see cref="JsonLocalizationOptions"/> 值
 | 
			
		||||
@@ -244,7 +243,7 @@ internal class CacheManager
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private static JsonLocalizationOptions GetJsonLocalizationOption()
 | 
			
		||||
    {
 | 
			
		||||
        var localizationOptions = Instance.Provider.GetRequiredService<IOptions<JsonLocalizationOptions>>();
 | 
			
		||||
        var localizationOptions = Provider.GetRequiredService<IOptions<JsonLocalizationOptions>>();
 | 
			
		||||
        return localizationOptions.Value;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -253,7 +252,7 @@ internal class CacheManager
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    private static BootstrapBlazorOptions GetBootstrapBlazorOption()
 | 
			
		||||
    {
 | 
			
		||||
        var localizationOptions = Instance.Provider.GetRequiredService<IOptions<BootstrapBlazorOptions>>();
 | 
			
		||||
        var localizationOptions = Provider.GetRequiredService<IOptions<BootstrapBlazorOptions>>();
 | 
			
		||||
        return localizationOptions.Value;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -269,7 +268,7 @@ internal class CacheManager
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        IStringLocalizer? ret = null;
 | 
			
		||||
        var factories = Instance.Provider.GetServices<IStringLocalizerFactory>();
 | 
			
		||||
        var factories = Provider.GetServices<IStringLocalizerFactory>();
 | 
			
		||||
        var factory = factories.LastOrDefault(a => a is not JsonStringLocalizerFactory);
 | 
			
		||||
        if (factory != null)
 | 
			
		||||
        {
 | 
			
		||||
@@ -345,7 +344,7 @@ internal class CacheManager
 | 
			
		||||
    /// <param name="typeName"></param>
 | 
			
		||||
    /// <param name="includeParentCultures"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static IEnumerable<LocalizedString> GetTypeStringsFromResolve(string typeName, bool includeParentCultures = true) => Instance.Provider.GetRequiredService<ILocalizationResolve>().GetAllStringsByType(typeName, includeParentCultures);
 | 
			
		||||
    public static IEnumerable<LocalizedString> GetTypeStringsFromResolve(string typeName, bool includeParentCultures = true) => Provider.GetRequiredService<ILocalizationResolve>().GetAllStringsByType(typeName, includeParentCultures);
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region DisplayName
 | 
			
		||||
 
 | 
			
		||||
@@ -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}");
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,11 +27,11 @@ public class WebsiteOptions : IConfigurableOptions
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Demo { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool WebPageEnable { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    public int MaxBlazorConnections { get; set; } = 5;
 | 
			
		||||
    public bool BlazorConnectionLimitEnable { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否显示关于页面
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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.0" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.11.2" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,13 +23,19 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
 | 
			
		||||
    [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
 | 
			
		||||
    [IgnoreExcel]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, IsVisibleWhenEdit = false, IsVisibleWhenAdd = false, Sortable = true, DefaultSort = true, DefaultSortOrder = SortOrder.Asc)]
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    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)
 | 
			
		||||
@@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions
 | 
			
		||||
        };
 | 
			
		||||
        var items = datas.GetData(option, out var totalCount, where);
 | 
			
		||||
        ret.TotalCount = totalCount;
 | 
			
		||||
 | 
			
		||||
        if (totalCount > 0)
 | 
			
		||||
        {
 | 
			
		||||
            if (!items.Any() && option.PageIndex != 1)
 | 
			
		||||
            {
 | 
			
		||||
                option.PageIndex = 1;
 | 
			
		||||
                items = datas.GetData(option, out totalCount, where);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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": "更新人"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -46,7 +46,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
 | 
			
		||||
    public async Task<bool> DeleteAsync(IEnumerable<T> models)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
 | 
			
		||||
        return await db.Deleteable<T>(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -140,18 +140,22 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
 | 
			
		||||
            return (await db.UpdateableT(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual async Task<bool> SaveAsync(List<T> model, ItemChangedType changedType)
 | 
			
		||||
    {
 | 
			
		||||
        return (await SaveReturnCountAsync(model, changedType).ConfigureAwait(false)) > 0;
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public async Task<int> SaveReturnCountAsync(List<T> model, ItemChangedType changedType)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        if (changedType == ItemChangedType.Add)
 | 
			
		||||
        {
 | 
			
		||||
            return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
            return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
 | 
			
		||||
            return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -27,18 +27,27 @@ using System.Security.Claims;
 | 
			
		||||
using ThingsGateway.ConfigurableOptions;
 | 
			
		||||
using ThingsGateway.NewLife.Caching;
 | 
			
		||||
using ThingsGateway.NewLife.Collections;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
using ThingsGateway.NewLife.Log;
 | 
			
		||||
using ThingsGateway.Reflection;
 | 
			
		||||
using ThingsGateway.Templates;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public static class WebEnableVariable
 | 
			
		||||
{
 | 
			
		||||
    public static bool WebEnable => Environment.GetEnvironmentVariable(nameof(WebEnable)).ToBoolean(true);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 全局应用类
 | 
			
		||||
/// </summary>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public static class App
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 私有设置,避免重复解析
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -157,7 +166,7 @@ public static class App
 | 
			
		||||
    var httpContextAccessor = RootServices?.GetService<IHttpContextAccessor>();
 | 
			
		||||
    try
 | 
			
		||||
    {
 | 
			
		||||
        return httpContextAccessor.HttpContext;
 | 
			
		||||
        return httpContextAccessor?.HttpContext;
 | 
			
		||||
    }
 | 
			
		||||
    catch
 | 
			
		||||
    {
 | 
			
		||||
@@ -545,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -85,11 +85,14 @@ internal static class InternalApp
 | 
			
		||||
            // 存储根服务(解决 Web 主机还未启动时在 HostedService 中使用 App.GetService 问题
 | 
			
		||||
            services.AddHostedService<GenericHostLifetimeEventsHostedService>();
 | 
			
		||||
 | 
			
		||||
            // 注册 Startup 过滤器
 | 
			
		||||
            services.AddTransient<IStartupFilter, StartupFilter>();
 | 
			
		||||
            if (WebEnableVariable.WebEnable == true)
 | 
			
		||||
            {
 | 
			
		||||
                // 注册 Startup 过滤器
 | 
			
		||||
                services.AddTransient<IStartupFilter, StartupFilter>();
 | 
			
		||||
 | 
			
		||||
            // 注册 HttpContextAccessor 服务
 | 
			
		||||
            services.AddHttpContextAccessor();
 | 
			
		||||
                // 注册 HttpContextAccessor 服务
 | 
			
		||||
                services.AddHttpContextAccessor();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 初始化应用服务
 | 
			
		||||
            services.AddApp();
 | 
			
		||||
@@ -212,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[]>()
 | 
			
		||||
@@ -237,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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ namespace ThingsGateway;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions>
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否启用规范化文档
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -50,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>
 | 
			
		||||
@@ -66,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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										341
									
								
								src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,341 @@
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
// 版权信息
 | 
			
		||||
// 版权归百小僧及百签科技(广东)有限公司所有。
 | 
			
		||||
// 所有权利保留。
 | 
			
		||||
// 官方网站:https://baiqian.com
 | 
			
		||||
//
 | 
			
		||||
// 许可证信息
 | 
			
		||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
 | 
			
		||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
 | 
			
		||||
// ------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.AspNetCore.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway;
 | 
			
		||||
 | 
			
		||||
namespace System;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// <see cref="WebApplication"/> 方式配置选项
 | 
			
		||||
/// </summary>
 | 
			
		||||
[SuppressSniffer]
 | 
			
		||||
public sealed class MiniRunOptions : IRunOptions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 内部构造函数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal MiniRunOptions()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认配置
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static MiniRunOptions Default { get; } = new MiniRunOptions();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认配置(带启动参数)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static MiniRunOptions Main(string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        return Default.WithArgs(args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认配置(静默启动)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static MiniRunOptions DefaultSilence { get; } = new MiniRunOptions().Silence();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认配置(静默启动 + 启动参数)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static MiniRunOptions MainSilence(string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        return DefaultSilence.WithArgs(args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="WebApplicationOptions"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="options"></param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureOptions(WebApplicationOptions options)
 | 
			
		||||
    {
 | 
			
		||||
        Options = options;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="IWebHostBuilder"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction"></param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureBuilder(Action<IWebHostBuilder> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        ActionBuilder = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="IHostBuilder"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction"></param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureFirstActionBuilder(Action<IHostBuilder> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        FirstActionBuilder = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="IServiceCollection"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction"></param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureServices(Action<IServiceCollection> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        ActionServices = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="InjectOptions"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction"></param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureInject(Action<IWebHostBuilder, InjectOptions> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        ActionInject = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="WebApplication"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction">配置委托</param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions Configure(Action<IHost> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        ActionConfigure = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置 <see cref="ConfigurationManager"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="configureAction">配置委托</param>
 | 
			
		||||
    /// <returns><see cref="MiniRunOptions"/></returns>
 | 
			
		||||
    public MiniRunOptions ConfigureConfiguration(Action<IHostEnvironment, IConfiguration> configureAction)
 | 
			
		||||
    {
 | 
			
		||||
        ActionConfigurationManager = configureAction;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用服务组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddComponent<TComponent>()
 | 
			
		||||
        where TComponent : class, IServiceComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        ServiceComponents.Add(typeof(TComponent), null);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用服务组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <typeparam name="TComponentOptions"></typeparam>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddComponent<TComponent, TComponentOptions>(TComponentOptions options)
 | 
			
		||||
        where TComponent : class, IServiceComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        ServiceComponents.Add(typeof(TComponent), options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用服务组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="componentType">组件类型</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddComponent(Type componentType, object options)
 | 
			
		||||
    {
 | 
			
		||||
        ServiceComponents.Add(componentType, options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用中间件组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions UseComponent<TComponent>()
 | 
			
		||||
        where TComponent : class, IApplicationComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        ApplicationComponents.Add(typeof(TComponent), null);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用中间件组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <typeparam name="TComponentOptions"></typeparam>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions UseComponent<TComponent, TComponentOptions>(TComponentOptions options)
 | 
			
		||||
        where TComponent : class, IApplicationComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        ApplicationComponents.Add(typeof(TComponent), options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加应用中间件组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="componentType">组件类型</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions UseComponent(Type componentType, object options)
 | 
			
		||||
    {
 | 
			
		||||
        ApplicationComponents.Add(componentType, options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加 IWebHostBuilder 组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddWebComponent<TComponent>()
 | 
			
		||||
        where TComponent : class, IWebComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        WebComponents.Add(typeof(TComponent), null);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加 IWebHostBuilder 组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TComponent">组件类型</typeparam>
 | 
			
		||||
    /// <typeparam name="TComponentOptions"></typeparam>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddWebComponent<TComponent, TComponentOptions>(TComponentOptions options)
 | 
			
		||||
        where TComponent : class, IWebComponent, new()
 | 
			
		||||
    {
 | 
			
		||||
        WebComponents.Add(typeof(TComponent), options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 添加 IWebHostBuilder 组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="componentType">组件类型</param>
 | 
			
		||||
    /// <param name="options">组件参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions AddWebComponent(Type componentType, object options)
 | 
			
		||||
    {
 | 
			
		||||
        WebComponents.Add(componentType, options);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 标识主机静默启动
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>不阻塞程序运行</remarks>
 | 
			
		||||
    /// <param name="silence">静默启动</param>
 | 
			
		||||
    /// <param name="logging">静默启动日志状态,默认 false</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions Silence(bool silence = true, bool logging = false)
 | 
			
		||||
    {
 | 
			
		||||
        IsSilence = silence;
 | 
			
		||||
        SilenceLogging = logging;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置进程启动参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="args">启动参数</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public MiniRunOptions WithArgs(string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        Args = args;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// <see cref="WebApplicationOptions"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal WebApplicationOptions Options { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="IServiceCollection"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IServiceCollection> ActionServices { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="IWebHostBuilder"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IHostBuilder> FirstActionBuilder { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="IWebHostBuilder"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IWebHostBuilder> ActionBuilder { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="InjectOptions"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IWebHostBuilder, InjectOptions> ActionInject { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="IHost"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IHost> ActionConfigure { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 自定义 <see cref="IConfiguration"/> 委托
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Action<IHostEnvironment, IConfiguration> ActionConfigurationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 应用服务组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Dictionary<Type, object> ServiceComponents { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// IWebHostBuilder 组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Dictionary<Type, object> WebComponents { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 应用中间件组件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal Dictionary<Type, object> ApplicationComponents { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 静默启动
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>不阻塞程序运行</remarks>
 | 
			
		||||
    internal bool IsSilence { get; private set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 静默启动日志状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal bool SilenceLogging { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 命令行参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal string[] Args { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -602,6 +602,33 @@ public static class Serve
 | 
			
		||||
        return app;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 启动 WebApplication 主机
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <remarks>未包含 Web 基础功能,需手动注册服务/中间件</remarks>
 | 
			
		||||
    /// <param name="options">配置选项</param>
 | 
			
		||||
    /// <param name="urls">默认 5000/5001 端口</param>
 | 
			
		||||
    /// <param name="cancellationToken"></param>
 | 
			
		||||
    /// <returns><see cref="IHost"/></returns>
 | 
			
		||||
    public static async Task<IHost> RunAsync(MiniRunOptions options, string urls = default, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        // 构建 WebApplication 对象
 | 
			
		||||
        BuildMiniApplication(options, urls, out var app);
 | 
			
		||||
 | 
			
		||||
        // 是否静默启动
 | 
			
		||||
        if (!options.IsSilence)
 | 
			
		||||
        {
 | 
			
		||||
            // 配置启动地址和端口
 | 
			
		||||
            await app.RunAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await app.StartAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return app;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建 WebApplication 对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -616,8 +643,8 @@ public static class Serve
 | 
			
		||||
 | 
			
		||||
        // 初始化 WebApplicationBuilder
 | 
			
		||||
        var builder = (options.Options == null
 | 
			
		||||
            ? WebApplication.CreateBuilder(args)
 | 
			
		||||
            : WebApplication.CreateBuilder(options.Options));
 | 
			
		||||
           ? WebApplication.CreateBuilder(args)
 | 
			
		||||
           : WebApplication.CreateBuilder(options.Options));
 | 
			
		||||
 | 
			
		||||
        // 调用自定义配置服务
 | 
			
		||||
        options?.FirstActionBuilder?.Invoke(builder);
 | 
			
		||||
@@ -674,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);
 | 
			
		||||
 | 
			
		||||
@@ -793,12 +820,138 @@ 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建 IHost 对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="options">配置选项</param>
 | 
			
		||||
    /// <param name="urls">默认 5000/5001 端口</param>
 | 
			
		||||
    /// <param name="app"><see cref="IHost"/></param>
 | 
			
		||||
    public static void BuildMiniApplication(MiniRunOptions options, string urls, out IHost app)
 | 
			
		||||
    {
 | 
			
		||||
        // 获取命令行参数
 | 
			
		||||
        var args = options.Args ?? Environment.GetCommandLineArgs().Skip(1).ToArray();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var builder = Host.CreateDefaultBuilder(args);
 | 
			
		||||
 | 
			
		||||
        // 静默启动排除指定日志类名
 | 
			
		||||
        if (options.IsSilence && !options.SilenceLogging)
 | 
			
		||||
        {
 | 
			
		||||
            builder = builder.ConfigureLogging(logging =>
 | 
			
		||||
            {
 | 
			
		||||
                logging.AddFilter((provider, category, logLevel) => !SilenceExcludesOfLogCategoryName.Any(u => category.StartsWith(u)));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // 配置 Web 主机
 | 
			
		||||
        builder = builder.ConfigureWebHost(webHostBuilder =>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 调用自定义配置服务
 | 
			
		||||
            options?.FirstActionBuilder?.Invoke(builder);
 | 
			
		||||
 | 
			
		||||
            // 注册 WebApplicationBuilder 组件
 | 
			
		||||
            if (options.WebComponents.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var (componentType, opt) in options.WebComponents)
 | 
			
		||||
                {
 | 
			
		||||
                    webHostBuilder.AddWebComponent(componentType, opt);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            webHostBuilder.Configure((WebHostBuilderContext app, IApplicationBuilder applicationBuilder) =>
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                // 添加自定义配置
 | 
			
		||||
                options.ActionConfigurationManager?.Invoke(app.HostingEnvironment, app.Configuration);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // 初始化框架
 | 
			
		||||
            webHostBuilder.Inject(options.ActionInject);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 配置服务
 | 
			
		||||
            if (options.ServiceComponents.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                webHostBuilder = webHostBuilder.ConfigureServices(services =>
 | 
			
		||||
                {
 | 
			
		||||
                    // 注册应用服务组件
 | 
			
		||||
                    foreach (var (componentType, opt) in options.ServiceComponents)
 | 
			
		||||
                    {
 | 
			
		||||
                        services.AddComponent(componentType, opt);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 配置启动地址和端口
 | 
			
		||||
            var startUrls = !string.IsNullOrWhiteSpace(urls) ? urls : webHostBuilder.GetSetting(nameof(urls));
 | 
			
		||||
 | 
			
		||||
            // 自定义启动端口
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(startUrls))
 | 
			
		||||
            {
 | 
			
		||||
                webHostBuilder = webHostBuilder.UseUrls(startUrls);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 调用自定义配置
 | 
			
		||||
            options?.ActionBuilder?.Invoke(webHostBuilder);
 | 
			
		||||
 | 
			
		||||
            // 配置中间件
 | 
			
		||||
            if (options.ApplicationComponents.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                webHostBuilder = webHostBuilder.Configure((context, app) =>
 | 
			
		||||
                {
 | 
			
		||||
                    // 注册应用中间件组件
 | 
			
		||||
                    foreach (var (componentType, opt) in options.ApplicationComponents)
 | 
			
		||||
                    {
 | 
			
		||||
                        app.UseComponent(context.HostingEnvironment, componentType, opt);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        builder = builder.ConfigureServices(services =>
 | 
			
		||||
        {
 | 
			
		||||
            // 调用自定义配置服务
 | 
			
		||||
            options?.ActionServices?.Invoke(services);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 构建主机
 | 
			
		||||
        app = builder.Build();
 | 
			
		||||
 | 
			
		||||
        InternalApp.RootServices ??= app.Services;
 | 
			
		||||
 | 
			
		||||
        var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
 | 
			
		||||
 | 
			
		||||
        applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
 | 
			
		||||
        // 配置所有 Startup Configure
 | 
			
		||||
        UseStartups(app.Services);
 | 
			
		||||
        // 释放内存
 | 
			
		||||
        App.AppStartups.Clear();
 | 
			
		||||
        // 调用自定义配置
 | 
			
		||||
        options?.ActionConfigure?.Invoke(app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建 IHost 对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -852,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)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,8 @@ public sealed class DatabaseLogger : ILogger, IDisposable
 | 
			
		||||
        // 设置日志消息模板
 | 
			
		||||
        logMsg.Message = _options.MessageFormat != null
 | 
			
		||||
            ? _options.MessageFormat(logMsg)
 | 
			
		||||
            : Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
 | 
			
		||||
            : string.Empty;
 | 
			
		||||
        //: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
 | 
			
		||||
 | 
			
		||||
        // 空检查
 | 
			
		||||
        if (logMsg.Message is null)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        {
 | 
			
		||||
            // 解析存储的验证信息
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user