Compare commits
	
		
			56 Commits
		
	
	
		
			10.9.93.0
			...
			10.11.17.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4e6be23aac | ||
|   | 2fabbd236b | ||
|   | 163a66530e | ||
|   | 29073a00c4 | ||
|   | c6d4d1ecfa | ||
|   | ba16889cad | ||
|   | 5aaed35b0f | ||
|   | df067c91eb | ||
|   | 2078b4a60b | ||
|   | 20a2e3ff8e | ||
|   | 61a973b1b5 | ||
|   | cbd72e2081 | ||
|   | 4e0377b20c | ||
|   | fd318d3cdc | ||
|   | 515bdb9700 | ||
|   | 46c1780017 | ||
|   | fe78a4c3ca | ||
|   | 2d7effadf9 | ||
|   | 346c560f8b | ||
|   | 8e3bd89f61 | ||
|   | 6da142d080 | ||
|   | ff7d029e6f | ||
|   | 21b4695683 | ||
|   | 02ad494a26 | ||
|   | 280366e1b2 | ||
|   | 6660ce3e34 | ||
|   | 7499162c1a | ||
|   | 40208a5cd6 | ||
|   | fa347f4f68 | ||
|   | d7df6fc605 | ||
|   | eb4bb2fd48 | ||
|   | faa9858974 | ||
|   | 1b3d2dda49 | ||
|   | a8a9453611 | ||
|   | e84f42ce14 | ||
|   | 6f814cf6b8 | ||
|   | e36432e4e9 | ||
|   | ebd71e807b | ||
|   | 34000d8d7d | ||
|   | e785f6660c | ||
|   | 831c611797 | ||
|   | 453817ef86 | ||
|   | 8ce0b981c1 | ||
|   | 4e5c51b54c | ||
|   | 3cc9d31f28 | ||
|   | 10391f869b | ||
|   | fba0723a6d | ||
|   | 2db3f78f0c | ||
|   | badf61fe01 | ||
|   | d74e0952dc | ||
|   | fb1699ce80 | ||
|   | 44adddbcd4 | ||
|   | 0eab889452 | ||
|   | e14d39a459 | ||
|   | 7575264ede | ||
|   | 3e1a077b96 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -364,6 +364,7 @@ FodyWeavers.xsd | ||||
|  | ||||
| /src/*Pro*/ | ||||
| /src/*Pro* | ||||
| /src/**/*Pro* | ||||
| /src/*pro* | ||||
| /src/*pro*/ | ||||
| /src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| 		<IncludeBuildOutput>false</IncludeBuildOutput> | ||||
| 		<!-- 避免 DLL 被打包到 lib/ --> | ||||
| 		<EnableSourceGenerator>true</EnableSourceGenerator> | ||||
| 		 | ||||
| 		<!-- 可选 --> | ||||
|  | ||||
| 		 | ||||
|   | ||||
| @@ -37,9 +37,8 @@ public class FileController : ControllerBase | ||||
|         var root = Directory.GetCurrentDirectory(); | ||||
|         var wwwroot = Path.Combine(root, "wwwroot"); | ||||
|         var filePath = Path.Combine(wwwroot, fileName); | ||||
|         // 防止路径穿越攻击 | ||||
| #pragma warning disable CA3003 | ||||
|         if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath)) | ||||
|         if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath)) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| @@ -49,6 +48,6 @@ public class FileController : ControllerBase | ||||
|  | ||||
|         Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); | ||||
|  | ||||
|         return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_'))); | ||||
|         return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_'))); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -38,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 | ||||
| { | ||||
| @@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity | ||||
|     /// <summary> | ||||
|     /// 主键Id | ||||
|     /// </summary> | ||||
|     [System.ComponentModel.DataAnnotations.Key] | ||||
|     public override long Id { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|   | ||||
| @@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|     /// 获取用户拥有的资源 | ||||
|     /// </summary> | ||||
|     /// <param name="id">用户id</param> | ||||
|     public async Task<GrantResourceData> OwnResourceAsync(long id) | ||||
|     public Task<GrantResourceData> OwnResourceAsync(long id) | ||||
|     { | ||||
|         return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false); | ||||
|         return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
| @@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|         var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下 | ||||
|         using var db = GetDB(); | ||||
|         //重置密码 | ||||
|         if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser | ||||
|         if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser | ||||
|         { | ||||
|             Password = password | ||||
|         }, it => it.Id == id).ConfigureAwait(false)) | ||||
|         }, it => it.Id == id).ConfigureAwait(false)) > 0) | ||||
|         { | ||||
|             DeleteUserFromCache(id);//从cache删除用户信息 | ||||
|             var verificatInfoIds = _verificatInfoService.GetListByUserId(id); | ||||
|   | ||||
| @@ -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删除用户数据 | ||||
|     } | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| @@ -136,14 +136,14 @@ public partial class SysResourcePage | ||||
|     private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu) | ||||
|     { | ||||
|         var sysResources = await SysResourceService.GetAllAsync(); | ||||
|         var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id); | ||||
|         var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items) | ||||
|     { | ||||
|         await Task.CompletedTask; | ||||
|         var result = ResourceUtil.BuildTableTrees(items, 0); | ||||
|         var result = AdminResourceUtil.BuildTableTrees(items, 0); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -35,7 +35,7 @@ public partial class GrantResourceDialog | ||||
|     { | ||||
|         var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList(); | ||||
|  | ||||
|         Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem); | ||||
|         Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem); | ||||
|         ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList(); | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ public partial class SysUserEdit | ||||
|         BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status)); | ||||
|         var items = await SysPositionService.SelectorAsync(new PositionSelectorInput()); | ||||
|         Items = PositionUtil.BuildCascaderItemList(items); | ||||
|         ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList(); | ||||
|         ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList(); | ||||
|         await InvokeAsync(StateHasChanged); | ||||
|         await base.OnInitializedAsync(); | ||||
|     } | ||||
|   | ||||
| @@ -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> | ||||
| { | ||||
| @@ -27,11 +27,11 @@ public class WebsiteOptions : IConfigurableOptions | ||||
|     /// </summary> | ||||
|     public bool Demo { get; set; } | ||||
|  | ||||
|     public bool WebPageEnable { get; set; } = true; | ||||
|  | ||||
|     public int MaxBlazorConnections { get; set; } = 5; | ||||
|     public bool BlazorConnectionLimitEnable { get; set; } = false; | ||||
|  | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 是否显示关于页面 | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -5,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.2" /> | ||||
| 		<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": "更新人" | ||||
|   } | ||||
| } | ||||
| @@ -46,7 +46,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new( | ||||
|     public async Task<bool> DeleteAsync(IEnumerable<T> models) | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false); | ||||
|         return await db.Deleteable<T>(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false); | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
| @@ -140,18 +140,22 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new( | ||||
|             return (await db.UpdateableT(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public virtual async Task<bool> SaveAsync(List<T> model, ItemChangedType changedType) | ||||
|     { | ||||
|         return (await SaveReturnCountAsync(model, changedType).ConfigureAwait(false)) > 0; | ||||
|     } | ||||
|     /// <inheritdoc/> | ||||
|     public async Task<int> SaveReturnCountAsync(List<T> model, ItemChangedType changedType) | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         if (changedType == ItemChangedType.Add) | ||||
|         { | ||||
|             return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|             return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|             return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)); | ||||
|         } | ||||
|     } | ||||
|     /// <summary> | ||||
|   | ||||
| @@ -5,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) | ||||
|         { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
| 	 | ||||
| 	<PropertyGroup> | ||||
| @@ -30,7 +31,7 @@ | ||||
| 	</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> | ||||
|  | ||||
|   | ||||
							
								
								
									
										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(); | ||||
|     } | ||||
| } | ||||
| @@ -683,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 | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -552,19 +552,29 @@ public static class Reflect | ||||
|     //    return false; | ||||
|     //} | ||||
|  | ||||
|  | ||||
|     private static readonly ExpiringDictionary<(MethodInfo, Type, object?), Delegate> _delegateCache = new(); | ||||
|  | ||||
|     /// <summary>把一个方法转为泛型委托,便于快速反射调用</summary> | ||||
|     /// <typeparam name="TFunc"></typeparam> | ||||
|     /// <param name="method"></param> | ||||
|     /// <param name="target"></param> | ||||
|     /// <returns></returns> | ||||
|     public static TFunc? As<TFunc>(this MethodInfo method, Object? target = null) | ||||
|     public static TFunc? As<TFunc>(this MethodInfo method, object? target = null) | ||||
|     { | ||||
|         if (method == null) return default; | ||||
|  | ||||
|         if (target == null) | ||||
|             return (TFunc?)(Object?)Delegate.CreateDelegate(typeof(TFunc), method, true); | ||||
|         else | ||||
|             return (TFunc?)(Object?)Delegate.CreateDelegate(typeof(TFunc), target, method, true); | ||||
|         var key = (method, typeof(TFunc), target); | ||||
|  | ||||
|         if (_delegateCache.TryGetValue(key, out var del)) | ||||
|             return (TFunc)(object)del; | ||||
|  | ||||
|         del = target == null | ||||
|             ? Delegate.CreateDelegate(typeof(TFunc), method, true) | ||||
|             : Delegate.CreateDelegate(typeof(TFunc), target, method, true); | ||||
|  | ||||
|         return (TFunc)(object)_delegateCache.GetOrAdd(key, del); | ||||
|     } | ||||
|  | ||||
|     #endregion | ||||
| } | ||||
| @@ -190,7 +190,31 @@ public sealed class Crc32 //: HashAlgorithm | ||||
|         crc.Update(stream, count); | ||||
|         return crc.Value; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 添加Sequence进行校验 | ||||
|     /// </summary> | ||||
|     /// <param name="sequence"></param> | ||||
|     /// <returns></returns> | ||||
|     public Crc32 Update(ReadOnlySequence<byte> sequence) | ||||
|     { | ||||
|         foreach (var segment in sequence) | ||||
|         { | ||||
|             Update(segment.Span); | ||||
|         } | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 计算校验码 (Sequence) | ||||
|     /// </summary> | ||||
|     /// <param name="sequence"></param> | ||||
|     /// <returns></returns> | ||||
|     public static UInt32 Compute(ReadOnlySequence<byte> sequence) | ||||
|     { | ||||
|         var crc = new Crc32(); | ||||
|         crc.Update(sequence); | ||||
|         return crc.Value; | ||||
|     } | ||||
|     //#region 抽象实现 | ||||
|     ///// <summary>哈希核心</summary> | ||||
|     ///// <param name="array"></param> | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
| 		<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
| 		<SignAssembly>True</SignAssembly> | ||||
| 		<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile> | ||||
| 		 | ||||
|  | ||||
|  | ||||
| 	</PropertyGroup> | ||||
|   | ||||
| @@ -168,12 +168,13 @@ public class TimerScheduler : ILogFeature | ||||
|                 _period = 60_000; | ||||
|                 foreach (var timer in arr) | ||||
|                 { | ||||
|                     if (!timer.Calling && CheckTime(timer, now)) | ||||
|                     if ((timer.Reentrant || !timer.Calling) && CheckTime(timer, now)) | ||||
|                     { | ||||
|                         // 必须在主线程设置状态,否则可能异步线程还没来得及设置开始状态,主线程又开始了新的一轮调度 | ||||
|                         timer.Calling = true; | ||||
|                         if (timer.IsAsyncTask) | ||||
|                             Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); | ||||
|                             ExecuteAsync(timer); | ||||
|                         //Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); | ||||
|                         else if (!timer.Async) | ||||
|                             Execute(timer); | ||||
|                         else | ||||
| @@ -306,8 +307,23 @@ public class TimerScheduler : ILogFeature | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var func = timer.Method.As<Func<Object?, Task>>(target); | ||||
|             await func!(timer.State).ConfigureAwait(false); | ||||
| #if NET6_0_OR_GREATER | ||||
|             if (timer.IsValueTask) | ||||
|             { | ||||
|                 var func = timer.Method.As<Func<Object?, ValueTask>>(target); | ||||
|                 var task = func!(timer.State); | ||||
|                 if (!task.IsCompleted) | ||||
|                     await task.ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
| #endif | ||||
|             { | ||||
|                 var func = timer.Method.As<Func<Object?, Task>>(target); | ||||
|                 var task = func!(timer.State); | ||||
|                 if (!task.IsCompleted) | ||||
|                     await task.ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|         catch (ThreadAbortException) { throw; } | ||||
|         catch (ThreadInterruptedException) { throw; } | ||||
|   | ||||
| @@ -72,7 +72,8 @@ public class TimerX : ITimer, ITimerx, IDisposable | ||||
|  | ||||
|     /// <summary>调用中</summary> | ||||
|     public Boolean Calling { get; internal set; } | ||||
|  | ||||
|     /// <summary>可重入</summary> | ||||
|     public Boolean Reentrant { get; set; } = false; | ||||
|     /// <summary>平均耗时。毫秒</summary> | ||||
|     public Int32 Cost { get; internal set; } | ||||
|  | ||||
| @@ -87,6 +88,8 @@ public class TimerX : ITimer, ITimerx, IDisposable | ||||
|  | ||||
|     private DateTime _AbsolutelyNext; | ||||
|     private readonly Cron[]? _crons; | ||||
|  | ||||
|     internal bool IsValueTask { get; } | ||||
|     #endregion | ||||
|  | ||||
|     //    #region 静态 | ||||
| @@ -158,6 +161,29 @@ public class TimerX : ITimer, ITimerx, IDisposable | ||||
|         Init(dueTime); | ||||
|     } | ||||
|  | ||||
| #if NET6_0_OR_GREATER | ||||
|  | ||||
|     /// <summary>实例化一个不可重入的定时器</summary> | ||||
|     /// <param name="callback">委托</param> | ||||
|     /// <param name="state">用户数据</param> | ||||
|     /// <param name="dueTime">多久之后开始。毫秒</param> | ||||
|     /// <param name="period">间隔周期。毫秒</param> | ||||
|     /// <param name="scheduler">调度器</param> | ||||
|     public TimerX(Func<Object, ValueTask> callback, Object? state, Int32 dueTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler) | ||||
|     { | ||||
|         IsValueTask = true; | ||||
|         if (callback == null) throw new ArgumentNullException(nameof(callback)); | ||||
|         if (dueTime < 0) throw new ArgumentOutOfRangeException(nameof(dueTime)); | ||||
|  | ||||
|         IsAsyncTask = true; | ||||
|         Async = true; | ||||
|         Period = period; | ||||
|  | ||||
|         Init(dueTime); | ||||
|     } | ||||
|  | ||||
| #endif | ||||
|  | ||||
|     /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary> | ||||
|     /// <param name="callback">委托</param> | ||||
|     /// <param name="state">用户数据</param> | ||||
| @@ -210,6 +236,37 @@ public class TimerX : ITimer, ITimerx, IDisposable | ||||
|         Init(ms); | ||||
|     } | ||||
|  | ||||
| #if NET6_0_OR_GREATER | ||||
|  | ||||
|     /// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary> | ||||
|     /// <param name="callback">委托</param> | ||||
|     /// <param name="state">用户数据</param> | ||||
|     /// <param name="startTime">绝对开始时间</param> | ||||
|     /// <param name="period">间隔周期。毫秒</param> | ||||
|     /// <param name="scheduler">调度器</param> | ||||
|     public TimerX(Func<Object, ValueTask> callback, Object? state, DateTime startTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler) | ||||
|     { | ||||
|         IsValueTask = true; | ||||
|         if (callback == null) throw new ArgumentNullException(nameof(callback)); | ||||
|         if (startTime <= DateTime.MinValue) throw new ArgumentOutOfRangeException(nameof(startTime)); | ||||
|         if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period)); | ||||
|  | ||||
|         IsAsyncTask = true; | ||||
|         Async = true; | ||||
|         Period = period; | ||||
|         Absolutely = true; | ||||
|  | ||||
|         //var now = DateTime.Now; | ||||
|         var now = Scheduler.GetNow(); | ||||
|         var next = startTime; | ||||
|         while (next < now) next = next.AddMilliseconds(period); | ||||
|  | ||||
|         var ms = (Int64)(next - now).TotalMilliseconds; | ||||
|         _AbsolutelyNext = next; | ||||
|         Init(ms); | ||||
|     } | ||||
|  | ||||
| #endif | ||||
|     /// <summary>实例化一个Cron定时器</summary> | ||||
|     /// <param name="callback">委托</param> | ||||
|     /// <param name="state">用户数据</param> | ||||
| @@ -274,6 +331,42 @@ public class TimerX : ITimer, ITimerx, IDisposable | ||||
|         //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now)); | ||||
|     } | ||||
|  | ||||
| #if NET6_0_OR_GREATER | ||||
|     /// <summary>实例化一个Cron定时器</summary> | ||||
|     /// <param name="callback">委托</param> | ||||
|     /// <param name="state">用户数据</param> | ||||
|     /// <param name="cronExpression">Cron表达式。支持多个表达式,分号分隔</param> | ||||
|     /// <param name="scheduler">调度器</param> | ||||
|     public TimerX(Func<Object, ValueTask> callback, Object? state, String cronExpression, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler) | ||||
|     { | ||||
|         IsValueTask = true; | ||||
|         if (callback == null) throw new ArgumentNullException(nameof(callback)); | ||||
|         if (cronExpression.IsNullOrEmpty()) throw new ArgumentNullException(nameof(cronExpression)); | ||||
|  | ||||
|         var list = new List<Cron>(); | ||||
|         foreach (var item in cronExpression.Split(";")) | ||||
|         { | ||||
|             var cron = new Cron(); | ||||
|             if (!cron.Parse(item)) throw new ArgumentException($"Invalid Cron expression[{item}]", nameof(cronExpression)); | ||||
|  | ||||
|             list.Add(cron); | ||||
|         } | ||||
|         _crons = list.ToArray(); | ||||
|  | ||||
|         IsAsyncTask = true; | ||||
|         Async = true; | ||||
|         Absolutely = true; | ||||
|  | ||||
|         //var now = DateTime.Now; | ||||
|         var now = Scheduler.GetNow(); | ||||
|         var next = _crons.Min(e => e.GetNext(now)); | ||||
|         var ms = (Int64)(next - now).TotalMilliseconds; | ||||
|         _AbsolutelyNext = next; | ||||
|         Init(ms); | ||||
|         //Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now)); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     public bool Disposed { get; private set; } | ||||
|     /// <summary>销毁定时器</summary> | ||||
|     public void Dispose() | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
| 	 | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -8,8 +8,8 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using ThingsGateway.Common.Extension; | ||||
| using ThingsGateway.NewLife; | ||||
| using ThingsGateway.Razor.Extension; | ||||
|  | ||||
| namespace ThingsGateway.Razor; | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| <Step @ref="@step" IsVertical="true"> | ||||
|     <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]> | ||||
|         <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload> | ||||
|         <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button> | ||||
|         <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton> | ||||
|     </StepItem> | ||||
|     <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]> | ||||
|  | ||||
| @@ -41,16 +41,12 @@ | ||||
|  | ||||
|             } | ||||
|  | ||||
|             <PopConfirmButton IsAsync IsDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton> | ||||
|             <Button class="mt-2" IsAsync OnClick="() => DeviceImport()">@RazorLocalizer["Close"]</Button> | ||||
|  | ||||
| @*  | ||||
|             <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@ | ||||
|  | ||||
|         </div> | ||||
|     </StepItem> | ||||
| @*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]> | ||||
|         <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton> | ||||
|     </StepItem> *@ | ||||
|  | ||||
| </Step> | ||||
| @code { | ||||
|     [NotNull] | ||||
|   | ||||
| @@ -24,18 +24,17 @@ public partial class ImportExcel | ||||
|     /// </summary> | ||||
|     [Parameter] | ||||
|     [EditorRequired] | ||||
|     public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; } | ||||
|     public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Import { get; set; } | ||||
|  | ||||
|     [Inject] | ||||
|     [NotNull] | ||||
|     private IStringLocalizer<ImportExcel>? Localizer { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 预览 | ||||
|     /// </summary> | ||||
|     [Parameter] | ||||
|     [EditorRequired] | ||||
|     public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; } | ||||
|     [Inject] | ||||
|     [NotNull] | ||||
|     private IStringLocalizer<ThingsGateway.Razor._Imports>? RazorLocalizer { get; set; } | ||||
|  | ||||
|  | ||||
|  | ||||
|     [Inject] | ||||
|     [NotNull] | ||||
| @@ -47,13 +46,17 @@ public partial class ImportExcel | ||||
|     [CascadingParameter] | ||||
|     private Func<Task>? OnCloseAsync { get; set; } | ||||
|  | ||||
|     private async Task DeviceImport(IBrowserFile file) | ||||
|     private async Task DeviceImport() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             _importPreviews.Clear(); | ||||
|             _importPreviews = await Preview.Invoke(file); | ||||
|             await step.Next(); | ||||
|             await InvokeAsync(async () => | ||||
|             { | ||||
|                 if (OnCloseAsync != null) | ||||
|                     await OnCloseAsync(); | ||||
|                 await ToastService.Default(); | ||||
|             }); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
| @@ -67,16 +70,12 @@ public partial class ImportExcel | ||||
|         { | ||||
|             await Task.Run(async () => | ||||
|             { | ||||
|                 await Import.Invoke(_importPreviews); | ||||
|                 _importPreviews = await Import.Invoke(_importFile); | ||||
|                 _importFile = null; | ||||
|  | ||||
|                 await InvokeAsync(async () => | ||||
|                 { | ||||
|                     if (OnCloseAsync != null) | ||||
|                         await OnCloseAsync(); | ||||
|                     await ToastService.Default(); | ||||
|                 }); | ||||
|             }); | ||||
|             await step.Next(); | ||||
|  | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| @using ThingsGateway.Extension | ||||
| @namespace ThingsGateway.Razor | ||||
| <Button OnClick="() => step.Reset()">@Localizer["Reset"]</Button> | ||||
| <h6 class="my-3 green--text">@Localizer["Tip"] </h6> | ||||
| <Step @ref="@step" IsVertical="true"> | ||||
|     <StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]> | ||||
|         <InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload> | ||||
|         <Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button> | ||||
|     </StepItem> | ||||
|     <StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]> | ||||
|  | ||||
|         <div class="overflow-y-auto"> | ||||
|  | ||||
|             @foreach (var item in _importPreviews) | ||||
|             { | ||||
|                 <div class="mt-2"> | ||||
|                     @( | ||||
|                                     Localizer["UploadCount", item.Key, item.Value.DataCount] | ||||
|                                     ) | ||||
|                 </div> | ||||
|                 <div class=@((item.Value.HasError ? "my-2 red--text" : "my-2 green--text"))> | ||||
|                     @( | ||||
|                                     (item.Value.HasError ? "Error" : "Success") | ||||
|                                     ) | ||||
|             </div> | ||||
|                         if (item.Value.HasError) | ||||
|                 { | ||||
|                     <div style="height:300px;" class="overflow-y-scroll"> | ||||
|                         <Virtualize Items="item.Value.Results.Where(a => !a.Success).OrderBy(a => a.Row).ToList()" Context="item1" ItemSize="60" OverscanCount=2> | ||||
|                             <ItemContent> | ||||
|                                 <div class="row g-0"> | ||||
|                                     <span class="col mx-2">@item1.Row</span> | ||||
|                                     <span class=@((item1.Success ? "green--text col-auto" : "red--text col-auto"))> | ||||
|                                         <strong>@item1.ErrorMessage</strong> | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                             </ItemContent> | ||||
|                         </Virtualize> | ||||
|                     </div> | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|  | ||||
|             <PopConfirmButton IsAsync IsKeepDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton> | ||||
|  | ||||
| @*  | ||||
|             <Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@ | ||||
|  | ||||
|         </div> | ||||
|     </StepItem> | ||||
| @*     <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]> | ||||
|         <PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton> | ||||
|     </StepItem> *@ | ||||
| </Step> | ||||
| @code { | ||||
|     [NotNull] | ||||
|     Step? step { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,86 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Microsoft.AspNetCore.Components.Forms; | ||||
|  | ||||
| using System.ComponentModel.DataAnnotations; | ||||
|  | ||||
| namespace ThingsGateway.Razor; | ||||
|  | ||||
| /// <inheritdoc/> | ||||
| public partial class ImportExcelConfirm | ||||
| { | ||||
|     private Dictionary<string, ImportPreviewOutputBase> _importPreviews = new(); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 导入 | ||||
|     /// </summary> | ||||
|     [Parameter] | ||||
|     [EditorRequired] | ||||
|     public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; } | ||||
|  | ||||
|     [Inject] | ||||
|     [NotNull] | ||||
|     private IStringLocalizer<ImportExcelConfirm>? Localizer { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 预览 | ||||
|     /// </summary> | ||||
|     [Parameter] | ||||
|     [EditorRequired] | ||||
|     public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; } | ||||
|  | ||||
|     [Inject] | ||||
|     [NotNull] | ||||
|     private ToastService? ToastService { get; set; } | ||||
|  | ||||
|     [Required] | ||||
|     private IBrowserFile _importFile { get; set; } | ||||
|  | ||||
|     [CascadingParameter] | ||||
|     private Func<Task>? OnCloseAsync { get; set; } | ||||
|  | ||||
|     private async Task DeviceImport(IBrowserFile file) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             _importPreviews.Clear(); | ||||
|             _importPreviews = await Preview.Invoke(file); | ||||
|             await step.Next(); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             await ToastService.Warn(ex); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task SaveDeviceImport() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             await Task.Run(async () => | ||||
|             { | ||||
|                 await Import.Invoke(_importPreviews); | ||||
|                 _importFile = null; | ||||
|  | ||||
|                 await InvokeAsync(async () => | ||||
|                 { | ||||
|                     if (OnCloseAsync != null) | ||||
|                         await OnCloseAsync(); | ||||
|                     await ToastService.Default(); | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             await InvokeAsync(async () => await ToastService.Warn(ex)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| ::deep .avatar { | ||||
|     border-radius: 1.5rem; | ||||
|     width: 24px; | ||||
|     height: 24px; | ||||
|     background-color: var(--bs-red); | ||||
|     color: #fff; | ||||
|     flex: 0 0 auto; | ||||
|     font-size: 1rem; | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| @namespace ThingsGateway.Razor | ||||
|  | ||||
| @if (show) | ||||
| { | ||||
|     <Spinner class="ms-auto"></Spinner> | ||||
| } | ||||
| @@ -8,7 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Razor; | ||||
| namespace ThingsGateway.Razor; | ||||
| 
 | ||||
| public partial class SpinnerComponent | ||||
| { | ||||
| @@ -10,7 +10,7 @@ | ||||
| 
 | ||||
| using Microsoft.JSInterop; | ||||
| 
 | ||||
| namespace ThingsGateway.Common.Extension; | ||||
| namespace ThingsGateway.Razor.Extension; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// JSRuntime扩展方法 | ||||
| @@ -49,4 +49,28 @@ public static class JSRuntimeExtensions | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static async ValueTask<T> GetLocalStorage<T>(this IJSRuntime jsRuntime, string name) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             return await jsRuntime.InvokeAsync<T>("getLocalStorage", name).ConfigureAwait(false); | ||||
|         } | ||||
|         catch | ||||
|         { | ||||
|             return default; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static async ValueTask SetLocalStorage<T>(this IJSRuntime jsRuntime, string name, T data) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             await jsRuntime.InvokeVoidAsync("setLocalStorage", name, data).ConfigureAwait(false); | ||||
|         } | ||||
|         catch | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -25,7 +25,8 @@ | ||||
|     "Success": "Success", | ||||
|     "TablesExportButtonExcelText": "Export Excel", | ||||
|     "TablesImportButtonExcelText": "Import Excel", | ||||
|     "True": "Yes" | ||||
|     "True": "Yes", | ||||
|     "Info": "Info" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.About": { | ||||
|     "Community": "Community", | ||||
| @@ -59,6 +60,19 @@ | ||||
|     "SearchText": "Search Page" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.ImportExcel": { | ||||
|     "First": "Step 1", | ||||
|     "Import": "If there are no errors during verification, it will be directly imported into the database", | ||||
|     "Next": "Next", | ||||
|     "Reset": "Reset", | ||||
|     "Second": "Step 2", | ||||
|     "Third": "Step 3", | ||||
|     "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient", | ||||
|     "Upload": "Upload File", | ||||
|     "UploadCount": " Table {0}, import {1} records", | ||||
|     "Validate": "Validate", | ||||
|     "ValidateText": "Validation Content" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.ImportExcelConfirm": { | ||||
|     "First": "Step 1", | ||||
|     "Import": "Import", | ||||
|     "Next": "Next", | ||||
| @@ -67,7 +81,7 @@ | ||||
|     "Third": "Step 3", | ||||
|     "Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient", | ||||
|     "Upload": "Upload File", | ||||
|     "UploadCount": " Table {0}, expecting to import {1} records", | ||||
|     "UploadCount": " Table {0}, import {1} records", | ||||
|     "Validate": "Validate", | ||||
|     "ValidateText": "Validation Content" | ||||
|   }, | ||||
|   | ||||
| @@ -25,7 +25,8 @@ | ||||
|     "Success": "成功", | ||||
|     "TablesExportButtonExcelText": "导出Excel", | ||||
|     "TablesImportButtonExcelText": "导入Excel", | ||||
|     "True": "是" | ||||
|     "True": "是", | ||||
|     "Info": "详情" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.About": { | ||||
|     "Community": "社区", | ||||
| @@ -59,6 +60,19 @@ | ||||
|     "SearchText": "搜索页面" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.ImportExcel": { | ||||
|     "First": "第一步", | ||||
|     "Import": "若验证无错误,将直接导入数据库", | ||||
|     "Next": "下一步", | ||||
|     "Reset": "重置", | ||||
|     "Second": "第二步", | ||||
|     "Third": "第三", | ||||
|     "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待", | ||||
|     "Upload": "上传文件", | ||||
|     "UploadCount": " 表 {0},导入 {1} 条数据", | ||||
|     "Validate": "验证", | ||||
|     "ValidateText": "验证内容" | ||||
|   }, | ||||
|   "ThingsGateway.Razor.ImportExcelConfirm": { | ||||
|     "First": "第一步", | ||||
|     "Import": "导入", | ||||
|     "Next": "下一步", | ||||
| @@ -67,7 +81,7 @@ | ||||
|     "Third": "第三", | ||||
|     "Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待", | ||||
|     "Upload": "上传文件", | ||||
|     "UploadCount": " 表 {0},预计导入 {1} 条数据", | ||||
|     "UploadCount": " 表 {0},导入 {1} 条数据", | ||||
|     "Validate": "验证", | ||||
|     "ValidateText": "验证内容" | ||||
|   }, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| 	<Import Project="..\..\PackNuget.props" /> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" /> | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| // 设置 culture | ||||
| function setCultureLocalStorage(culture) { | ||||
|     localStorage.setItem("culture", culture); | ||||
| } | ||||
|  | ||||
| // 获取 culture | ||||
| function getCultureLocalStorage() { | ||||
|     return localStorage.getItem("culture"); | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| // 设置 culture | ||||
| function setCultureLocalStorage(culture) { | ||||
|     localStorage.setItem("culture", culture); | ||||
| } | ||||
|  | ||||
| // 获取 culture | ||||
| function getCultureLocalStorage() { | ||||
|     return localStorage.getItem("culture"); | ||||
| } | ||||
|  | ||||
|  function getLocalStorage(name) { | ||||
|     return JSON.parse(localStorage.getItem(name)) ?? 0; | ||||
| } | ||||
|  function setLocalStorage(name, data) { | ||||
|     if (localStorage) { | ||||
|         localStorage.setItem(name, JSON.stringify(data)); | ||||
|     } | ||||
| } | ||||
| @@ -27,16 +27,17 @@ | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertDatas">要插入的数据列表</param> | ||||
|         /// <param name="tableName">表名称</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>插入的记录数</returns> | ||||
|         public int BulkCopy<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         public int BulkCopy<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             int result = 0; | ||||
|             // 使用分页方式处理大数据量插入 | ||||
|             db.Utilities.PageEach(insertDatas, pageSize, pageItems => | ||||
|             { | ||||
|                 // 同步调用批量插入API并累加结果 | ||||
|                 result += questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).GetAwaiter().GetResult(); | ||||
|                 result += questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).GetAwaiter().GetResult(); | ||||
|             }); | ||||
|             return result; | ||||
|         } | ||||
| @@ -46,16 +47,17 @@ | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertDatas">要插入的数据列表</param> | ||||
|         /// <param name="tableName">表名称</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>插入的记录数</returns> | ||||
|         public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             int result = 0; | ||||
|             // 异步分页处理大数据量插入 | ||||
|             await db.Utilities.PageEachAsync(insertDatas, pageSize, async pageItems => | ||||
|             { | ||||
|                 // 异步调用批量插入API并累加结果 | ||||
|                 result += await questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).ConfigureAwait(false); | ||||
|                 result += await questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).ConfigureAwait(false); | ||||
|             }).ConfigureAwait(false); | ||||
|             return result; | ||||
|         } | ||||
|   | ||||
| @@ -7,16 +7,12 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// <summary> | ||||
|         /// 绑定RestAPI需要的信息 | ||||
|         /// </summary> | ||||
|         public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string httpPort, ref string username, ref string password) | ||||
|         public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string username, ref string password) | ||||
|         { | ||||
|             if (builder.TryGetValue("Host", out object hostValue)) | ||||
|             { | ||||
|                 host = Convert.ToString(hostValue); | ||||
|             } | ||||
|             if (builder.TryGetValue("HttpPort", out object httpPortValue)) | ||||
|             { | ||||
|                 httpPort = Convert.ToString(httpPortValue); | ||||
|             } | ||||
|             if (builder.TryGetValue("Username", out object usernameValue)) | ||||
|             { | ||||
|                 username = Convert.ToString(usernameValue); | ||||
|   | ||||
| @@ -28,16 +28,16 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// 初始化 QuestDbRestAPI 实例 | ||||
|         /// </summary> | ||||
|         /// <param name="db">SqlSugar 数据库客户端</param> | ||||
|         public QuestDbRestAPI(ISqlSugarClient db) | ||||
|         /// <param name="httpPort">restApi端口</param> | ||||
|         public QuestDbRestAPI(ISqlSugarClient db, int httpPort = 9000) | ||||
|         { | ||||
|             var builder = new DbConnectionStringBuilder(); | ||||
|             builder.ConnectionString = db.CurrentConnectionConfig.ConnectionString; | ||||
|             this.db = db; | ||||
|             string httpPort = String.Empty; | ||||
|             string host = String.Empty; | ||||
|             string username = String.Empty; | ||||
|             string password = String.Empty; | ||||
|             QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref httpPort, ref username, ref password); | ||||
|             QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref username, ref password); | ||||
|             BindHost(host, httpPort, username, password); | ||||
|         } | ||||
|  | ||||
| @@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar | ||||
|             // HTTP GET 请求执行SQL | ||||
|             var result = string.Empty; | ||||
|             var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}"; | ||||
|  | ||||
|             var request = new HttpRequestMessage(HttpMethod.Get, url); | ||||
|             if (!string.IsNullOrWhiteSpace(authorization)) | ||||
|                 client.DefaultRequestHeaders.Add("Authorization", authorization); | ||||
|             var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false); | ||||
|             { | ||||
|                 request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); | ||||
|             } | ||||
|  | ||||
|             using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false); | ||||
|             result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|             return result; | ||||
|         } | ||||
| @@ -68,34 +73,34 @@ namespace ThingsGateway.SqlSugar | ||||
|             return ExecuteCommandAsync(sql).GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 异步批量插入单条数据 | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertData">要插入的数据</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>影响的行数</returns> | ||||
|         public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             if (db.CurrentConnectionConfig.MoreSettings == null) | ||||
|                 db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings(); | ||||
|             db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true; | ||||
|             var sql = db.InsertableT(insertData).ToSqlString(); | ||||
|             var result = await ExecuteCommandAsync(sql).ConfigureAwait(false); | ||||
|             return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0; | ||||
|         } | ||||
|         ///// <summary> | ||||
|         ///// 异步批量插入单条数据 | ||||
|         ///// </summary> | ||||
|         ///// <typeparam name="T">数据类型</typeparam> | ||||
|         ///// <param name="insertData">要插入的数据</param> | ||||
|         ///// <param name="dateFormat">日期格式字符串</param> | ||||
|         ///// <returns>影响的行数</returns> | ||||
|         //public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         //{ | ||||
|         //    if (db.CurrentConnectionConfig.MoreSettings == null) | ||||
|         //        db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings(); | ||||
|         //    db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true; | ||||
|         //    var sql = db.InsertableT(insertData).ToSqlString(); | ||||
|         //    var result = await ExecuteCommandAsync(sql).ConfigureAwait(false); | ||||
|         //    return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0; | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 同步批量插入单条数据 | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertData">要插入的数据</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>影响的行数</returns> | ||||
|         public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult(); | ||||
|         } | ||||
|         ///// <summary> | ||||
|         ///// 同步批量插入单条数据 | ||||
|         ///// </summary> | ||||
|         ///// <typeparam name="T">数据类型</typeparam> | ||||
|         ///// <param name="insertData">要插入的数据</param> | ||||
|         ///// <param name="dateFormat">日期格式字符串</param> | ||||
|         ///// <returns>影响的行数</returns> | ||||
|         //public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         //{ | ||||
|         //    return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult(); | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 创建分页批量插入器 | ||||
| @@ -115,9 +120,10 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertList">要插入的数据列表</param> | ||||
|         /// <param name="tableName">表名称</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>插入的记录数</returns> | ||||
|         public async Task<int> BulkCopyAsync<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         public async Task<int> BulkCopyAsync<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             var result = 0; | ||||
|             var fileName = $"{Guid.NewGuid()}.csv"; | ||||
| @@ -126,35 +132,43 @@ namespace ThingsGateway.SqlSugar | ||||
|             { | ||||
|                 // 准备多部分表单数据 | ||||
|                 var boundary = "---------------" + DateTime.Now.Ticks.ToString("x"); | ||||
|                 var list = new List<Hashtable>(); | ||||
|                 var name = db.EntityMaintenance.GetEntityInfo<T>().DbTableName; | ||||
|  | ||||
|                 tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName; | ||||
|  | ||||
|                 // 获取或创建列信息缓存 | ||||
|                 var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode(); | ||||
|                 var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () => | ||||
|                  db.CopyNew().DbMaintenance.GetColumnInfosByTableName(name)); | ||||
|  | ||||
|                 // 构建schema信息 | ||||
|                 columns.ForEach(d => | ||||
|                  db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName)); | ||||
|                 var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () => | ||||
|                 { | ||||
|                     if (d.DataType == "TIMESTAMP") | ||||
|                     var list = new List<Hashtable>(); | ||||
|  | ||||
|                     // 构建schema信息 | ||||
|                     columns.ForEach(d => | ||||
|                     { | ||||
|                         list.Add(new Hashtable() | ||||
|                         if (d.DataType == "TIMESTAMP") | ||||
|                         { | ||||
|                             list.Add(new Hashtable() | ||||
|                         { | ||||
|                             { "name", d.DbColumnName }, | ||||
|                             { "type", d.DataType }, | ||||
|                             { "pattern", dateFormat} | ||||
|                         }); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         list.Add(new Hashtable() | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             list.Add(new Hashtable() | ||||
|                         { | ||||
|                             { "name", d.DbColumnName }, | ||||
|                             { "type", d.DataType } | ||||
|                         }); | ||||
|                     } | ||||
|                 }); | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     return list; | ||||
|                 } | ||||
|  ); | ||||
|  | ||||
|                 var schema = JsonConvert.SerializeObject(list); | ||||
|  | ||||
|                 // 写入CSV文件 | ||||
| @@ -170,8 +184,8 @@ namespace ThingsGateway.SqlSugar | ||||
|                 // 准备HTTP请求内容 | ||||
|                 using var httpContent = new MultipartFormDataContent(boundary); | ||||
|                 using var fileStream = File.OpenRead(filePath); | ||||
|                 if (!string.IsNullOrWhiteSpace(this.authorization)) | ||||
|                     client.DefaultRequestHeaders.Add("Authorization", this.authorization); | ||||
|                 //if (!string.IsNullOrWhiteSpace(this.authorization)) | ||||
|                 //    client.DefaultRequestHeaders.Add("Authorization", this.authorization); | ||||
|                 httpContent.Add(new StringContent(schema), "schema"); | ||||
|                 var streamContent = new StreamContent(fileStream); | ||||
|                 streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); | ||||
| @@ -183,8 +197,8 @@ namespace ThingsGateway.SqlSugar | ||||
|                     "multipart/form-data; boundary=" + boundary); | ||||
|  | ||||
|                 // 发送请求并处理响应 | ||||
|                 var httpResponseMessage = | ||||
|                     await Post(client, name, httpContent).ConfigureAwait(false); | ||||
|                 using var httpResponseMessage = | ||||
|                       await Post(client, tableName, httpContent).ConfigureAwait(false); | ||||
|                 var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|                 var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync); | ||||
|  | ||||
| @@ -266,11 +280,12 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">数据类型</typeparam> | ||||
|         /// <param name="insertList">要插入的数据列表</param> | ||||
|         /// <param name="tableName">表名称</param> | ||||
|         /// <param name="dateFormat">日期格式字符串</param> | ||||
|         /// <returns>插入的记录数</returns> | ||||
|         public int BulkCopy<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         public int BulkCopy<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new() | ||||
|         { | ||||
|             return BulkCopyAsync(insertList, dateFormat).GetAwaiter().GetResult(); | ||||
|             return BulkCopyAsync(insertList, tableName, dateFormat).GetAwaiter().GetResult(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
| @@ -280,7 +295,7 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// <param name="httpPort">HTTP端口</param> | ||||
|         /// <param name="username">用户名</param> | ||||
|         /// <param name="password">密码</param> | ||||
|         private void BindHost(string host, string httpPort, string username, string password) | ||||
|         private void BindHost(string host, int httpPort, string username, string password) | ||||
|         { | ||||
|             url = host; | ||||
|             if (url.EndsWith('/')) | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
| { | ||||
|     public static class QuestDbSqlSugarClientExtensions | ||||
|     { | ||||
|         public static QuestDbRestAPI RestApi(this ISqlSugarClient db) | ||||
|         public static QuestDbRestAPI RestApi(this ISqlSugarClient db, int httpPort = 9000) | ||||
|         { | ||||
|             return new QuestDbRestAPI(db); | ||||
|             return new QuestDbRestAPI(db, httpPort); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -111,7 +111,7 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// <param name="context">SqlSugar提供者</param> | ||||
|         /// <param name="dataRecord">数据记录器</param> | ||||
|         /// <param name="fieldNames">字段名列表</param> | ||||
|         public IDataReaderEntityBuilder(SqlSugarProvider context, IDataRecord dataRecord, List<string> fieldNames) | ||||
|         public IDataReaderEntityBuilder(SqlSugarProvider context, IDataRecord dataRecord, IEnumerable<string> fieldNames) | ||||
|         { | ||||
|             this.Context = context; | ||||
|             this.DataRecord = dataRecord; | ||||
|   | ||||
| @@ -679,7 +679,7 @@ namespace ThingsGateway.SqlSugar | ||||
|             IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|             { | ||||
|                 var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr, | ||||
|                     columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T)); | ||||
|                     columns.Select(it => it.Item1)).CreateBuilder(typeof(T)); | ||||
|                 return cacheResult; | ||||
|             }); | ||||
|             using (dr) | ||||
| @@ -706,7 +706,7 @@ namespace ThingsGateway.SqlSugar | ||||
|             IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|             { | ||||
|                 var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr, | ||||
|                     columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T)); | ||||
|                     columns.Select(it => it.Item1)).CreateBuilder(typeof(T)); | ||||
|                 return cacheResult; | ||||
|             }); | ||||
|             if (cancellationToken.IsCancellationRequested) yield break; | ||||
| @@ -743,7 +743,7 @@ namespace ThingsGateway.SqlSugar | ||||
|             IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|             { | ||||
|                 var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr, | ||||
|                     columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T)); | ||||
|                     columns.Select(it => it.Item1)).CreateBuilder(typeof(T)); | ||||
|                 return cacheResult; | ||||
|             }); | ||||
|             using (dr) | ||||
| @@ -775,7 +775,7 @@ namespace ThingsGateway.SqlSugar | ||||
|             IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|             { | ||||
|                 var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr, | ||||
|                     columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T)); | ||||
|                     columns.Select(it => it.Item1)).CreateBuilder(typeof(T)); | ||||
|                 return cacheResult; | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,6 @@ | ||||
|         V Get<V>(string key); | ||||
|         IEnumerable<string> GetAllKey<V>(); | ||||
|         void Remove<V>(string key); | ||||
|         V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue); | ||||
|         V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600); | ||||
|     } | ||||
| } | ||||
| @@ -31,7 +31,7 @@ namespace ThingsGateway.SqlSugar | ||||
|             return ReflectionInoCore<V>.GetInstance().GetAllKey(); | ||||
|         } | ||||
|  | ||||
|         public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue) | ||||
|         public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600) | ||||
|         { | ||||
|             return ReflectionInoCore<V>.GetInstance().GetOrCreate(cacheKey, create); | ||||
|         } | ||||
| @@ -43,10 +43,13 @@ namespace ThingsGateway.SqlSugar | ||||
|     } | ||||
|     public class ReflectionInoCore<V> | ||||
|     { | ||||
|         private MemoryCache InstanceCache => MemoryCache.Instance; | ||||
|         private MemoryCache InstanceCache = new MemoryCache() { Expire = 180 }; | ||||
|         private static ReflectionInoCore<V> _instance = null; | ||||
|         private static readonly object _instanceLock = new object(); | ||||
|         private ReflectionInoCore() { } | ||||
|         private ReflectionInoCore() | ||||
|         { | ||||
|  | ||||
|         } | ||||
|  | ||||
|         public V this[string key] | ||||
|         { | ||||
| @@ -86,7 +89,7 @@ namespace ThingsGateway.SqlSugar | ||||
|  | ||||
|         public void Add(string key, V value, int cacheDurationInSeconds) | ||||
|         { | ||||
|             Check.ThrowNotSupportedException("ReflectionInoCache.Add(string key, V value, int cacheDurationInSeconds)"); | ||||
|             this.InstanceCache.Add<V>(key, value, cacheDurationInSeconds); | ||||
|         } | ||||
|  | ||||
|         public void Remove(string key) | ||||
| @@ -107,9 +110,10 @@ namespace ThingsGateway.SqlSugar | ||||
|             return this.InstanceCache.Keys; | ||||
|         } | ||||
|  | ||||
|         public V GetOrCreate(string cacheKey, Func<V> create) | ||||
|         public V GetOrCreate(string cacheKey, Func<V> create, int expire = 3600) | ||||
|         { | ||||
|             return InstanceCache.GetOrAdd<V>(cacheKey, (a) => create()); | ||||
|             return InstanceCache.GetOrAdd<V>(cacheKey, (a) => | ||||
|             create(), expire); | ||||
|         } | ||||
|     } | ||||
|     public static class ReflectionInoHelper | ||||
|   | ||||
| @@ -447,6 +447,28 @@ namespace ThingsGateway.SqlSugar | ||||
|         } | ||||
|  | ||||
|         public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true) | ||||
|         { | ||||
|  | ||||
|             if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>(); | ||||
|             string cacheKey = "QuestDB.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId; | ||||
|             cacheKey = GetCacheKey(cacheKey); | ||||
|  | ||||
|             if (isCache) | ||||
|             { | ||||
|  | ||||
|                 return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|                 { | ||||
|                     return GetColInfo(tableName); | ||||
|                 }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return GetColInfo(tableName); | ||||
|  | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private List<DbColumnInfo> GetColInfo(string tableName) | ||||
|         { | ||||
|             var sql = String.Format(GetColumnInfosByTableNameSql, tableName); | ||||
|             List<DbColumnInfo> result = new List<DbColumnInfo>(); | ||||
|   | ||||
| @@ -406,22 +406,24 @@ AND sql LIKE '%" + tableName + "%'"); | ||||
|         public override bool CreateDatabase(string databaseName, string databaseDirectory = null) | ||||
|         { | ||||
|             var connString = this.Context.CurrentConnectionConfig.ConnectionString; | ||||
|             var path = Regex.Match(connString, @"[a-z,A-Z]\:\\.+\\").Value; | ||||
|             if (path.IsNullOrEmpty()) | ||||
|  | ||||
|  | ||||
|             // 提取 Data Source=xxx(不管是绝对还是相对路径) | ||||
|             var match = Regex.Match(connString, @"(?i)Data\s+Source\s*=\s*(.+?)(;|$)"); | ||||
|             if (match.Success) | ||||
|             { | ||||
|                 path = Regex.Match(connString, @"\/.+\/").Value; | ||||
|             } | ||||
|             if (path.IsNullOrEmpty()) | ||||
|             { | ||||
|                 path = Regex.Match(connString, @"[a-z,A-Z]\:\\").Value; | ||||
|             } | ||||
|             if (!path.IsNullOrEmpty()) | ||||
|             { | ||||
|                 if (!FileHelper.IsExistDirectory(path)) | ||||
|                 var filePath = match.Groups[1].Value.Trim(); // => ./DB/data.sqlite | ||||
|                 var folderPath = Path.GetDirectoryName(filePath); // => ./DB | ||||
|  | ||||
|                 if (!folderPath.IsNullOrEmpty()) | ||||
|                 { | ||||
|                     FileHelper.CreateDirectory(path); | ||||
|                     if (!FileHelper.IsExistDirectory(folderPath)) | ||||
|                     { | ||||
|                         FileHelper.CreateDirectory(folderPath); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.Context.Ado.Connection.Open(); | ||||
|             this.Context.Ado.Connection.Close(); | ||||
|             return true; | ||||
|   | ||||
| @@ -717,8 +717,32 @@ namespace ThingsGateway.SqlSugar | ||||
|         /// <returns>列信息列表</returns> | ||||
|         public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true) | ||||
|         { | ||||
|             var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 "; | ||||
|  | ||||
|             if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>(); | ||||
|             string cacheKey = "TDengine.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId; | ||||
|             cacheKey = GetCacheKey(cacheKey); | ||||
|  | ||||
|             if (isCache) | ||||
|             { | ||||
|  | ||||
|                 return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () => | ||||
|                      { | ||||
|                          return GetColInfo(tableName); | ||||
|                      }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return GetColInfo(tableName); | ||||
|  | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         private List<DbColumnInfo> GetColInfo(string tableName) | ||||
|         { | ||||
|             List<DbColumnInfo> result = new List<DbColumnInfo>(); | ||||
|  | ||||
|             var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 "; | ||||
|             DataTable dt = null; | ||||
|             try | ||||
|             { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<GenerateDocumentationFile>True</GenerateDocumentationFile> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0;net9.0;</TargetFrameworks> | ||||
| @@ -22,20 +23,21 @@ | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="SqlSugarCore.Dm" Version="8.8.0" /> | ||||
| 		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.728" /> | ||||
| 		<PackageReference Include="SqlSugarCore.Dm" Version="8.8.1" /> | ||||
| 		<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.821" /> | ||||
| 		<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.20" /> | ||||
| 		<!--<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />--> | ||||
| 		<PackageReference Include="MySqlConnector" Version="2.4.0" /> | ||||
| 		<PackageReference Include="Npgsql" Version="9.0.3" /> | ||||
| 		<PackageReference Include="CsvHelper" Version="33.1.0" /> | ||||
| 		<PackageReference Include="TDengine.Connector" Version="3.1.7" /> | ||||
| 		<PackageReference Include="TDengine.Connector" Version="3.1.8" /> | ||||
| 		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" /> | ||||
| 		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.20" /> | ||||
| 		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.23" /> | ||||
| 		<PackageReference Include="System.Data.Common" Version="4.3.0" /> | ||||
| 		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.0" /> | ||||
| 		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" /> | ||||
| 		<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" /> | ||||
| 		<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> | ||||
| 		<PackageReference Include="System.Formats.Asn1" Version="8.0.2" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| <Project> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<PluginVersion>10.9.91</PluginVersion> | ||||
| 		<ProPluginVersion>10.9.91</ProPluginVersion> | ||||
| 		<DefaultVersion>10.9.93</DefaultVersion> | ||||
| 		<AuthenticationVersion>2.9.29</AuthenticationVersion> | ||||
| 		<SourceGeneratorVersion>10.9.29</SourceGeneratorVersion> | ||||
| 		<NET8Version>8.0.18</NET8Version> | ||||
| 		<NET9Version>9.0.7</NET9Version> | ||||
| 		<PluginVersion>10.11.17</PluginVersion> | ||||
| 		<ProPluginVersion>10.11.17</ProPluginVersion> | ||||
| 		<DefaultVersion>10.11.17</DefaultVersion> | ||||
| 		<AuthenticationVersion>10.11.2</AuthenticationVersion> | ||||
| 		<SourceGeneratorVersion>10.11.2</SourceGeneratorVersion> | ||||
| 		<NET8Version>8.0.19</NET8Version> | ||||
| 		<NET9Version>9.0.8</NET9Version> | ||||
| 		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages> | ||||
| 		<IsTrimmable>false</IsTrimmable> | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| @@ -27,7 +28,8 @@ | ||||
| 		<AnalysisModeStyle>None</AnalysisModeStyle> | ||||
|  | ||||
| 		<NoWarn> | ||||
| 			CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;NETSDK1206 | ||||
| 			CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;RCS1102;RCS1194;NETSDK1206 | ||||
|  | ||||
| 		</NoWarn> | ||||
| 		<TargetFrameworks>net8.0;</TargetFrameworks> | ||||
| 		<LangVersion>13.0</LangVersion> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <Project> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net462;netstandard2.0;net6.0;</TargetFrameworks> | ||||
| 		<TargetFrameworks>net462;netstandard2.0;net6.0;net8.0</TargetFrameworks> | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -4,10 +4,11 @@ | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>netstandard2.0;</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="CS-Script" Version="4.10.0" /> | ||||
| 		<PackageReference Include="CS-Script" Version="4.11.0" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -89,7 +89,7 @@ public partial class ChannelComponent : ComponentBase | ||||
|                 await Channel.SetupAsync(config); | ||||
|             } | ||||
|  | ||||
|             await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, default); | ||||
|             await Channel.ConnectAsync(default); | ||||
|  | ||||
|             if (OnConnectClick.HasDelegate) | ||||
|                 await OnConnectClick.InvokeAsync(Channel); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|             <EditorItem @bind-Field=Model.EncodingName> | ||||
|                 <EditTemplate Context="value"> | ||||
|                     <div class="col-12 col-sm-4"> | ||||
|                         <Select @bind-Value=value.EncodingName Items="EncodingItems" /> | ||||
|                         <Select @bind-Value=value.EncodingName Items="EncodingItems" IsClearable/> | ||||
|                     </div> | ||||
|                 </EditTemplate> | ||||
|             </EditorItem> | ||||
|   | ||||
| @@ -31,6 +31,6 @@ public partial class ConverterConfigComponent : ComponentBase | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.VariableStringLength), true); | ||||
|         EncodingItems = new List<SelectedItem>() { new SelectedItem("", "none") }.Concat(Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName))).ToList(); | ||||
|         EncodingItems = Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName)).ToList(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -77,7 +77,8 @@ public partial class LogConsole : IDisposable | ||||
|  | ||||
|     [Inject] | ||||
|     private ToastService ToastService { get; set; } | ||||
|  | ||||
|     [Inject] | ||||
|     ITextFileReadService TextFileReadService { get; set; } | ||||
|     public void Dispose() | ||||
|     { | ||||
|         Disposed = true; | ||||
| @@ -94,7 +95,7 @@ public partial class LogConsole : IDisposable | ||||
|  | ||||
|             if (LogPath != null) | ||||
|             { | ||||
|                 var files = TextFileReader.GetFiles(LogPath); | ||||
|                 var files = await TextFileReadService.GetLogFilesAsync(LogPath); | ||||
|                 if (!files.IsSuccess) | ||||
|                 { | ||||
|                     Messages = new List<LogMessage>(); | ||||
| @@ -105,7 +106,7 @@ public partial class LogConsole : IDisposable | ||||
|                     await Task.Run(async () => | ||||
|                     { | ||||
|                         Stopwatch sw = Stopwatch.StartNew(); | ||||
|                         var result = TextFileReader.LastLog(files.Content.FirstOrDefault()); | ||||
|                         var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault()); | ||||
|                         if (result.IsSuccess) | ||||
|                         { | ||||
|                             Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList(); | ||||
| @@ -143,7 +144,7 @@ public partial class LogConsole : IDisposable | ||||
|     { | ||||
|         if (LogPath != null) | ||||
|         { | ||||
|             var files = TextFileReader.GetFiles(LogPath); | ||||
|             var files = await TextFileReadService.GetLogFilesAsync(LogPath); | ||||
|             if (files.IsSuccess) | ||||
|             { | ||||
|                 foreach (var item in files.Content) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Razor; | ||||
| namespace ThingsGateway.Debug; | ||||
| 
 | ||||
| public class ValueTransformConfig | ||||
| { | ||||
| @@ -1,9 +1,5 @@ | ||||
| @namespace ThingsGateway.Gateway.Razor | ||||
| @using ThingsGateway.Admin.Application | ||||
| @using ThingsGateway.Admin.Razor | ||||
| @namespace ThingsGateway.Debug | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.Gateway.Application | ||||
| @inherits ComponentDefault | ||||
| 
 | ||||
| <ValidateForm class="p-4 h-100" Model="@ValueTransformConfig" OnValidSubmit="OnSave"> | ||||
|     <EditorForm AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=1 LabelWidth=150 Model="ValueTransformConfig"> | ||||
| @@ -14,7 +14,7 @@ using System.Text.RegularExpressions; | ||||
| 
 | ||||
| using ThingsGateway.NewLife.Extension; | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Razor; | ||||
| namespace ThingsGateway.Debug; | ||||
| 
 | ||||
| public partial class ValueTransformConfigPage | ||||
| { | ||||
| @@ -205,5 +205,10 @@ public partial class ValueTransformConfigPage | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Inject] | ||||
|     ToastService ToastService { get; set; } | ||||
| 
 | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     #endregion 修改 | ||||
| } | ||||
| @@ -1,4 +1,23 @@ | ||||
| { | ||||
|  | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformType": { | ||||
|     "None": "None", | ||||
|     "Linear": "Linear", | ||||
|     "Sqrt": "Sqrt" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformConfig": { | ||||
|     "TransformType": "TransformType", | ||||
|     "MinMax": "MinMax", | ||||
|     "ClampToRawRange": "ClampToRawRange", | ||||
|     "DecimalPlaces": "DecimalPlaces", | ||||
|     "RawMin": "RawMin", | ||||
|     "RawMax": "RawMax", | ||||
|     "ActualMin": "ActualMin", | ||||
|     "ActualMax": "ActualMax" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ChannelComponent": { | ||||
|     "BaudRate": "Baud Rate", | ||||
|     "BindUrl": "Local Bind IP Address", | ||||
|   | ||||
| @@ -1,4 +1,21 @@ | ||||
| { | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformType": { | ||||
|     "None": "无", | ||||
|     "Linear": "线性", | ||||
|     "Sqrt": "开方" | ||||
|   }, | ||||
|   "ThingsGateway.Debug.ValueTransformConfig": { | ||||
|     "TransformType": "转换方式", | ||||
|     "MinMax": "最小最大值", | ||||
|     "ClampToRawRange": "限制范围", | ||||
|     "DecimalPlaces": "保留小数位", | ||||
|     "RawMin": "原始最小值", | ||||
|     "RawMax": "原始最大值", | ||||
|     "ActualMin": "实际最小值", | ||||
|     "ActualMax": "实际最大值" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ChannelComponent": { | ||||
|     "BaudRate": "波特率", | ||||
|     "BindUrl": "本地url", | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user