Compare commits
	
		
			65 Commits
		
	
	
		
			10.9.64.0
			...
			10.11.14.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | ||
| 
						 | 
					e785f6660c | ||
| 
						 | 
					831c611797 | ||
| 
						 | 
					453817ef86 | ||
| 
						 | 
					8ce0b981c1 | ||
| 
						 | 
					4e5c51b54c | ||
| 
						 | 
					3cc9d31f28 | ||
| 
						 | 
					10391f869b | ||
| 
						 | 
					fba0723a6d | ||
| 
						 | 
					2db3f78f0c | ||
| 
						 | 
					badf61fe01 | ||
| 
						 | 
					d74e0952dc | ||
| 
						 | 
					fb1699ce80 | ||
| 
						 | 
					44adddbcd4 | ||
| 
						 | 
					0eab889452 | ||
| 
						 | 
					e14d39a459 | ||
| 
						 | 
					7575264ede | ||
| 
						 | 
					3e1a077b96 | ||
| 
						 | 
					a921cb8400 | ||
| 
						 | 
					4de7c31ed7 | ||
| 
						 | 
					08326a2cfd | ||
| 
						 | 
					e045de5acb | ||
| 
						 | 
					d3bef31aa6 | ||
| 
						 | 
					347d4d6e5d | ||
| 
						 | 
					b006a066e1 | ||
| 
						 | 
					e331bc5d3c | ||
| 
						 | 
					2e81806231 | ||
| 
						 | 
					37b48cf221 | ||
| 
						 | 
					5997487434 | ||
| 
						 | 
					4ce843182f | ||
| 
						 | 
					a4aa000cf0 | ||
| 
						 | 
					01aa6ca066 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -364,6 +364,8 @@ FodyWeavers.xsd
 | 
			
		||||
 | 
			
		||||
/src/*Pro*/
 | 
			
		||||
/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>
 | 
			
		||||
		
 | 
			
		||||
		<!-- 可选 -->
 | 
			
		||||
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -122,6 +122,7 @@ using Microsoft.AspNetCore.Components;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1849
 | 
			
		||||
#pragma warning disable CA2007
 | 
			
		||||
#pragma warning disable CS0162
 | 
			
		||||
#pragma warning disable CS8632
 | 
			
		||||
@@ -276,6 +277,7 @@ namespace {namespaceName}
 | 
			
		||||
#pragma warning restore CS8632
 | 
			
		||||
#pragma warning restore CS0162
 | 
			
		||||
#pragma warning restore CA2007
 | 
			
		||||
#pragma warning restore CA1849
 | 
			
		||||
");
 | 
			
		||||
        var bases = class_symbol.GetTypeHierarchy().Where(t => !SymbolEqualityComparer.Default.Equals(t, class_symbol));
 | 
			
		||||
        var members = class_symbol.GetMembers() // members of the type itself
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
 | 
			
		||||
<div align="center"><h1 align="center">ThingsBlazor</a></h1></div>
 | 
			
		||||
<div align="center"><h1 align="center">ThingsBlazor</h1></div>
 | 
			
		||||
<div align="center"><h3 align="center">权限管理框架</h3></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,9 +38,9 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
 | 
			
		||||
    static OperDescAttribute()
 | 
			
		||||
    {
 | 
			
		||||
        // 创建长时间运行的后台任务,并将日志消息队列中数据写入存储中
 | 
			
		||||
        Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
 | 
			
		||||
        AppService = App.RootServices.GetService<IAppService>();
 | 
			
		||||
        // 创建长时间运行的后台任务,并将日志消息队列中数据写入存储中
 | 
			
		||||
        Task.Factory.StartNew(ProcessQueueAsync, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public OperDescAttribute(string description, bool isRecordPar = true, object localizerType = null)
 | 
			
		||||
@@ -93,7 +93,7 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将日志消息写入数据库中
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private static async Task ProcessQueue()
 | 
			
		||||
    private static async Task ProcessQueueAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var appLifetime = App.RootServices!.GetService<IHostApplicationLifetime>()!;
 | 
			
		||||
        while (!appLifetime.ApplicationStopping.IsCancellationRequested)
 | 
			
		||||
 
 | 
			
		||||
@@ -34,17 +34,20 @@ public class FileController : ControllerBase
 | 
			
		||||
            return BadRequest("Invalid file name.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", fileName);
 | 
			
		||||
 | 
			
		||||
        if (!System.IO.File.Exists(filePath))
 | 
			
		||||
        var root = Directory.GetCurrentDirectory();
 | 
			
		||||
        var wwwroot = Path.Combine(root, "wwwroot");
 | 
			
		||||
        var filePath = Path.Combine(wwwroot, fileName);
 | 
			
		||||
#pragma warning disable CA3003
 | 
			
		||||
        if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
#pragma warning restore CA3003
 | 
			
		||||
 | 
			
		||||
        var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 | 
			
		||||
 | 
			
		||||
        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,6 +38,7 @@ 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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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": "用户登录已过期,请重新登录"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class USheetDatas
 | 
			
		||||
{
 | 
			
		||||
@@ -30,7 +30,7 @@ public class AdminOAuthHandler<TOptions>(
 | 
			
		||||
{
 | 
			
		||||
    static AdminOAuthHandler()
 | 
			
		||||
    {
 | 
			
		||||
        Task.Factory.StartNew(Insertable, TaskCreationOptions.LongRunning);
 | 
			
		||||
        Task.Factory.StartNew(InsertableAsync, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -41,7 +41,7 @@ public class AdminOAuthHandler<TOptions>(
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建访问日志
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private static async Task Insertable()
 | 
			
		||||
    private static async Task InsertableAsync()
 | 
			
		||||
    {
 | 
			
		||||
        var db = DbContext.GetDB<SysOperateLog>();
 | 
			
		||||
        var appLifetime = App.RootServices!.GetService<IHostApplicationLifetime>()!;
 | 
			
		||||
 
 | 
			
		||||
@@ -128,18 +128,19 @@ internal sealed class SysOrgService : BaseService<SysOrg>, ISysOrgService
 | 
			
		||||
    [OperDesc("DeleteOrg")]
 | 
			
		||||
    public async Task<bool> DeleteOrgAsync(IEnumerable<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        var sysDeleteOrgList = new List<long>();//需要删除的组织ID集合
 | 
			
		||||
        var sysOrgList = await GetAllAsync().ConfigureAwait(false);//获取所有组织
 | 
			
		||||
        foreach (var it in ids)
 | 
			
		||||
        {
 | 
			
		||||
            var children = SysOrgService.GetSysOrgChildren(sysOrgList, it);//查找下级组织
 | 
			
		||||
            sysDeleteOrgList.AddRange(children.Select(it => it.Id).ToList());
 | 
			
		||||
            sysDeleteOrgList.Add(it);
 | 
			
		||||
        }
 | 
			
		||||
        //获取所有ID
 | 
			
		||||
        if (ids.Any())
 | 
			
		||||
        if (sysDeleteOrgList.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            using var db = GetDB();
 | 
			
		||||
            var sysOrgList = await GetAllAsync().ConfigureAwait(false);//获取所有组织
 | 
			
		||||
            var sysDeleteOrgList = new List<long>();//需要删除的组织ID集合
 | 
			
		||||
            foreach (var it in ids)
 | 
			
		||||
            {
 | 
			
		||||
                var children = SysOrgService.GetSysOrgChildren(sysOrgList, it);//查找下级组织
 | 
			
		||||
                sysDeleteOrgList.AddRange(children.Select(it => it.Id).ToList());
 | 
			
		||||
                sysDeleteOrgList.Add(it);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //如果组织下有用户则不能删除
 | 
			
		||||
            if (await db.Queryable<SysUser>().AnyAsync(it => sysDeleteOrgList.Contains(it.OrgId)).ConfigureAwait(false))
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ public class SysPositionService : BaseService<SysPosition>, ISysPositionService
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var dels = (await GetAllAsync().ConfigureAwait(false)).Where(a => ids.Contains(a.Id));
 | 
			
		||||
            await SysUserService.CheckApiDataScopeAsync(dels.Select(a => a.OrgId).ToList(), dels.Select(a => a.CreateUserId).ToList()).ConfigureAwait(false);
 | 
			
		||||
            await SysUserService.CheckApiDataScopeAsync(dels.Select(a => a.OrgId), dels.Select(a => a.CreateUserId)).ConfigureAwait(false);
 | 
			
		||||
            //删除职位
 | 
			
		||||
            var result = await base.DeleteAsync(ids).ConfigureAwait(false);
 | 
			
		||||
            if (result)
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ public interface ISysResourceService
 | 
			
		||||
    /// <param name="resourceList">资源列表</param>
 | 
			
		||||
    /// <param name="parentId">父ID</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    IEnumerable<SysResource> ConstructMenuTrees(IEnumerable<SysResource> resourceList, long parentId = 0);
 | 
			
		||||
    IEnumerable<SysResource> ConstructMenuTrees(List<SysResource> resourceList, long parentId = 0);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 复制资源到其他模块
 | 
			
		||||
@@ -44,7 +44,7 @@ public interface ISysResourceService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ids">id列表</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<bool> DeleteResourceAsync(IEnumerable<long> ids);
 | 
			
		||||
    Task<bool> DeleteResourceAsync(HashSet<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库读取全部资源列表
 | 
			
		||||
 
 | 
			
		||||
@@ -75,10 +75,10 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
 | 
			
		||||
    /// <param name="ids">id列表</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [OperDesc("DeleteResource")]
 | 
			
		||||
    public async Task<bool> DeleteResourceAsync(IEnumerable<long> ids)
 | 
			
		||||
    public async Task<bool> DeleteResourceAsync(HashSet<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        //删除
 | 
			
		||||
        if (ids.Any())
 | 
			
		||||
        if (ids.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            //获取所有菜单和按钮
 | 
			
		||||
            var resourceList = await GetAllAsync().ConfigureAwait(false);
 | 
			
		||||
@@ -86,10 +86,11 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
 | 
			
		||||
            var delSysResources = resourceList.Where(it => ids.Contains(it.Id));
 | 
			
		||||
            //找到要删除的模块
 | 
			
		||||
            var delModules = resourceList.Where(a => a.Category == ResourceCategoryEnum.Module).Where(it => ids.Contains(it.Id));
 | 
			
		||||
            if (delModules.Any())
 | 
			
		||||
 | 
			
		||||
            //获取模块下的所有列表
 | 
			
		||||
            var delHashSet = delModules.Select(a => a.Id).ToHashSet();
 | 
			
		||||
            if (delHashSet.Count != 0)
 | 
			
		||||
            {
 | 
			
		||||
                //获取模块下的所有列表
 | 
			
		||||
                var delHashSet = delModules.Select(a => a.Id).ToHashSet();
 | 
			
		||||
                var delModuleResources = resourceList.Where(it => delHashSet.Contains(it.Module));
 | 
			
		||||
                delSysResources = delSysResources.Concat(delModuleResources).ToHashSet();
 | 
			
		||||
            }
 | 
			
		||||
@@ -345,17 +346,14 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IEnumerable<SysResource> ConstructMenuTrees(IEnumerable<SysResource> resourceList, long parentId = 0)
 | 
			
		||||
    public IEnumerable<SysResource> ConstructMenuTrees(List<SysResource> resourceList, long parentId = 0)
 | 
			
		||||
    {
 | 
			
		||||
        //找下级资源ID列表
 | 
			
		||||
        var resources = resourceList.Where(it => it.ParentId == parentId).OrderBy(it => it.SortCode);
 | 
			
		||||
        if (resources.Any())//如果数量大于0
 | 
			
		||||
        foreach (var item in resources)//遍历资源
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var item in resources)//遍历资源
 | 
			
		||||
            {
 | 
			
		||||
                var children = ConstructMenuTrees(resourceList, item.Id).ToList();//添加子节点
 | 
			
		||||
                item.Children = children.Count > 0 ? children : null;
 | 
			
		||||
            }
 | 
			
		||||
            var children = ConstructMenuTrees(resourceList, item.Id).ToList();//添加子节点
 | 
			
		||||
            item.Children = children.Count > 0 ? children : null;
 | 
			
		||||
        }
 | 
			
		||||
        return resources;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public interface ISysRoleService
 | 
			
		||||
    /// 删除角色
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ids">id列表</param>
 | 
			
		||||
    Task<bool> DeleteRoleAsync(IEnumerable<long> ids);
 | 
			
		||||
    Task<bool> DeleteRoleAsync(HashSet<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从缓存/数据库获取全部角色信息
 | 
			
		||||
@@ -43,7 +43,7 @@ public interface ISysRoleService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="input">角色id列表</param>
 | 
			
		||||
    /// <returns>角色列表</returns>
 | 
			
		||||
    Task<IEnumerable<SysRole>> GetRoleListByIdListAsync(IEnumerable<long> input);
 | 
			
		||||
    Task<IEnumerable<SysRole>> GetRoleListByIdListAsync(HashSet<long> input);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 根据用户id获取角色列表
 | 
			
		||||
 
 | 
			
		||||
@@ -61,42 +61,46 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
 | 
			
		||||
        var topOrgList = sysOrgList.Where(it => it.ParentId == 0);//获取顶级机构
 | 
			
		||||
        var globalRole = sysRoles.Where(it => it.Category == RoleCategoryEnum.Global);//获取全局角色
 | 
			
		||||
        if (globalRole.Any())
 | 
			
		||||
        var children = globalRole.Select(it => new RoleTreeOutput
 | 
			
		||||
        {
 | 
			
		||||
            result.Add(new RoleTreeOutput()
 | 
			
		||||
            {
 | 
			
		||||
                Id = CommonUtils.GetSingleId(),
 | 
			
		||||
                Name = Localizer["Global"],
 | 
			
		||||
                Children = globalRole.Select(it => new RoleTreeOutput
 | 
			
		||||
                {
 | 
			
		||||
                    Id = it.Id,
 | 
			
		||||
                    Name = it.Name,
 | 
			
		||||
                    IsRole = true
 | 
			
		||||
                }).ToList()
 | 
			
		||||
            });//添加全局角色
 | 
			
		||||
        }
 | 
			
		||||
            Id = it.Id,
 | 
			
		||||
            Name = it.Name,
 | 
			
		||||
            IsRole = true
 | 
			
		||||
        }).ToList();
 | 
			
		||||
 | 
			
		||||
        result.Add(new RoleTreeOutput()
 | 
			
		||||
        {
 | 
			
		||||
            Id = CommonUtils.GetSingleId(),
 | 
			
		||||
            Name = Localizer["Global"],
 | 
			
		||||
            Children = children
 | 
			
		||||
        });//添加全局角色
 | 
			
		||||
        //遍历顶级机构
 | 
			
		||||
        foreach (var org in topOrgList)
 | 
			
		||||
        {
 | 
			
		||||
            var childIds = await _sysOrgService.GetOrgChildIdsAsync(org.Id, true, sysOrgList).ConfigureAwait(false);//获取机构下的所有子级ID
 | 
			
		||||
            var childRoles = sysRoles.Where(it => it.OrgId != 0 && childIds.Contains(it.OrgId));//获取机构下的所有角色
 | 
			
		||||
            if (childRoles.Any())
 | 
			
		||||
 | 
			
		||||
            List<RoleTreeOutput> childrenRoleTreeOutputs = new();
 | 
			
		||||
 | 
			
		||||
            foreach (var it in childRoles)
 | 
			
		||||
            {
 | 
			
		||||
                childrenRoleTreeOutputs.Add(new RoleTreeOutput()
 | 
			
		||||
                {
 | 
			
		||||
                    Id = it.Id,
 | 
			
		||||
                    Name = it.Name,
 | 
			
		||||
                    IsRole = true
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            if (childrenRoleTreeOutputs.Count > 0)
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
                var roleTreeOutput = new RoleTreeOutput
 | 
			
		||||
                {
 | 
			
		||||
                    Id = org.Id,
 | 
			
		||||
                    Name = org.Name,
 | 
			
		||||
                    IsRole = false
 | 
			
		||||
                    IsRole = false,
 | 
			
		||||
                    Children = childrenRoleTreeOutputs
 | 
			
		||||
                };//实例化角色树
 | 
			
		||||
                foreach (var it in childRoles)
 | 
			
		||||
                {
 | 
			
		||||
                    roleTreeOutput.Children.Add(new RoleTreeOutput()
 | 
			
		||||
                    {
 | 
			
		||||
                        Id = it.Id,
 | 
			
		||||
                        Name = it.Name,
 | 
			
		||||
                        IsRole = true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                result.Add(roleTreeOutput);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -147,7 +151,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="input">角色id列表</param>
 | 
			
		||||
    /// <returns>角色列表</returns>
 | 
			
		||||
    public async Task<IEnumerable<SysRole>> GetRoleListByIdListAsync(IEnumerable<long> input)
 | 
			
		||||
    public async Task<IEnumerable<SysRole>> GetRoleListByIdListAsync(HashSet<long> input)
 | 
			
		||||
    {
 | 
			
		||||
        var roles = await GetAllAsync().ConfigureAwait(false);
 | 
			
		||||
        var roleList = roles.Where(it => input.Contains(it.Id));
 | 
			
		||||
@@ -162,7 +166,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ids">id列表</param>
 | 
			
		||||
    [OperDesc("DeleteRole")]
 | 
			
		||||
    public async Task<bool> DeleteRoleAsync(IEnumerable<long> ids)
 | 
			
		||||
    public async Task<bool> DeleteRoleAsync(HashSet<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        var sysRoles = await GetAllAsync().ConfigureAwait(false);//获取所有角色
 | 
			
		||||
        var hasSuperAdmin = sysRoles.Any(it => it.Id == RoleConst.SuperAdminRoleId && ids.Contains(it.Id));//判断是否有超级管理员
 | 
			
		||||
@@ -170,10 +174,10 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
            throw Oops.Bah(Localizer["CanotDeleteAdmin"]);
 | 
			
		||||
 | 
			
		||||
        var dels = (await GetAllAsync().ConfigureAwait(false)).Where(a => ids.Contains(a.Id));
 | 
			
		||||
        await SysUserService.CheckApiDataScopeAsync(dels.Select(a => a.OrgId).ToList(), dels.Select(a => a.CreateUserId).ToList()).ConfigureAwait(false);
 | 
			
		||||
        await SysUserService.CheckApiDataScopeAsync(dels.Select(a => a.OrgId), dels.Select(a => a.CreateUserId)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        //数据库是string所以这里转下
 | 
			
		||||
        var targetIds = ids.Select(it => it.ToString());
 | 
			
		||||
        var targetIds = ids.Select(it => it.ToString()).ToList();
 | 
			
		||||
        //定义删除的关系
 | 
			
		||||
        var delRelations = new List<RelationCategoryEnum> {
 | 
			
		||||
            RelationCategoryEnum.RoleHasResource,
 | 
			
		||||
@@ -184,7 +188,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
        //事务
 | 
			
		||||
        var result = await db.UseTranAsync(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            await db.Deleteable<SysRole>().In(ids.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);//删除
 | 
			
		||||
            await db.Deleteable<SysRole>().In(ids).ExecuteCommandHasChangeAsync().ConfigureAwait(false);//删除
 | 
			
		||||
            //删除关系表角色与资源关系,角色与权限关系
 | 
			
		||||
            await db.Deleteable<SysRelation>(it => ids.Contains(it.ObjectId) && delRelations.Contains(it.Category)).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
            //删除关系表角色与用户关系
 | 
			
		||||
@@ -317,7 +321,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
 | 
			
		||||
            #region 角色权限处理.
 | 
			
		||||
            var defaultDataScope = sysRole.DefaultDataScope;//获取默认数据范围
 | 
			
		||||
 | 
			
		||||
            if (menusList.Any())
 | 
			
		||||
            if (relationRoles.Count != 0)
 | 
			
		||||
            {
 | 
			
		||||
                //获取权限授权树
 | 
			
		||||
                var permissions = App.GetService<IApiPermissionService>().PermissionTreeSelector(menusList.Select(it => it.Href));
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主键Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public override long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ public interface ISysUserService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ids">用户ID列表。</param>
 | 
			
		||||
    /// <returns>是否删除成功。</returns>
 | 
			
		||||
    Task<bool> DeleteUserAsync(IEnumerable<long> ids);
 | 
			
		||||
    Task<bool> DeleteUserAsync(HashSet<long> ids);
 | 
			
		||||
 | 
			
		||||
    /// <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);
 | 
			
		||||
@@ -587,7 +587,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
            #region 用户权限处理.
 | 
			
		||||
 | 
			
		||||
            //获取菜单信息
 | 
			
		||||
            if (menusList.Any())
 | 
			
		||||
            if (relationUsers.Count != 0)
 | 
			
		||||
            {
 | 
			
		||||
                //获取权限授权树
 | 
			
		||||
                var permissions = App.GetService<IApiPermissionService>().PermissionTreeSelector(menusList.Select(it => it.Href));
 | 
			
		||||
@@ -642,7 +642,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    [OperDesc("DeleteUser")]
 | 
			
		||||
    public async Task<bool> DeleteUserAsync(IEnumerable<long> ids)
 | 
			
		||||
    public async Task<bool> DeleteUserAsync(HashSet<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        var containsSuperAdmin = await db.Queryable<SysUser>().Where(it => it.Id == RoleConst.SuperAdminId && ids.Contains(it.Id)).AnyAsync().ConfigureAwait(false);//判断是否有超管
 | 
			
		||||
@@ -672,7 +672,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
            .ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            //删除用户
 | 
			
		||||
            await db.Deleteable<SysUser>().In(ids.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);//删除
 | 
			
		||||
            await db.Deleteable<SysUser>().In(ids).ExecuteCommandHasChangeAsync().ConfigureAwait(false);//删除
 | 
			
		||||
 | 
			
		||||
            //删除关系表用户与资源关系,用户与权限关系,用户与角色关系
 | 
			
		||||
            await db.Deleteable<SysRelation>(it => ids.Contains(it.ObjectId) && delRelations.Contains(it.Category)).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
@@ -718,16 +718,16 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
 | 
			
		||||
    public void DeleteUserFromCache(IEnumerable<long> ids)
 | 
			
		||||
    {
 | 
			
		||||
        var userIds = ids.Select(it => it.ToString()).ToArray();//id转string列表
 | 
			
		||||
        var sysUsers = App.CacheService.HashGet<SysUser>(CacheConst.Cache_SysUser, userIds).Where(it => it != null);//获取用户列表
 | 
			
		||||
        if (sysUsers.Any() == true)
 | 
			
		||||
        var sysUsers = App.CacheService.HashGet<SysUser>(CacheConst.Cache_SysUser, userIds);//获取用户列表
 | 
			
		||||
        if (sysUsers.Count != 0)
 | 
			
		||||
        {
 | 
			
		||||
            var accounts = sysUsers.Where(it => it != null).Select(it => it.Account).ToArray();//账号集合
 | 
			
		||||
            var phones = sysUsers.Select(it => it.Phone);//手机号集合
 | 
			
		||||
            var phones = sysUsers.Select(it => it?.Phone);//手机号集合
 | 
			
		||||
 | 
			
		||||
            if (sysUsers.Any(it => it.TenantId != null))//如果有租户id不是空的表示是多租户模式
 | 
			
		||||
            if (sysUsers.Any(it => it?.TenantId != null))//如果有租户id不是空的表示是多租户模式
 | 
			
		||||
            {
 | 
			
		||||
                var userAccountKey = CacheConst.Cache_SysUserAccount;
 | 
			
		||||
                var tenantIds = sysUsers.Where(it => it.TenantId != null).Select(it => it.TenantId.Value).Distinct().ToArray();//租户id列表
 | 
			
		||||
                var tenantIds = sysUsers.Where(it => it?.TenantId != null).Select(it => it.TenantId.Value).Distinct().ToArray();//租户id列表
 | 
			
		||||
                foreach (var tenantId in tenantIds)
 | 
			
		||||
                {
 | 
			
		||||
                    userAccountKey = $"{userAccountKey}:{tenantId}";
 | 
			
		||||
 
 | 
			
		||||
@@ -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删除用户数据
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -145,7 +145,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
 | 
			
		||||
    public void Delete(long id)
 | 
			
		||||
    {
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        db.Deleteable<VerificatInfo>(id).ExecuteCommand();
 | 
			
		||||
        db.Deleteable<VerificatInfo>(a => a.Id == id).ExecuteCommand();
 | 
			
		||||
        VerificatInfoService.RemoveCache(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,12 +31,13 @@ public partial class ChoiceTable<TItem> where TItem : class, new()
 | 
			
		||||
 | 
			
		||||
    public async Task OnAddAsync(IEnumerable<TItem> selectorOutputs)
 | 
			
		||||
    {
 | 
			
		||||
        if (MaxCount > 0 && selectorOutputs.Count() + SelectedRows.Count > MaxCount)
 | 
			
		||||
        var data = selectorOutputs is IReadOnlyCollection<TItem> list ? list : selectorOutputs.ToList();
 | 
			
		||||
        if (MaxCount > 0 && data.Count + SelectedRows.Count > MaxCount)
 | 
			
		||||
        {
 | 
			
		||||
            await ToastService.Warning(AdminLocalizer["MaxCount"]);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        foreach (var item in selectorOutputs)
 | 
			
		||||
        foreach (var item in data)
 | 
			
		||||
        {
 | 
			
		||||
            SelectedRows.Add(item);
 | 
			
		||||
            await table2.QueryAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -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,8 +123,8 @@ public class BlazorAppContext
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
 | 
			
		||||
            OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
 | 
			
		||||
            AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
 | 
			
		||||
            OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
 | 
			
		||||
            {
 | 
			
		||||
                Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
 | 
			
		||||
@@ -132,8 +132,8 @@ public class BlazorAppContext
 | 
			
		||||
                Icon = item.Icon,
 | 
			
		||||
                Url = item.Href,
 | 
			
		||||
                Target = item.Target.ToString(),
 | 
			
		||||
            });
 | 
			
		||||
            UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
 | 
			
		||||
            }).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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -93,7 +93,7 @@ public partial class SysResourcePage
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var result = await SysResourceService.DeleteResourceAsync(sysResources.Select(a => a.Id));
 | 
			
		||||
            var result = await SysResourceService.DeleteResourceAsync(sysResources.Select(a => a.Id).ToHashSet());
 | 
			
		||||
            if (ReloadUser != null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReloadUser();
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ public partial class SysRolePage
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await SysRoleService.DeleteRoleAsync(sysRoles.Select(a => a.Id));
 | 
			
		||||
            return await SysRoleService.DeleteRoleAsync(sysRoles.Select(a => a.Id).ToHashSet());
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ public partial class SysUserPage
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await SysUserService.DeleteUserAsync(sysUsers.Select(a => a.Id));
 | 
			
		||||
            return await SysUserService.DeleteUserAsync(sysUsers.Select(a => a.Id).ToHashSet());
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
 | 
			
		||||
@@ -17,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0</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
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
    <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=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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 -->
 | 
			
		||||
 
 | 
			
		||||
@@ -45,11 +45,11 @@ public class Startup : AppStartup
 | 
			
		||||
            options.ServicesStopConcurrently = true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //// 事件总线
 | 
			
		||||
        //services.AddEventBus(options =>
 | 
			
		||||
        //{
 | 
			
		||||
        // 事件总线
 | 
			
		||||
        services.AddEventBus(options =>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        //});
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 任务调度
 | 
			
		||||
        services.AddSchedule(options => options.AddPersistence<JobPersistence>());
 | 
			
		||||
@@ -183,19 +183,22 @@ 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
{
 | 
			
		||||
@@ -443,10 +443,7 @@ public static class ObjectExtensions
 | 
			
		||||
        where TAttribute : Attribute
 | 
			
		||||
    {
 | 
			
		||||
        // 空检查
 | 
			
		||||
        if (type == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new ArgumentNullException(nameof(type));
 | 
			
		||||
        }
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(type);
 | 
			
		||||
 | 
			
		||||
        // 检查特性并获取特性对象
 | 
			
		||||
        return type.IsDefined(typeof(TAttribute), inherit)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +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,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -13,7 +14,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.8.1" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.9.3" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ 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; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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": "更新人"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -43,12 +43,12 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
                }
 | 
			
		||||
                if (sql.StartsWith("INSERT"))
 | 
			
		||||
                {
 | 
			
		||||
                    Console.ForegroundColor = ConsoleColor.Yellow;
 | 
			
		||||
                    Console.ForegroundColor = ConsoleColor.Blue;
 | 
			
		||||
                    DbContext.WriteLog($"添加{config.ConfigId}库操作");
 | 
			
		||||
                }
 | 
			
		||||
                if (sql.StartsWith("DELETE"))
 | 
			
		||||
                {
 | 
			
		||||
                    Console.ForegroundColor = ConsoleColor.Red;
 | 
			
		||||
                    Console.ForegroundColor = ConsoleColor.Blue;
 | 
			
		||||
                    DbContext.WriteLog($"删除{config.ConfigId}库操作");
 | 
			
		||||
                }
 | 
			
		||||
                DbContext.WriteLogWithSql(UtilMethods.GetNativeSql(sql, pars));
 | 
			
		||||
@@ -62,7 +62,7 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
            if (ex.Parameters == null) return;
 | 
			
		||||
            Console.ForegroundColor = ConsoleColor.Red;
 | 
			
		||||
            DbContext.WriteLog($"{config.ConfigId}库操作异常");
 | 
			
		||||
            DbContext.WriteErrorLogWithSql(UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parameters));
 | 
			
		||||
            DbContext.WriteErrorLogWithSql(UtilMethods.GetNativeSql(ex.Sql, ex.Parameters));
 | 
			
		||||
            NewLife.Log.XTrace.WriteException(ex);
 | 
			
		||||
            Console.ForegroundColor = ConsoleColor.White;
 | 
			
		||||
        };
 | 
			
		||||
@@ -110,10 +110,6 @@ public class SugarAopService : ISugarAopService
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        //查询数据转换
 | 
			
		||||
        db.Aop.DataExecuted = (value, entity) =>
 | 
			
		||||
        {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        db.Aop.OnLogExecuted = (sql, pars) =>
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@ public static class CodeFirstUtils
 | 
			
		||||
        var seedDataTypes = App.EffectiveTypes
 | 
			
		||||
    .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass
 | 
			
		||||
    && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>))) && u.Assembly.FullName == assemblyName);
 | 
			
		||||
        if (!seedDataTypes.Any()) return;
 | 
			
		||||
        foreach (var seedType in seedDataTypes)//遍历种子类
 | 
			
		||||
        {
 | 
			
		||||
            //使用与指定参数匹配程度最高的构造函数来创建指定类型的实例。
 | 
			
		||||
@@ -62,10 +61,12 @@ public static class CodeFirstUtils
 | 
			
		||||
            // seedDataTable.TableName = db.EntityMaintenance.GetEntityInfo(entityType).DbTableName;//获取表名
 | 
			
		||||
            var ignoreAdd = seedDataMethod!.GetCustomAttribute<IgnoreSeedDataAddAttribute>();//读取忽略插入特性
 | 
			
		||||
            var ignoreUpdate = seedDataMethod!.GetCustomAttribute<IgnoreSeedDataUpdateAttribute>();//读取忽略更新特性
 | 
			
		||||
 | 
			
		||||
            var seedDataList = seedData.ToList();
 | 
			
		||||
            if (entityInfo.Columns.Any(u => u.IsPrimarykey))//判断种子数据是否有主键
 | 
			
		||||
            {
 | 
			
		||||
                // 按主键进行批量增加和更新
 | 
			
		||||
                var storage = db.StorageableByObject(seedData.ToList()).ToStorage();
 | 
			
		||||
                var storage = db.StorageableByObject(seedDataList).ToStorage();
 | 
			
		||||
                if (ignoreAdd == null)
 | 
			
		||||
                    storage.AsInsertable.ExecuteCommand();//执行插入
 | 
			
		||||
                if (ignoreUpdate == null && config.IsUpdateSeedData) storage.AsUpdateable.ExecuteCommand();//只有没有忽略更新的特性才执行更新
 | 
			
		||||
@@ -75,7 +76,7 @@ public static class CodeFirstUtils
 | 
			
		||||
                //全量插入
 | 
			
		||||
                // 无主键则只进行插入
 | 
			
		||||
                if (!db.Queryable(entityInfo.DbTableName, entityInfo.DbTableName).Any() && ignoreAdd == null)
 | 
			
		||||
                    db.InsertableByObject(seedData.ToList()).ExecuteCommand();
 | 
			
		||||
                    db.InsertableByObject(seedDataList).ExecuteCommand();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -89,7 +90,6 @@ public static class CodeFirstUtils
 | 
			
		||||
        // 获取所有实体表-初始化表结构
 | 
			
		||||
        var entityTypes = App.EffectiveTypes.Where(u =>
 | 
			
		||||
            !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && u.Assembly.FullName == assemblyName);
 | 
			
		||||
        if (!entityTypes.Any()) return;//没有就退出
 | 
			
		||||
        foreach (var entityType in entityTypes)
 | 
			
		||||
        {
 | 
			
		||||
            var tenantAtt = entityType.GetCustomAttribute<TenantAttribute>();//获取Sqlsugar多库特性
 | 
			
		||||
 
 | 
			
		||||
@@ -136,7 +136,7 @@ public static class DbContext
 | 
			
		||||
                await db.Fastest<TITEM>().PageSize(size).BulkCopyAsync(datas).ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                await db.Insertable(datas is IReadOnlyList<TITEM> values ? values : datas.ToList()).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                await db.Insertable(datas is IReadOnlyCollection<TITEM> values ? values : datas.ToList()).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -155,7 +155,7 @@ public static class DbContext
 | 
			
		||||
                await db.Fastest<TITEM>().PageSize(size).BulkUpdateAsync(datas).ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                await db.Updateable(datas is IReadOnlyList<TITEM> values ? values : datas.ToList()).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                await db.Updateable(datas is IReadOnlyCollection<TITEM> values ? values : datas.ToList()).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<GenerateDocumentationFile>True</GenerateDocumentationFile>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
@@ -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" />-->
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
 
 | 
			
		||||
@@ -213,12 +213,18 @@ public static class AppServiceCollectionExtensions
 | 
			
		||||
        // 缓存
 | 
			
		||||
        if (cacheOptions.CacheType == CacheType.Memory)
 | 
			
		||||
        {
 | 
			
		||||
            services.AddSingleton<ICache, MemoryCache>(a => new()
 | 
			
		||||
            services.AddSingleton<ICache>(a =>
 | 
			
		||||
            {
 | 
			
		||||
                Capacity = cacheOptions.MemoryCacheOptions.Capacity,
 | 
			
		||||
                Expire = cacheOptions.MemoryCacheOptions.Expire,
 | 
			
		||||
                Period = cacheOptions.MemoryCacheOptions.Period
 | 
			
		||||
            });
 | 
			
		||||
                Cache.Default = new MemoryCache()
 | 
			
		||||
                {
 | 
			
		||||
                    Capacity = cacheOptions.MemoryCacheOptions.Capacity,
 | 
			
		||||
                    Expire = cacheOptions.MemoryCacheOptions.Expire,
 | 
			
		||||
                    Period = cacheOptions.MemoryCacheOptions.Period
 | 
			
		||||
                };
 | 
			
		||||
                return Cache.Default;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else if (cacheOptions.CacheType == CacheType.Redis)
 | 
			
		||||
        {
 | 
			
		||||
@@ -278,7 +284,6 @@ public static class AppServiceCollectionExtensions
 | 
			
		||||
                    && u.GetParameters().Length > 0
 | 
			
		||||
                    && u.GetParameters().First().ParameterType == typeof(IServiceCollection));
 | 
			
		||||
 | 
			
		||||
            if (!serviceMethods.Any()) continue;
 | 
			
		||||
 | 
			
		||||
            // 自动安装属性调用
 | 
			
		||||
            foreach (var method in serviceMethods)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,8 @@ using System.Runtime.CompilerServices;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Extensions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -187,7 +189,7 @@ public static class ObjectExtensions
 | 
			
		||||
            {
 | 
			
		||||
                if (current == last) func(arg);
 | 
			
		||||
                task.Dispose();
 | 
			
		||||
            });
 | 
			
		||||
            }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -207,8 +209,8 @@ public static class ObjectExtensions
 | 
			
		||||
            Task.Delay(milliseconds).ContinueWith(task =>
 | 
			
		||||
            {
 | 
			
		||||
                if (current == last) func();
 | 
			
		||||
                task.Dispose();
 | 
			
		||||
            });
 | 
			
		||||
                task.TryDispose();
 | 
			
		||||
            }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ namespace ThingsGateway;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions>
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否启用规范化文档
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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);
 | 
			
		||||
@@ -799,6 +826,132 @@ public static class Serve
 | 
			
		||||
        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));
 | 
			
		||||
        // 配置所有 Starup Configure
 | 
			
		||||
        UseStartups(app.Services);
 | 
			
		||||
        // 释放内存
 | 
			
		||||
        App.AppStartups.Clear();
 | 
			
		||||
        // 调用自定义配置
 | 
			
		||||
        options?.ActionConfigure?.Invoke(app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建 IHost 对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -908,7 +1061,6 @@ public static class Serve
 | 
			
		||||
                    && u.GetParameters().Length > 0
 | 
			
		||||
                    && u.GetParameters().First().ParameterType == typeof(IServiceProvider));
 | 
			
		||||
 | 
			
		||||
            if (!configureMethods.Any()) continue;
 | 
			
		||||
 | 
			
		||||
            // 自动安装属性调用
 | 
			
		||||
            foreach (var method in configureMethods)
 | 
			
		||||
@@ -935,7 +1087,6 @@ public static class Serve
 | 
			
		||||
                    && u.GetParameters().Length > 0
 | 
			
		||||
                    && u.GetParameters().First().ParameterType == typeof(IApplicationBuilder));
 | 
			
		||||
 | 
			
		||||
            if (!configureMethods.Any()) continue;
 | 
			
		||||
 | 
			
		||||
            // 自动安装属性调用
 | 
			
		||||
            foreach (var method in configureMethods)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
#pragma warning disable CA1822 // 将成员标记为 static
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -150,18 +150,18 @@ public static class DependencyInjectionServiceCollectionExtensions
 | 
			
		||||
        {
 | 
			
		||||
            Register(services, dependencyType, type, injectionAttribute);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!canInjectInterfaces.Any()) return;
 | 
			
		||||
        var list = canInjectInterfaces.ToList();
 | 
			
		||||
        if (list.Count == 0) return;
 | 
			
		||||
 | 
			
		||||
        // 只注册第一个接口
 | 
			
		||||
        if (injectionAttribute.Pattern is InjectionPatterns.FirstInterface or InjectionPatterns.SelfWithFirstInterface)
 | 
			
		||||
        {
 | 
			
		||||
            Register(services, dependencyType, type, injectionAttribute, canInjectInterfaces.Last());
 | 
			
		||||
            Register(services, dependencyType, type, injectionAttribute, list.Last());
 | 
			
		||||
        }
 | 
			
		||||
        // 注册多个接口
 | 
			
		||||
        else if (injectionAttribute.Pattern is InjectionPatterns.ImplementedInterfaces or InjectionPatterns.All)
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var inter in canInjectInterfaces)
 | 
			
		||||
            foreach (var inter in list)
 | 
			
		||||
            {
 | 
			
		||||
                Register(services, dependencyType, type, injectionAttribute, inter);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,12 +47,14 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
 | 
			
		||||
    {
 | 
			
		||||
        var services = _serviceProvider.GetServices<TService>();
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        if (services
 | 
			
		||||
            .OfType<AspectDispatchProxy>()
 | 
			
		||||
            .FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
 | 
			
		||||
        {
 | 
			
		||||
            service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
 | 
			
		||||
        }
 | 
			
		||||
#pragma warning restore CA1851
 | 
			
		||||
 | 
			
		||||
        return service;
 | 
			
		||||
    }
 | 
			
		||||
@@ -80,12 +82,14 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
 | 
			
		||||
        // 解析所有实现
 | 
			
		||||
        var services = _serviceProvider.GetServices<TService>();
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        if (services
 | 
			
		||||
            .OfType<AspectDispatchProxy>()
 | 
			
		||||
            .FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
 | 
			
		||||
        {
 | 
			
		||||
            service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
 | 
			
		||||
        }
 | 
			
		||||
#pragma warning restore CA1851
 | 
			
		||||
 | 
			
		||||
        // 如果服务不存在,抛出异常
 | 
			
		||||
        return service ?? throw new InvalidOperationException($"Named service `{serviceName}` is not registered in container.");
 | 
			
		||||
 
 | 
			
		||||
@@ -356,7 +356,7 @@ internal sealed class EventBusHostedService : BackgroundService
 | 
			
		||||
                        GC.WaitForPendingFinalizers();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }, stoppingToken);
 | 
			
		||||
            }, stoppingToken, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ internal sealed partial class ChannelEventPublisher : IEventPublisher
 | 
			
		||||
            await Task.Delay(TimeSpan.FromMilliseconds(delay), eventSource.CancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await _eventSourceStorer.WriteAsync(eventSource, eventSource.CancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }, eventSource.CancellationToken);
 | 
			
		||||
        }, eventSource.CancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ public static class IEndpointRouteBuilderExtensions
 | 
			
		||||
            && u.IsDefined(typeof(MapHubAttribute), true)
 | 
			
		||||
            && (typeof(Hub).IsAssignableFrom(u) || u.HasImplementedRawGeneric(typeof(Hub<>))));
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        if (!hubs.Any()) return;
 | 
			
		||||
 | 
			
		||||
        // 反射获取 MapHub 拓展方法
 | 
			
		||||
@@ -65,5 +66,6 @@ public static class IEndpointRouteBuilderExtensions
 | 
			
		||||
            hub.GetMethod("HubEndpointConventionBuilderSettings", BindingFlags.Public | BindingFlags.Static)
 | 
			
		||||
                ?.Invoke(null, new object[] { hubEndpointConventionBuilder });
 | 
			
		||||
        }
 | 
			
		||||
#pragma warning restore CA1851
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -162,8 +162,8 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
 | 
			
		||||
        _databaseLoggingWriter = _serviceScope.ServiceProvider.GetRequiredService(databaseLoggingWriterType) as IDatabaseLoggingWriter;
 | 
			
		||||
 | 
			
		||||
        // 创建长时间运行的后台任务,并将日志消息队列中数据写入存储中
 | 
			
		||||
        _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync
 | 
			
		||||
            , TaskCreationOptions.LongRunning);
 | 
			
		||||
        _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, CancellationToken.None
 | 
			
		||||
            , TaskCreationOptions.LongRunning, TaskScheduler.Default);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
 | 
			
		||||
        _fileLoggingWriter = new FileLoggingWriter(this);
 | 
			
		||||
 | 
			
		||||
        // 创建长时间运行的后台任务,并将日志消息队列中数据写入文件中
 | 
			
		||||
        _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, TaskCreationOptions.LongRunning);
 | 
			
		||||
        _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,7 @@ public static class OptionsBuilderExtensions
 | 
			
		||||
        var builderInterfaces = optionsBuilderType.GetInterfaces()
 | 
			
		||||
            .Where(u => optionsBuilderDependency.IsAssignableFrom(u) && u != optionsBuilderDependency);
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        if (!builderInterfaces.Any())
 | 
			
		||||
        {
 | 
			
		||||
            return optionsBuilder;
 | 
			
		||||
@@ -95,6 +96,7 @@ public static class OptionsBuilderExtensions
 | 
			
		||||
        {
 | 
			
		||||
            InvokeMapMethod(optionsBuilder, optionsBuilderType, builderInterface);
 | 
			
		||||
        }
 | 
			
		||||
#pragma warning restore CA1851
 | 
			
		||||
 | 
			
		||||
        return optionsBuilder;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -115,7 +115,7 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
 | 
			
		||||
        if (Persistence is not null)
 | 
			
		||||
        {
 | 
			
		||||
            // 创建长时间运行的后台任务,并将作业运行消息写入持久化中
 | 
			
		||||
            _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, TaskCreationOptions.LongRunning);
 | 
			
		||||
            _processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -163,19 +163,16 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
 | 
			
		||||
            var initialSchedulerBuilders = _schedulerBuilders.Concat(preloadSchedulerBuilders ?? Enumerable.Empty<SchedulerBuilder>());
 | 
			
		||||
 | 
			
		||||
            // 如果作业调度器中包含作业计划构建器
 | 
			
		||||
            if (initialSchedulerBuilders.Any())
 | 
			
		||||
            // 逐条遍历并加载到内存中
 | 
			
		||||
            foreach (var schedulerBuilder in initialSchedulerBuilders)
 | 
			
		||||
            {
 | 
			
		||||
                // 逐条遍历并加载到内存中
 | 
			
		||||
                foreach (var schedulerBuilder in initialSchedulerBuilders)
 | 
			
		||||
                SchedulerBuilder schedulerBuilderObj = null;
 | 
			
		||||
                if (isSetPersistence)
 | 
			
		||||
                {
 | 
			
		||||
                    SchedulerBuilder schedulerBuilderObj = null;
 | 
			
		||||
                    if (isSetPersistence)
 | 
			
		||||
                    {
 | 
			
		||||
                        schedulerBuilderObj = await Persistence.OnLoadingAsync(schedulerBuilder, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    _ = TrySaveJob(schedulerBuilderObj ?? schedulerBuilder, out _, false);
 | 
			
		||||
                    schedulerBuilderObj = await Persistence.OnLoadingAsync(schedulerBuilder, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _ = TrySaveJob(schedulerBuilderObj ?? schedulerBuilder, out _, false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -484,11 +481,12 @@ internal sealed partial class SchedulerFactory : ISchedulerFactory
 | 
			
		||||
                .Where(t => t.NextShouldRun(startAt))
 | 
			
		||||
                .Select(t => t.NextRunTime.Value));
 | 
			
		||||
 | 
			
		||||
        // 空检查
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
        if (!nextRunTimes.Any()) return null;
 | 
			
		||||
 | 
			
		||||
        // 获取最早触发的时间
 | 
			
		||||
        var earliestTriggerTime = nextRunTimes.Min();
 | 
			
		||||
#pragma warning restore CA1851
 | 
			
		||||
 | 
			
		||||
        // 计算总休眠时间
 | 
			
		||||
        var sleepMilliseconds = (earliestTriggerTime - startAt).TotalMilliseconds;
 | 
			
		||||
 
 | 
			
		||||
@@ -400,7 +400,7 @@ internal sealed class ScheduleHostedService : BackgroundService
 | 
			
		||||
                            // 通知 GC 垃圾回收器回收
 | 
			
		||||
                            //_schedulerFactory.GCCollect();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, stoppingToken);
 | 
			
		||||
                    }, stoppingToken, TaskCreationOptions.None, TaskScheduler.Default);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
 | 
			
		||||
		
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
@@ -30,7 +31,8 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
 | 
			
		||||
		<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Text.Encoding.CodePages" Version="$(NET9Version)" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
@@ -43,6 +45,7 @@
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(NET9Version)" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(NET9Version)" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(NET9Version)" />
 | 
			
		||||
		
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,10 +31,8 @@ internal readonly struct ComponentMetadata
 | 
			
		||||
        Version = version?.ToString();
 | 
			
		||||
        Description = description;
 | 
			
		||||
 | 
			
		||||
#pragma warning disable CA1863 // 使用 "CompositeFormat"
 | 
			
		||||
        NuGetPage = string.Format(Constants.NUGET_PACKAGE_PAGE, name, version?.ToString() ?? string.Empty);
 | 
			
		||||
        DocumentationPage = string.Format(Constants.FURION_COMPONENT_DOCS_PAGE, Name);
 | 
			
		||||
#pragma warning restore CA1863 // 使用 "CompositeFormat"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -546,7 +546,9 @@ internal sealed partial class HttpRemoteService : IHttpRemoteService
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // ReSharper disable once MethodHasAsyncOverload
 | 
			
		||||
#pragma warning disable CA1849
 | 
			
		||||
                InvokeStatusCodeHandlers(httpRequestBuilder, httpResponseMessage, timeoutCancellationToken);
 | 
			
		||||
#pragma warning restore CA1849
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 检查 HTTP 响应内容长度是否在设定的最大缓冲区大小限制内
 | 
			
		||||
 
 | 
			
		||||
@@ -232,7 +232,7 @@ public abstract class Cache : DisposeBase, ICache
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual Int64 Increment(String key, Int64 value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var v = Get<Int64>(key);
 | 
			
		||||
            v += value;
 | 
			
		||||
@@ -248,7 +248,7 @@ public abstract class Cache : DisposeBase, ICache
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual Double Increment(String key, Double value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var v = Get<Double>(key);
 | 
			
		||||
            v += value;
 | 
			
		||||
@@ -264,7 +264,7 @@ public abstract class Cache : DisposeBase, ICache
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual Int64 Decrement(String key, Int64 value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var v = Get<Int64>(key);
 | 
			
		||||
            v -= value;
 | 
			
		||||
@@ -273,14 +273,14 @@ public abstract class Cache : DisposeBase, ICache
 | 
			
		||||
            return v;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
    /// <summary>递减,原子操作</summary>
 | 
			
		||||
    /// <param name="key">键</param>
 | 
			
		||||
    /// <param name="value">变化量</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual Double Decrement(String key, Double value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var v = Get<Double>(key);
 | 
			
		||||
            v -= value;
 | 
			
		||||
 
 | 
			
		||||
@@ -865,7 +865,7 @@ public class MemoryCache : Cache
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override void HashAdd<T>(string key, string hashKey, T value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            //获取字典
 | 
			
		||||
            var exist = GetDictionary<T>(key);
 | 
			
		||||
@@ -879,7 +879,7 @@ public class MemoryCache : Cache
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override bool HashSet<T>(string key, Dictionary<string, T> dic)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            //获取字典
 | 
			
		||||
            var exist = GetDictionary<T>(key);
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
    {
 | 
			
		||||
        if (_inited) return;
 | 
			
		||||
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_inited) return;
 | 
			
		||||
            _inited = true;
 | 
			
		||||
@@ -274,6 +274,7 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected virtual T? OnCreate() => (T?)typeof(T).CreateInstance();
 | 
			
		||||
    #endregion
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    #region 定期清理
 | 
			
		||||
    private TimerX? _timer;
 | 
			
		||||
@@ -281,7 +282,7 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull
 | 
			
		||||
    private void StartTimer()
 | 
			
		||||
    {
 | 
			
		||||
        if (_timer != null) return;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_timer != null) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ public class Pool<T> : IPool<T> where T : class
 | 
			
		||||
        public T? Value;
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    #region 构造
 | 
			
		||||
    /// <summary>实例化对象池。默认大小CPU*2</summary>
 | 
			
		||||
@@ -63,7 +64,7 @@ public class Pool<T> : IPool<T> where T : class
 | 
			
		||||
    private void Init()
 | 
			
		||||
    {
 | 
			
		||||
        if (_items != null) return;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_items != null) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
namespace ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
public class BoundedQueue<T> : IEnumerable<T>
 | 
			
		||||
{
 | 
			
		||||
    private readonly Queue<T> _queue;
 | 
			
		||||
    private readonly int _capacity;
 | 
			
		||||
    private readonly object _syncRoot = new object();
 | 
			
		||||
 | 
			
		||||
    public BoundedQueue(int capacity)
 | 
			
		||||
    {
 | 
			
		||||
        if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
 | 
			
		||||
        _capacity = capacity;
 | 
			
		||||
        _queue = new Queue<T>(capacity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Enqueue(T item)
 | 
			
		||||
    {
 | 
			
		||||
        lock (_syncRoot)
 | 
			
		||||
        {
 | 
			
		||||
            if (_queue.Count == _capacity)
 | 
			
		||||
                _queue.Dequeue();
 | 
			
		||||
            _queue.Enqueue(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int Count
 | 
			
		||||
    {
 | 
			
		||||
        get { lock (_syncRoot) return _queue.Count; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IEnumerator<T> GetEnumerator()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_syncRoot)
 | 
			
		||||
        {
 | 
			
		||||
            return new List<T>(_queue).GetEnumerator();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Threading;
 | 
			
		||||
namespace ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
public class ExpiringDictionary<TKey, TValue> : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly ConcurrentDictionary<TKey, TValue> _dict = new();
 | 
			
		||||
    private readonly TimerX _cleanupTimer;
 | 
			
		||||
 | 
			
		||||
    public ExpiringDictionary(int cleanupInterval = 60000)
 | 
			
		||||
    {
 | 
			
		||||
        _cleanupTimer = new TimerX(Clear, null, cleanupInterval, cleanupInterval) { Async = true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void TryAdd(TKey key, TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        _dict.TryAdd(key, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryGetValue(TKey key, out TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        return _dict.TryGetValue(key, out value);
 | 
			
		||||
    }
 | 
			
		||||
    public TValue GetOrAdd(TKey key, Func<TKey, TValue> func)
 | 
			
		||||
    {
 | 
			
		||||
        return _dict.GetOrAdd(key, func);
 | 
			
		||||
    }
 | 
			
		||||
    public TValue GetOrAdd(TKey key, TValue value)
 | 
			
		||||
    {
 | 
			
		||||
        return _dict.GetOrAdd(key, value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool TryRemove(TKey key) => _dict.TryRemove(key, out _);
 | 
			
		||||
 | 
			
		||||
    public void Clear() => _dict.Clear();
 | 
			
		||||
 | 
			
		||||
    private void Clear(object? state)
 | 
			
		||||
    {
 | 
			
		||||
        _dict.Clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        _dict.Clear();
 | 
			
		||||
        _cleanupTimer.Dispose();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,6 +18,7 @@ public sealed class IncrementCount
 | 
			
		||||
    private long _current = 0;
 | 
			
		||||
    private long _max = long.MaxValue;
 | 
			
		||||
    private long _start = 0;
 | 
			
		||||
    private object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="IncrementCount"/>
 | 
			
		||||
    public IncrementCount(long max, long start = 0, int tick = 1)
 | 
			
		||||
@@ -43,7 +44,7 @@ public sealed class IncrementCount
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public long GetCurrentValue()
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            long current = _current;
 | 
			
		||||
            _current += IncreaseTick;
 | 
			
		||||
@@ -65,7 +66,7 @@ public sealed class IncrementCount
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void ResetCurrentValue()
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            _current = _start;
 | 
			
		||||
        }
 | 
			
		||||
@@ -77,7 +78,7 @@ public sealed class IncrementCount
 | 
			
		||||
    /// <param name="value">指定值</param>
 | 
			
		||||
    public void ResetCurrentValue(long value)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            _current = value <= _max ? value >= _start ? value : _start : _max;
 | 
			
		||||
        }
 | 
			
		||||
@@ -88,7 +89,7 @@ public sealed class IncrementCount
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void ResetMaxValue(long max)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (max > _start)
 | 
			
		||||
            {
 | 
			
		||||
@@ -108,7 +109,7 @@ public sealed class IncrementCount
 | 
			
		||||
    /// <param name="start">初始值</param>
 | 
			
		||||
    public void ResetStartValue(long start)
 | 
			
		||||
    {
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (start < _max)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -132,17 +132,6 @@ public class MachineInfo : IExtend
 | 
			
		||||
    /// <summary>机器信息提供者。外部实现可修改部分行为</summary>
 | 
			
		||||
    public static IMachineInfo? Provider { get; set; }
 | 
			
		||||
 | 
			
		||||
    //static MachineInfo() => RegisterAsync().Wait(100);
 | 
			
		||||
 | 
			
		||||
    private static Task<MachineInfo>? _task;
 | 
			
		||||
    /// <summary>异步注册一个初始化后的机器信息实例</summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static Task<MachineInfo> RegisterAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (_task != null) return _task;
 | 
			
		||||
 | 
			
		||||
        return _task = Task.Factory.StartNew(() => Register());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static MachineInfo Register()
 | 
			
		||||
    {
 | 
			
		||||
@@ -694,13 +683,20 @@ public class MachineInfo : IExtend
 | 
			
		||||
            if (dic.TryGetValue("MemTotal", out var str) && !str.IsNullOrEmpty())
 | 
			
		||||
                Memory = (UInt64)str.TrimEnd(" kB").ToLong();
 | 
			
		||||
 | 
			
		||||
            ulong ma = 0;
 | 
			
		||||
            if (dic.TryGetValue("MemAvailable", out str) && !str.IsNullOrEmpty())
 | 
			
		||||
                AvailableMemory = (UInt64)str.TrimEnd(" kB").ToLong();
 | 
			
		||||
            else if (dic.TryGetValue("MemFree", out str) && !str.IsNullOrEmpty())
 | 
			
		||||
                AvailableMemory =
 | 
			
		||||
                    (UInt64)(str.TrimEnd(" kB").ToLong() +
 | 
			
		||||
                    dic["Buffers"]?.TrimEnd(" kB").ToLong() ?? 0 +
 | 
			
		||||
                    dic["Cached"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            {
 | 
			
		||||
                ma = (UInt64)(str.TrimEnd(" kB").ToLong());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //低于3.14内核的版本用 free+cache
 | 
			
		||||
            var mf = (UInt64)(dic["MemFree"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            var mc = (UInt64)(dic["Cached"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            var bf = (UInt64)(dic["Buffers"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
 | 
			
		||||
            var free = mf + mc + bf;
 | 
			
		||||
 | 
			
		||||
            AvailableMemory = ma > free ? ma : free;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // A2/A4温度获取,Buildroot,CPU温度和主板温度
 | 
			
		||||
 
 | 
			
		||||
@@ -103,6 +103,54 @@ public static class Runtime
 | 
			
		||||
 | 
			
		||||
    /// <summary>是否OSX环境</summary>
 | 
			
		||||
    public static Boolean OSX => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
 | 
			
		||||
 | 
			
		||||
#if NET6_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
    public static Boolean? isLegacyWindows;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断是否老系统 (Vista/2008/7/2008R2)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static Boolean IsLegacyWindows
 | 
			
		||||
    {
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            if (isLegacyWindows != null) return isLegacyWindows.Value;
 | 
			
		||||
 | 
			
		||||
            if (Windows == false)
 | 
			
		||||
            {
 | 
			
		||||
                isLegacyWindows = false;
 | 
			
		||||
                return isLegacyWindows.Value;
 | 
			
		||||
            }
 | 
			
		||||
            var version = Environment.OSVersion.Version;
 | 
			
		||||
 | 
			
		||||
            // 如果能拿到真实的 6.x 就直接判断
 | 
			
		||||
            if (version.Major == 6 && version.Minor <= 1)
 | 
			
		||||
            {
 | 
			
		||||
                isLegacyWindows = true;
 | 
			
		||||
                return isLegacyWindows.Value;
 | 
			
		||||
            }
 | 
			
		||||
            if (version.Major < 6)
 | 
			
		||||
            {
 | 
			
		||||
                isLegacyWindows = true;
 | 
			
		||||
                return isLegacyWindows.Value;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 如果拿到的是 10.0(Win8.1 之后有虚拟化问题),用 OSDescription 来兜底
 | 
			
		||||
            var desc = RuntimeInformation.OSDescription;
 | 
			
		||||
            // desc 示例: "Microsoft Windows 6.1.7601" (Win7/2008R2)
 | 
			
		||||
            if (desc.Contains("Windows 6.0") || desc.Contains("Windows 6.1"))
 | 
			
		||||
            {
 | 
			
		||||
                isLegacyWindows = true;
 | 
			
		||||
                return isLegacyWindows.Value;
 | 
			
		||||
            }
 | 
			
		||||
            isLegacyWindows = false;
 | 
			
		||||
            return isLegacyWindows.Value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#else
 | 
			
		||||
    /// <summary>是否Web环境</summary>
 | 
			
		||||
    public static Boolean IsWeb => !String.IsNullOrEmpty(System.Web.HttpRuntime.AppDomainAppId);
 | 
			
		||||
@@ -115,6 +163,8 @@ public static class Runtime
 | 
			
		||||
 | 
			
		||||
    /// <summary>是否OSX环境</summary>
 | 
			
		||||
    public static Boolean OSX { get; } = Environment.OSVersion.Platform == PlatformID.MacOSX;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ public class Config<TConfig> where TConfig : Config<TConfig>, new()
 | 
			
		||||
 | 
			
		||||
        Provider = prv;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly Object _lock = new Object();
 | 
			
		||||
    private static TConfig? _Current;
 | 
			
		||||
    /// <summary>当前实例。通过置空可以使其重新加载。</summary>
 | 
			
		||||
    public static TConfig Current
 | 
			
		||||
@@ -47,7 +47,7 @@ public class Config<TConfig> where TConfig : Config<TConfig>, new()
 | 
			
		||||
        get
 | 
			
		||||
        {
 | 
			
		||||
            if (_Current != null) return _Current;
 | 
			
		||||
            lock (typeof(TConfig))
 | 
			
		||||
            lock (_lock)
 | 
			
		||||
            {
 | 
			
		||||
                if (_Current != null) return _Current;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -110,7 +110,7 @@ public abstract class FileConfigProvider : ConfigProvider
 | 
			
		||||
        if (model == null) return false;
 | 
			
		||||
 | 
			
		||||
        // 加锁,避免多线程冲突
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            // 文件存储,直接覆盖Root
 | 
			
		||||
            Root.Childs?.Clear();
 | 
			
		||||
@@ -168,7 +168,7 @@ public abstract class FileConfigProvider : ConfigProvider
 | 
			
		||||
    private void InitTimer()
 | 
			
		||||
    {
 | 
			
		||||
        if (_timer != null) return;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_timer != null) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -180,6 +180,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
 | 
			
		||||
    /// <param name="value"></param>
 | 
			
		||||
    public virtual void Init(String value) { }
 | 
			
		||||
    #endregion
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    #region 加载/保存
 | 
			
		||||
    /// <summary>从数据源加载数据到配置树</summary>
 | 
			
		||||
@@ -189,7 +190,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
 | 
			
		||||
    private void EnsureLoad()
 | 
			
		||||
    {
 | 
			
		||||
        if (_Loaded) return;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_Loaded) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -84,13 +84,14 @@ public class Snowflake
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    #region 核心方法
 | 
			
		||||
    private Boolean _inited;
 | 
			
		||||
    private void Init()
 | 
			
		||||
    {
 | 
			
		||||
        if (_inited) return;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_inited) return;
 | 
			
		||||
 | 
			
		||||
@@ -152,7 +153,7 @@ public class Snowflake
 | 
			
		||||
 | 
			
		||||
        // 核心理念:时间不同时序号置零,时间相同时序号递增
 | 
			
		||||
        var seq = 0;
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            while (true)
 | 
			
		||||
            {
 | 
			
		||||
@@ -180,7 +181,7 @@ public class Snowflake
 | 
			
		||||
        //{
 | 
			
		||||
        //    if (ms > origin)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        lock (this)
 | 
			
		||||
        //        lock (lockThis)
 | 
			
		||||
        //        {
 | 
			
		||||
        //            origin = Volatile.Read(ref _lastTime);
 | 
			
		||||
        //            if (ms > origin)
 | 
			
		||||
@@ -209,7 +210,7 @@ public class Snowflake
 | 
			
		||||
        //    origin = Volatile.Read(ref _lastTime);
 | 
			
		||||
        //    if (ms == origin)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        lock (this)
 | 
			
		||||
        //        lock (lockThis)
 | 
			
		||||
        //        {
 | 
			
		||||
        //            origin = Volatile.Read(ref _lastTime);
 | 
			
		||||
        //            if (ms == origin)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ namespace ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class SystemTextJsonExtension
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(带缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -31,37 +32,51 @@ public static class SystemTextJsonExtension
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions NoneIndentedOptions;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(带缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions IgnoreNullIndentedOptions;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认Json规则(无缩进)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static JsonSerializerOptions IgnoreNullNoneIndentedOptions;
 | 
			
		||||
 | 
			
		||||
    public static JsonSerializerOptions GetOptions(bool writeIndented, bool ignoreNull)
 | 
			
		||||
    {
 | 
			
		||||
        var options = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = writeIndented,
 | 
			
		||||
            DefaultIgnoreCondition = ignoreNull
 | 
			
		||||
                ? JsonIgnoreCondition.WhenWritingNull
 | 
			
		||||
                : JsonIgnoreCondition.Never,
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        options.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        options.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        options.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
 | 
			
		||||
        return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static SystemTextJsonExtension()
 | 
			
		||||
    {
 | 
			
		||||
        IndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = true, // 缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        // 如有自定义Converter,这里添加
 | 
			
		||||
        // IndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        IndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
            WriteIndented = false, // 不缩进
 | 
			
		||||
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | 
			
		||||
            NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
 | 
			
		||||
        };
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
 | 
			
		||||
        NoneIndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
 | 
			
		||||
        // NoneIndentedOptions.Converters.Add(new ByteArrayJsonConverter());
 | 
			
		||||
 | 
			
		||||
        IndentedOptions = GetOptions(true, false);
 | 
			
		||||
        NoneIndentedOptions = GetOptions(false, false);
 | 
			
		||||
 | 
			
		||||
        IgnoreNullIndentedOptions = GetOptions(true, true);
 | 
			
		||||
        IgnoreNullNoneIndentedOptions = GetOptions(false, true);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 反序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -96,17 +111,17 @@ public static class SystemTextJsonExtension
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, bool indented = true)
 | 
			
		||||
    public static string ToSystemTextJsonString(this object item, bool indented = true, bool ignoreNull = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
        return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 序列化
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true)
 | 
			
		||||
    public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true, bool ignoreNull = true)
 | 
			
		||||
    {
 | 
			
		||||
        return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
        return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/Admin/ThingsGateway.NewLife.X/GlobalSuppressions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/Admin/ThingsGateway.NewLife.X/GlobalSuppressions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
// This file is used by Code Analysis to maintain SuppressMessage
 | 
			
		||||
// attributes that are applied to this project.
 | 
			
		||||
// Project-level suppressions either have no target or are given
 | 
			
		||||
// a specific target and scoped to a namespace, type, member, etc.
 | 
			
		||||
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
[assembly: SuppressMessage("Reliability", "CA1849", Justification = "<挂起>", Scope = "module")]
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +67,7 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
        _cache = null;
 | 
			
		||||
    }
 | 
			
		||||
    #endregion
 | 
			
		||||
    protected object lockThis = new();
 | 
			
		||||
 | 
			
		||||
    #region 添删改查
 | 
			
		||||
    /// <summary>批量写入数据(高性能)</summary>
 | 
			
		||||
@@ -74,7 +75,6 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
    /// <param name="append">是否附加在尾部。为false时从头写入,覆盖已有数据</param>
 | 
			
		||||
    public void Write(IEnumerable<T> models, Boolean append)
 | 
			
		||||
    {
 | 
			
		||||
        if (!models.Any() && append) return;
 | 
			
		||||
 | 
			
		||||
        var file = GetFile();
 | 
			
		||||
        file.EnsureDirectory(true);
 | 
			
		||||
@@ -146,7 +146,7 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
    {
 | 
			
		||||
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));
 | 
			
		||||
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            if (_cache != null) return _cache.RemoveAll(x => predicate(x));
 | 
			
		||||
 | 
			
		||||
@@ -190,7 +190,7 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
    {
 | 
			
		||||
        if (Comparer == null) throw new ArgumentNullException(nameof(Comparer));
 | 
			
		||||
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var list = _cache ?? FindAll();
 | 
			
		||||
            if (!add && list.Count == 0) return false;
 | 
			
		||||
@@ -266,7 +266,7 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
        var file = GetFile();
 | 
			
		||||
        if (!File.Exists(file)) yield break;
 | 
			
		||||
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            using var csv = new CsvFile(file, false) { Encoding = Encoding };
 | 
			
		||||
 | 
			
		||||
@@ -334,7 +334,7 @@ public class CsvDb<T> : DisposeBase where T : new()
 | 
			
		||||
    {
 | 
			
		||||
        if (_cache != null) return _cache!.Count;
 | 
			
		||||
 | 
			
		||||
        lock (this)
 | 
			
		||||
        lock (lockThis)
 | 
			
		||||
        {
 | 
			
		||||
            var file = GetFile();
 | 
			
		||||
            if (!File.Exists(file)) return 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ public class ConsoleLog : Logger
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            lock (this)
 | 
			
		||||
            lock (lockThis)
 | 
			
		||||
            {
 | 
			
		||||
                var cc = Console.ForegroundColor;
 | 
			
		||||
                cc = level switch
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user