mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-10-31 07:33:58 +08:00 
			
		
		
		
	Compare commits
	
		
			45 Commits
		
	
	
		
			10.10.5.0
			...
			10.11.23.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 179ca0aa0e | ||
|   | fc09a52da1 | ||
|   | 5436b91c89 | ||
|   | d1c46f51a6 | ||
|   | 4539d8d198 | ||
|   | 9ea9529a5f | ||
|   | 4e6be23aac | ||
|   | 2fabbd236b | ||
|   | 163a66530e | ||
|   | 29073a00c4 | ||
|   | c6d4d1ecfa | ||
|   | ba16889cad | ||
|   | 5aaed35b0f | ||
|   | df067c91eb | ||
|   | 2078b4a60b | ||
|   | 20a2e3ff8e | ||
|   | 61a973b1b5 | ||
|   | cbd72e2081 | ||
|   | 4e0377b20c | ||
|   | fd318d3cdc | ||
|   | 515bdb9700 | ||
|   | 46c1780017 | ||
|   | fe78a4c3ca | ||
|   | 2d7effadf9 | ||
|   | 346c560f8b | ||
|   | 8e3bd89f61 | ||
|   | 6da142d080 | ||
|   | ff7d029e6f | ||
|   | 21b4695683 | ||
|   | 02ad494a26 | ||
|   | 280366e1b2 | ||
|   | 6660ce3e34 | ||
|   | 7499162c1a | ||
|   | 40208a5cd6 | ||
|   | fa347f4f68 | ||
|   | d7df6fc605 | ||
|   | eb4bb2fd48 | ||
|   | faa9858974 | ||
|   | 1b3d2dda49 | ||
|   | a8a9453611 | ||
|   | e84f42ce14 | ||
|   | 6f814cf6b8 | ||
|   | e36432e4e9 | ||
|   | ebd71e807b | ||
|   | 34000d8d7d | 
							
								
								
									
										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> | ||||
| 		 | ||||
| 		<!-- 可选 --> | ||||
|  | ||||
| 		 | ||||
| @@ -26,6 +27,6 @@ | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" /> | ||||
| 		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" Private="false" /> | ||||
| 	</ItemGroup> | ||||
| </Project> | ||||
|   | ||||
| @@ -37,9 +37,8 @@ public class FileController : ControllerBase | ||||
|         var root = Directory.GetCurrentDirectory(); | ||||
|         var wwwroot = Path.Combine(root, "wwwroot"); | ||||
|         var filePath = Path.Combine(wwwroot, fileName); | ||||
|         // 防止路径穿越攻击 | ||||
| #pragma warning disable CA3003 | ||||
|         if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath)) | ||||
|         if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath)) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| @@ -49,6 +48,6 @@ public class FileController : ControllerBase | ||||
|  | ||||
|         Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition"); | ||||
|  | ||||
|         return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_'))); | ||||
|         return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_'))); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -38,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> | ||||
|   | ||||
| @@ -89,8 +89,8 @@ | ||||
|                     </div> | ||||
|                 </Side> | ||||
|                 <Main> | ||||
|                         <Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowContextMenuFullScreen="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true | ||||
|                              AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems" | ||||
|                         <Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true | ||||
|                          ShowFullscreenToolbarButton=false ShowContextMenuFullScreen=false ShowFullScreen=false AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems" | ||||
|                              DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=> | ||||
|                              { | ||||
|                              return Task.FromResult(!(a.Url=="/"||a.Url.IsNullOrEmpty())); | ||||
|   | ||||
| @@ -45,7 +45,7 @@ | ||||
|     </app> | ||||
|      | ||||
|     <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script> | ||||
|     <script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script> | ||||
|     <script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script> | ||||
|     <script src="_framework/blazor.server.js"></script> | ||||
|  | ||||
|        <!-- PWA Service Worker --> | ||||
|   | ||||
| @@ -15,6 +15,7 @@ using System.Text; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.DB; | ||||
| using ThingsGateway.NewLife; | ||||
| using ThingsGateway.NewLife.Log; | ||||
|  | ||||
| namespace ThingsGateway.AdminServer; | ||||
| @@ -64,7 +65,7 @@ public class Program | ||||
|             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||||
|                 builder.Host.UseSystemd(); | ||||
|  | ||||
|             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||
|             if (Runtime.IsLegacyWindows) | ||||
|                 builder.Logging.ClearProviders(); //去除默认的事件日志提供者,某些情况下会日志输出异常,导致程序崩溃 | ||||
|         }).ConfigureBuilder(builder => | ||||
|         { | ||||
|   | ||||
| @@ -45,11 +45,11 @@ public class Startup : AppStartup | ||||
|             options.ServicesStopConcurrently = true; | ||||
|         }); | ||||
|  | ||||
|         //// 事件总线 | ||||
|         //services.AddEventBus(options => | ||||
|         //{ | ||||
|         // 事件总线 | ||||
|         services.AddEventBus(options => | ||||
|         { | ||||
|  | ||||
|         //}); | ||||
|         }); | ||||
|  | ||||
|         // 任务调度 | ||||
|         services.AddSchedule(options => options.AddPersistence<JobPersistence>()); | ||||
| @@ -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' " />--> | ||||
|   | ||||
| @@ -30,9 +30,27 @@ public class ImportPreviewOutputBase | ||||
|     /// <summary> | ||||
|     /// 返回状态 | ||||
|     /// </summary> | ||||
|     public ConcurrentList<(int Row, bool Success, string? ErrorMessage)> Results { get; set; } = new(); | ||||
|     public ConcurrentList<ImportPreviewResult> Results { get; set; } = new(); | ||||
| } | ||||
| public class ImportPreviewResult | ||||
| { | ||||
|     public ImportPreviewResult() | ||||
|     { | ||||
|  | ||||
|     } | ||||
|     public ImportPreviewResult(int row, bool success, string error) | ||||
|     { | ||||
|         this.Row = row; | ||||
|         this.Success = success; | ||||
|         this.ErrorMessage = error; | ||||
|     } | ||||
|  | ||||
|     public int Row { get; set; } | ||||
|  | ||||
|     public bool Success { get; set; } | ||||
|  | ||||
|     public string? ErrorMessage { get; set; } | ||||
| } | ||||
| /// <summary> | ||||
| /// 导入预览 | ||||
| /// </summary> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Application; | ||||
| namespace ThingsGateway.Common; | ||||
| 
 | ||||
| public class SmartTriggerScheduler | ||||
| { | ||||
| @@ -8,7 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Application; | ||||
| namespace ThingsGateway.Common; | ||||
| 
 | ||||
| public sealed class StringOrdinalIgnoreCaseEqualityComparer : EqualityComparer<string> | ||||
| { | ||||
| @@ -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.9.0" /> | ||||
| 		<PackageReference Include="BootstrapBlazor" Version="9.10.0" /> | ||||
| 	</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; } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| // ------------------------------------------------------------------------------ | ||||
|  | ||||
| using Microsoft.AspNetCore.Components.Forms; | ||||
| using Microsoft.AspNetCore.Http; | ||||
|  | ||||
| namespace ThingsGateway.DB; | ||||
|  | ||||
| @@ -41,4 +42,31 @@ public static class FileExtensions | ||||
|         } | ||||
|         return fileName; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 存储本地文件 | ||||
|     /// </summary> | ||||
|     /// <param name="pPath">存储的第一层目录</param> | ||||
|     /// <param name="file"></param> | ||||
|     /// <returns>文件全路径</returns> | ||||
|     public static async Task<string> StorageLocal(this IFormFile file, string pPath = "imports") | ||||
|     { | ||||
|         string uploadFileFolder = App.WebHostEnvironment?.WebRootPath ?? "wwwroot"!;//赋值路径 | ||||
|         var now = CommonUtils.GetSingleId(); | ||||
|         var filePath = Path.Combine(uploadFileFolder, pPath); | ||||
|         if (!Directory.Exists(filePath))//如果不存在就创建文件夹 | ||||
|             Directory.CreateDirectory(filePath); | ||||
|         //var fileSuffix = Path.GetExtension(file.Name).ToLower();// 文件后缀 | ||||
|         var fileObjectName = $"{now}{file.Name}";//存储后的文件名 | ||||
|         var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路径 | ||||
|         fileName = fileName.Replace("\\", "/");//格式化一系 | ||||
|         //存储文件 | ||||
|         using (var stream = File.Create(Path.Combine(filePath, fileObjectName))) | ||||
|         { | ||||
|             await file.CopyToAsync(stream).ConfigureAwait(false); | ||||
|         } | ||||
|         return fileName; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions | ||||
|         return datas; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null) | ||||
|     { | ||||
|         if (queryFunc != null) | ||||
| @@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions | ||||
|         }; | ||||
|         var items = datas.GetData(option, out var totalCount, where); | ||||
|         ret.TotalCount = totalCount; | ||||
|  | ||||
|         if (totalCount > 0) | ||||
|         { | ||||
|             if (!items.Any() && option.PageIndex != 1) | ||||
|             { | ||||
|                 option.PageIndex = 1; | ||||
|                 items = datas.GetData(option, out totalCount, where); | ||||
|             } | ||||
|         } | ||||
|         ret.Items = items.ToList(); | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 根据查询条件返回QueryData | ||||
|     /// </summary> | ||||
|     public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null) | ||||
|     { | ||||
|         var ret = new QueryData<SelectedItem>() | ||||
|         { | ||||
|             IsSorted = false, | ||||
|             IsFiltered = false, | ||||
|             IsAdvanceSearch = false, | ||||
|             IsSearch = !option.SearchText.IsNullOrWhiteSpace() | ||||
|         }; | ||||
|  | ||||
|         var items = datas.Skip((option.StartIndex)).Take(option.Count); | ||||
|         ret.TotalCount = datas.Count(); | ||||
|  | ||||
|         ret.Items = func(items).ToList(); | ||||
|         return ret; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -209,16 +209,10 @@ public static class SqlSugarExtensions | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new() | ||||
|     public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new() | ||||
|     { | ||||
|         return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0; | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new() | ||||
|     { | ||||
|         return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression) | ||||
|             .ExecuteCommandAsync().ConfigureAwait(false) > 0; | ||||
|         return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression) | ||||
|             .ExecuteCommandAsync(); | ||||
|     } | ||||
|  | ||||
|     private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput) | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/en-US.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   | ||||
|   "ThingsGateway.Admin.Application.BaseDataEntity": { | ||||
|     "CreateOrgId": "CreateOrgId" | ||||
|   }, | ||||
|   "ThingsGateway.Admin.Application.BaseEntity": { | ||||
|     "CreateTime": "CreateTime", | ||||
|     "CreateUser": "CreateUser", | ||||
|     "SortCode": "SortCode", | ||||
|     "UpdateTime": "UpdateTime", | ||||
|     "UpdateUser": "UpdateUser" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Admin/ThingsGateway.DB/Locales/zh-CN.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|    | ||||
|   "ThingsGateway.DB.BaseDataEntity": { | ||||
|     "CreateOrgId": "创建机构Id" | ||||
|   }, | ||||
|   "ThingsGateway.DB.BaseEntity": { | ||||
|     "CreateTime": "创建时间", | ||||
|     "CreateUser": "创建人", | ||||
|     "SortCode": "排序", | ||||
|     "UpdateTime": "更新时间", | ||||
|     "UpdateUser": "更新人" | ||||
|   } | ||||
| } | ||||
| @@ -46,7 +46,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new( | ||||
|     public async Task<bool> DeleteAsync(IEnumerable<T> models) | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false); | ||||
|         return await db.Deleteable<T>(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false); | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
| @@ -140,18 +140,22 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new( | ||||
|             return (await db.UpdateableT(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc/> | ||||
|     public virtual async Task<bool> SaveAsync(List<T> model, ItemChangedType changedType) | ||||
|     { | ||||
|         return (await SaveReturnCountAsync(model, changedType).ConfigureAwait(false)) > 0; | ||||
|     } | ||||
|     /// <inheritdoc/> | ||||
|     public async Task<int> SaveReturnCountAsync(List<T> model, ItemChangedType changedType) | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         if (changedType == ItemChangedType.Add) | ||||
|         { | ||||
|             return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|             return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0; | ||||
|             return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)); | ||||
|         } | ||||
|     } | ||||
|     /// <summary> | ||||
|   | ||||
| @@ -5,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)); | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -411,7 +411,7 @@ public static class SpecificationDocumentBuilder | ||||
|         // 本地函数 | ||||
|         static string DefaultSchemaIdSelector(Type modelType) | ||||
|         { | ||||
|             var modelName = modelType.Name; | ||||
|             var modelName = modelType.FullName; | ||||
|  | ||||
|             // 处理泛型类型问题 | ||||
|             if (modelType.IsConstructedGenericType) | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -60,6 +60,9 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull | ||||
|             Name = str; | ||||
|         else | ||||
|             Name = $"Pool<{typeof(T).Name}>"; | ||||
|  | ||||
|         // 启动定期清理的定时器 | ||||
|         StartTimer(); | ||||
|     } | ||||
|  | ||||
|     /// <summary>销毁</summary> | ||||
| @@ -227,8 +230,7 @@ public class ObjectPool<T> : DisposeBase, IPool<T> where T : notnull | ||||
|  | ||||
|         Interlocked.Increment(ref _FreeCount); | ||||
|  | ||||
|         // 启动定期清理的定时器 | ||||
|         StartTimer(); | ||||
|  | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										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 = 600000) | ||||
|     { | ||||
|         _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(); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -76,6 +76,65 @@ public static class PathHelper | ||||
|     #endregion | ||||
|  | ||||
|     #region 路径操作辅助 | ||||
|  | ||||
|     public static string GetRelativePath(string relativeTo, string path) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(relativeTo)) | ||||
|             throw new ArgumentNullException(nameof(relativeTo)); | ||||
|         if (string.IsNullOrEmpty(path)) | ||||
|             throw new ArgumentNullException(nameof(path)); | ||||
|  | ||||
|         // 转为绝对路径 | ||||
|         relativeTo = Path.GetFullPath(relativeTo); | ||||
|         path = Path.GetFullPath(path); | ||||
|  | ||||
|         // 末尾确保有分隔符,便于处理目录 | ||||
|         if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString())) | ||||
|             relativeTo += Path.DirectorySeparatorChar; | ||||
|  | ||||
|         // 相同路径 | ||||
|         if (string.Equals(relativeTo, path, StringComparison.OrdinalIgnoreCase)) | ||||
|             return "."; | ||||
|  | ||||
|         // 检查 UNC 或不同盘符 | ||||
|         bool isUnc = relativeTo.StartsWith(@"\\") && path.StartsWith(@"\\"); | ||||
|         if (!isUnc) | ||||
|         { | ||||
|             // 不同盘符直接返回绝对路径 | ||||
|             if (!string.Equals(Path.GetPathRoot(relativeTo), Path.GetPathRoot(path), StringComparison.OrdinalIgnoreCase)) | ||||
|                 return path; | ||||
|         } | ||||
|  | ||||
|         // 找到共同前缀长度 | ||||
|         int length = Math.Min(relativeTo.Length, path.Length); | ||||
|         int lastSeparatorIndex = -1; | ||||
|         int i; | ||||
|         for (i = 0; i < length; i++) | ||||
|         { | ||||
|             if (char.ToLowerInvariant(relativeTo[i]) != char.ToLowerInvariant(path[i])) | ||||
|                 break; | ||||
|             if (relativeTo[i] == Path.DirectorySeparatorChar) | ||||
|                 lastSeparatorIndex = i; | ||||
|         } | ||||
|  | ||||
|         // 计算上退的 ".." | ||||
|         string up = ""; | ||||
|         for (int j = lastSeparatorIndex + 1; j < relativeTo.Length; j++) | ||||
|         { | ||||
|             if (relativeTo[j] == Path.DirectorySeparatorChar) | ||||
|                 up += ".." + Path.DirectorySeparatorChar; | ||||
|         } | ||||
|  | ||||
|         // 获取剩余下行部分 | ||||
|         string down = path.Substring(lastSeparatorIndex + 1); | ||||
|  | ||||
|         // 拼接结果 | ||||
|         string result = up + down; | ||||
|  | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private static String GetPath(String path, Int32 mode) | ||||
|     { | ||||
|         // 处理路径分隔符,兼容Windows和Linux | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| using System.Collections; | ||||
| using System.Diagnostics; | ||||
| using System.Reflection; | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| namespace ThingsGateway.NewLife.Reflection; | ||||
|  | ||||
| @@ -552,19 +553,65 @@ public static class Reflect | ||||
|     //    return false; | ||||
|     //} | ||||
|  | ||||
|  | ||||
|     private static class DelegateCache<TFunc> | ||||
|     { | ||||
|         public static readonly ExpiringDictionary<DelegateCacheKey, TFunc> Cache = 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 = new DelegateCacheKey(method, typeof(TFunc), target); | ||||
|  | ||||
|         var func = DelegateCache<TFunc>.Cache.GetOrAdd( | ||||
|              key, | ||||
|              _ => (TFunc)(object)( | ||||
|                      target == null | ||||
|                          ? Delegate.CreateDelegate(typeof(TFunc), method, true) | ||||
|                          : Delegate.CreateDelegate(typeof(TFunc), target, method, true))); | ||||
|  | ||||
|         return func; | ||||
|     } | ||||
|  | ||||
|     private readonly struct DelegateCacheKey : IEquatable<DelegateCacheKey> | ||||
|     { | ||||
|         public readonly MethodInfo Method; | ||||
|         public readonly Type FuncType; | ||||
|         public readonly object? Target; | ||||
|  | ||||
|         public DelegateCacheKey(MethodInfo method, Type funcType, object? target) | ||||
|         { | ||||
|             Method = method; | ||||
|             FuncType = funcType; | ||||
|             Target = target; | ||||
|         } | ||||
|  | ||||
|         public bool Equals(DelegateCacheKey other) => | ||||
|             Method.Equals(other.Method) | ||||
|             && FuncType.Equals(other.FuncType) | ||||
|             && ReferenceEquals(Target, other.Target); | ||||
|  | ||||
|         public override bool Equals(object? obj) => | ||||
|             obj is DelegateCacheKey other && Equals(other); | ||||
|  | ||||
|         public override int GetHashCode() | ||||
|         { | ||||
|             unchecked | ||||
|             { | ||||
|                 int hash = Method.GetHashCode(); | ||||
|                 hash = (hash * 397) ^ FuncType.GetHashCode(); | ||||
|                 if (Target != null) | ||||
|                     hash = (hash * 397) ^ RuntimeHelpers.GetHashCode(Target); // 不受对象重写 GetHashCode 影响 | ||||
|                 return hash; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     #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,9 +3,10 @@ | ||||
| 	<Import Project="..\..\PackNuget.props" /> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" /> | ||||
| 		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.1" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -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] | ||||
|         { | ||||
| @@ -107,10 +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()); | ||||
|             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>(); | ||||
|   | ||||
| @@ -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,18 +23,18 @@ | ||||
| 	</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.21" /> | ||||
| 		<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" /> | ||||
|   | ||||
| @@ -1,14 +1,17 @@ | ||||
| <Project> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<PluginVersion>10.10.2</PluginVersion> | ||||
| 		<ProPluginVersion>10.10.2</ProPluginVersion> | ||||
| 		<DefaultVersion>10.10.5</DefaultVersion> | ||||
| 		<AuthenticationVersion>2.9.29</AuthenticationVersion> | ||||
| 		<SourceGeneratorVersion>10.9.29</SourceGeneratorVersion> | ||||
| 		<NET8Version>8.0.18</NET8Version> | ||||
| 		<NET9Version>9.0.7</NET9Version> | ||||
| 		<PluginVersion>10.11.23</PluginVersion> | ||||
| 		<ProPluginVersion>10.11.23</ProPluginVersion> | ||||
| 		<DefaultVersion>10.11.23</DefaultVersion> | ||||
| 		<AuthenticationVersion>10.11.3</AuthenticationVersion> | ||||
| 		<SourceGeneratorVersion>10.11.3</SourceGeneratorVersion> | ||||
| 		<NET8Version>8.0.19</NET8Version> | ||||
| 		<NET9Version>9.0.8</NET9Version> | ||||
| 		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages> | ||||
| 		<IsTrimmable>false</IsTrimmable> | ||||
| 		<ManagementProPluginVersion>10.11.22</ManagementProPluginVersion> | ||||
| 		<ManagementPluginVersion>10.11.22</ManagementPluginVersion> | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| @@ -27,7 +30,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> | ||||
|   | ||||
| @@ -82,13 +82,14 @@ public static class CSharpScriptEngineExtension | ||||
|     { | ||||
|         if (source.IsNullOrEmpty()) return null; | ||||
|         var field = $"{CacheKey}-{source}"; | ||||
|         var exfield = $"{CacheKey}-Exception-{source}"; | ||||
|         var runScript = Instance.Get<T>(field); | ||||
|         if (runScript == null) | ||||
|         { | ||||
|             lock (m_waiterLock) | ||||
|             { | ||||
|                 runScript = Instance.Get<T>(field); | ||||
|                 if (runScript == null) | ||||
|                 var hasValue = Instance.TryGetValue<T>(field, out runScript); | ||||
|                 if (hasValue == false) | ||||
|                 { | ||||
|                     var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); | ||||
|                     var _using = new StringBuilder(); | ||||
| @@ -111,8 +112,6 @@ public static class CSharpScriptEngineExtension | ||||
|                     } | ||||
|                     try | ||||
|                     { | ||||
|  | ||||
|  | ||||
|                         // 动态加载并执行代码 | ||||
|                         runScript = evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<T>( | ||||
|                            $@" | ||||
| @@ -140,11 +139,22 @@ public static class CSharpScriptEngineExtension | ||||
|                         string exString = string.Format(CSScriptResource.CSScriptResource.Error1, typeof(T).FullName); | ||||
|                         throw new(exString); | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         //如果编译失败,应该不重复编译,避免oom | ||||
|                         Instance.Set<T>(field, null, TimeSpan.FromHours(1)); | ||||
|                         Instance.Set(exfield, ex, TimeSpan.FromHours(1)); | ||||
|                         throw; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Instance.SetExpire(field, TimeSpan.FromHours(1)); | ||||
|  | ||||
|         Instance.SetExpire(exfield, TimeSpan.FromHours(1)); | ||||
|         if (runScript == null) | ||||
|         { | ||||
|             throw (Instance.Get<Exception>(exfield) ?? new Exception("compilation error")); | ||||
|         } | ||||
|         return runScript; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -93,30 +93,38 @@ public static class ExpressionEvaluatorExtension | ||||
|     public static ReadWriteExpressions GetOrAddScript(string source) | ||||
|     { | ||||
|         var field = $"{CacheKey}-{source}"; | ||||
|         var exfield = $"{CacheKey}-Exception-{source}"; | ||||
|         var runScript = Instance.Get<ReadWriteExpressions>(field); | ||||
|         if (runScript == null) | ||||
|         { | ||||
|             if (!source.Contains("return")) | ||||
|             var hasValue = Instance.TryGetValue<ReadWriteExpressions>(field, out runScript); | ||||
|             if (!hasValue) | ||||
|             { | ||||
|                 source = $"return {source}";//只判断简单脚本中可省略return字符串 | ||||
|             } | ||||
|             var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); | ||||
|             var _using = new StringBuilder(); | ||||
|             var _body = new StringBuilder(); | ||||
|             src.ToList().ForEach(l => | ||||
|             { | ||||
|                 if (l.StartsWith("using ")) | ||||
|  | ||||
|  | ||||
|                 if (!source.Contains("return")) | ||||
|                 { | ||||
|                     _using.AppendLine(l); | ||||
|                     source = $"return {source}";//只判断简单脚本中可省略return字符串 | ||||
|                 } | ||||
|                 else | ||||
|                 var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); | ||||
|                 var _using = new StringBuilder(); | ||||
|                 var _body = new StringBuilder(); | ||||
|                 src.ToList().ForEach(l => | ||||
|                 { | ||||
|                     _body.AppendLine(l); | ||||
|                 } | ||||
|             }); | ||||
|             // 动态加载并执行代码 | ||||
|             runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>( | ||||
|                 $@" | ||||
|                     if (l.StartsWith("using ")) | ||||
|                     { | ||||
|                         _using.AppendLine(l); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         _body.AppendLine(l); | ||||
|                     } | ||||
|                 }); | ||||
|                 // 动态加载并执行代码 | ||||
|                 try | ||||
|                 { | ||||
|                     runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>( | ||||
| $@" | ||||
|         using System; | ||||
|         using System.Linq; | ||||
|         using System.Collections.Generic; | ||||
| @@ -137,9 +145,26 @@ public static class ExpressionEvaluatorExtension | ||||
|             }} | ||||
|         }} | ||||
|     "); | ||||
|             Instance.Set(field, runScript); | ||||
|                     Instance.Set(field, runScript); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     //如果编译失败,应该不重复编译,避免oom | ||||
|                     Instance.Set<ReadWriteExpressions>(field, null, TimeSpan.FromHours(1)); | ||||
|                     Instance.Set(exfield, ex, TimeSpan.FromHours(1)); | ||||
|                     throw; | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         Instance.SetExpire(field, TimeSpan.FromHours(1)); | ||||
|         Instance.SetExpire(exfield, TimeSpan.FromHours(1)); | ||||
|         if (runScript == null) | ||||
|         { | ||||
|             throw (Instance.Get<Exception>(exfield) ?? new Exception("compilation error")); | ||||
|         } | ||||
|         return runScript; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,10 +4,11 @@ | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>netstandard2.0;</TargetFrameworks> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="CS-Script" Version="4.10.1" /> | ||||
| 		<PackageReference Include="CS-Script" Version="4.11.0" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| // This file is used by Code Analysis to maintain SuppressMessage | ||||
| // attributes that are applied to this project. | ||||
| // Project-level suppressions either have no target or are given | ||||
| // a specific target and scoped to a namespace, type, member, etc. | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| [assembly: SuppressMessage("Reliability", "CA2007:考虑对等待的任务调用 ConfigureAwait", Justification = "<挂起>", Scope = "member", Target = "~M:ThingsGateway.Foundation.Demo.Program.Main(System.String[])~System.Threading.Tasks.Task")] | ||||
| [assembly: SuppressMessage("Reliability", "CA2007:考虑对等待的任务调用 ConfigureAwait", Justification = "<挂起>", Scope = "member", Target = "~M:ThingsGateway.Foundation.Demo.ModbusMasterDemo.TestRead~System.Threading.Tasks.Task")] | ||||
							
								
								
									
										11
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| global using TouchSocket.Core; | ||||
							
								
								
									
										221
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/ModbusMasterDemo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/ModbusMasterDemo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using ThingsGateway.Foundation.Modbus; | ||||
| using ThingsGateway.NewLife.Json.Extension; | ||||
|  | ||||
| namespace ThingsGateway.Foundation.Demo; | ||||
|  | ||||
| #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait | ||||
| #pragma warning disable CA1861 // 不要将常量数组作为参数 | ||||
|  | ||||
| /// <summary> | ||||
| /// ModbusMaster | ||||
| /// </summary> | ||||
| public class ModbusMasterDemo | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 新建链路 | ||||
|     /// </summary> | ||||
|     /// <returns></returns> | ||||
|     public IChannel GetChannel(ChannelOptions channelOptions) | ||||
|     { | ||||
|         TouchSocketConfig touchSocketConfig = new TouchSocketConfig(); | ||||
|         return touchSocketConfig.GetChannel(channelOptions); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 新建协议对象 | ||||
|     /// </summary> | ||||
|     /// <param name="channel"></param> | ||||
|     /// <returns></returns> | ||||
|     public ModbusMaster GetDevice(IChannel channel) | ||||
|     { | ||||
|         var client = new ModbusMaster(); | ||||
|         client.InitChannel(channel); | ||||
|         return client; | ||||
|     } | ||||
|     public async Task TestReadWrite() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:502", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|         //读取具体类型数据 | ||||
|         var data = await device.ReadDoubleAsync("400001"); //通过字符串转化地址,读取保持寄存器地址0 | ||||
|         device.Logger?.Info($"读取到的数据:{data.ToJsonNetString()}"); | ||||
|  | ||||
|  | ||||
|         //读取原始字节数组 | ||||
|  | ||||
|         var bytes = await device.ReadAsync("400001", 10); //通过字符串转化地址,读取保持寄存器地址0,10个寄存器 | ||||
|         device.Logger?.Info($"读取到的数据:{data.ToJsonNetString()}"); | ||||
|  | ||||
|         bytes = await device.ModbusReadAsync(new ModbusAddress() | ||||
|         { | ||||
|             StartAddress = 0, | ||||
|             FunctionCode = 3, | ||||
|             Length = 10, | ||||
|         }); //配置地址对象,读取保持寄存器地址0,10个寄存器 | ||||
|  | ||||
|         if (bytes.IsSuccess) | ||||
|         { | ||||
|             //解析bytes字节数组 | ||||
|             var byteData = bytes.Content.Span; | ||||
|             var data1 = device.ThingsGatewayBitConverter.ToDouble(byteData, 0); | ||||
|             var data2 = device.ThingsGatewayBitConverter.ToDouble(byteData, 8); | ||||
|             var data3 = device.ThingsGatewayBitConverter.ToUInt16(byteData, 16); | ||||
|             device.Logger?.Info($"读取到的数据:{data1},{data2},{data3}"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         //写入数据 | ||||
|         var write = await device.WriteAsync("400001", (double)123.456); //通过字符串转化地址,写入保持寄存器地址0 | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|         write = await device.WriteAsync("400001", new double[] { 123.456, 123.456 }); //通过字符串转化地址,写入保持寄存器地址2,2个double寄存器 | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|  | ||||
|         write = await device.ModbusRequestAsync(new ModbusAddress() | ||||
|         { | ||||
|             StartAddress = 0, | ||||
|             FunctionCode = 3, | ||||
|             MasterWriteDatas = device.ThingsGatewayBitConverter.GetBytes(new double[] { 123.456, 123.456 }) | ||||
|         }, false); //通过字符串转化地址,写入保持寄存器地址2,2个double寄存器 | ||||
|  | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|  | ||||
|     } | ||||
|     public async Task TestMulRead() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:502", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|  | ||||
|         //批量打包 | ||||
|         var variableRuntimes = new List<VariableClass>() | ||||
|             { | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.Double, | ||||
|                     RegisterAddress="40001", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.UInt16, | ||||
|                     RegisterAddress="40009", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.Double, | ||||
|                     RegisterAddress="40005", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|  | ||||
|             }; | ||||
|  | ||||
|         var deviceVariableSourceReads = device.LoadSourceRead<VariableSourceClass>(variableRuntimes, 125, "1000"); | ||||
|         foreach (var item in deviceVariableSourceReads) | ||||
|         { | ||||
|             var result = await device.ReadAsync(item.AddressObject); | ||||
|             if (result.IsSuccess) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     var result1 = item.VariableRuntimes.PraseStructContent(device, result.Content.Span, exWhenAny: true); | ||||
|                     if (!result1.IsSuccess) | ||||
|                     { | ||||
|                         item.LastErrorMessage = result1.ErrorMessage; | ||||
|                         var time = DateTime.Now; | ||||
|                         item.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false)); | ||||
|                         device.Logger?.Warning(result1.ToString()); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     device.Logger?.LogWarning(ex); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 item.LastErrorMessage = result.ErrorMessage; | ||||
|                 var time = DateTime.Now; | ||||
|                 item.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false)); | ||||
|                 device.Logger?.Warning(result.ToString()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         device.Logger?.Info($"批量读取到的数据:{variableRuntimes.Select(a => new { a.RegisterAddress, a.Value }).ToJsonNetString()}"); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public async Task TestVariableObject() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:502", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|  | ||||
|         //使用变量对象读取 | ||||
|         var testModbusObject = new TestModbusObject(device, 125); | ||||
|         await testModbusObject.MultiReadAsync(); | ||||
|         device.Logger?.Info($"批量读取到的数据:{testModbusObject.ToJsonNetString()}"); | ||||
|  | ||||
|         //源生成的写入方法 | ||||
|         var write = await testModbusObject.WriteDouble1Async(123.456); | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|     } | ||||
| } | ||||
| [GeneratorVariable] | ||||
| public partial class TestModbusObject : VariableObject | ||||
| { | ||||
|     public TestModbusObject(IDevice device, int maxPack) : base(device, maxPack) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     [VariableRuntime(RegisterAddress = "400001")] | ||||
|     public double Double1 { get; set; } | ||||
|     [VariableRuntime(RegisterAddress = "400005")] | ||||
|     public double Double2 { get; set; } | ||||
|  | ||||
|     [VariableRuntime(RegisterAddress = "400009")] | ||||
|     public ushort UShort3 { get; set; } | ||||
|     [VariableRuntime(RegisterAddress = "4000010")] | ||||
|     public ushort UShort4 { get; set; } | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/Foundation/ThingsGateway.Foundation.Demo/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| namespace ThingsGateway.Foundation.Demo; | ||||
| #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait | ||||
|  | ||||
| public class Program | ||||
| { | ||||
|  | ||||
|     public static async Task Main(string[] args) | ||||
|     { | ||||
|         ModbusMasterDemo modbusMasterDemo = new(); | ||||
|         await modbusMasterDemo.TestReadWrite(); | ||||
|         await modbusMasterDemo.TestMulRead(); | ||||
|         await modbusMasterDemo.TestVariableObject(); | ||||
|  | ||||
|         Console.ReadKey(); | ||||
|  | ||||
|         SiemensS7MasterDemo siemensS7MasterDemo = new(); | ||||
|         await siemensS7MasterDemo.TestReadWrite(); | ||||
|         await siemensS7MasterDemo.TestMulRead(); | ||||
|         await siemensS7MasterDemo.TestVariableObject(); | ||||
|  | ||||
|         Console.ReadKey(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,214 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using ThingsGateway.Foundation.SiemensS7; | ||||
| using ThingsGateway.NewLife.Json.Extension; | ||||
|  | ||||
| namespace ThingsGateway.Foundation.Demo; | ||||
|  | ||||
| #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait | ||||
| #pragma warning disable CA1861 // 不要将常量数组作为参数 | ||||
|  | ||||
| /// <summary> | ||||
| /// SiemensS7Master | ||||
| /// </summary> | ||||
| public class SiemensS7MasterDemo | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 新建链路 | ||||
|     /// </summary> | ||||
|     /// <returns></returns> | ||||
|     public IChannel GetChannel(ChannelOptions channelOptions) | ||||
|     { | ||||
|         TouchSocketConfig touchSocketConfig = new TouchSocketConfig(); | ||||
|         return touchSocketConfig.GetChannel(channelOptions); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 新建协议对象 | ||||
|     /// </summary> | ||||
|     /// <param name="channel"></param> | ||||
|     /// <returns></returns> | ||||
|     public SiemensS7Master GetDevice(IChannel channel) | ||||
|     { | ||||
|         var client = new SiemensS7Master(); | ||||
|         client.InitChannel(channel); | ||||
|         return client; | ||||
|     } | ||||
|     public async Task TestReadWrite() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:102", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|         //读取具体类型数据 | ||||
|         var data = await device.ReadDoubleAsync("V1"); //通过字符串转化地址,读取v1 | ||||
|         device.Logger?.Info($"读取到的数据:{data.ToJsonNetString()}"); | ||||
|  | ||||
|  | ||||
|         //读取原始字节数组 | ||||
|  | ||||
|         var bytes = await device.ReadAsync("V1", 20); //通过字符串转化地址,读取v1,10个寄存器 | ||||
|         device.Logger?.Info($"读取到的数据:{data.ToJsonNetString()}"); | ||||
|  | ||||
|  | ||||
|         if (bytes.IsSuccess) | ||||
|         { | ||||
|             //解析bytes字节数组 | ||||
|             var byteData = bytes.Content.Span; | ||||
|             var data1 = device.ThingsGatewayBitConverter.ToDouble(byteData, 0); | ||||
|             var data2 = device.ThingsGatewayBitConverter.ToDouble(byteData, 8); | ||||
|             var data3 = device.ThingsGatewayBitConverter.ToUInt16(byteData, 16); | ||||
|             device.Logger?.Info($"读取到的数据:{data1},{data2},{data3}"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         //写入数据 | ||||
|         var write = await device.WriteAsync("v1", (double)123.456); //通过字符串转化地址,写入保持寄存器地址0 | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|         write = await device.WriteAsync("v1", new double[] { 123.456, 123.456 }); //通过字符串转化地址,写入保持寄存器地址2,2个double寄存器 | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|  | ||||
|     } | ||||
|     public async Task TestMulRead() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:102", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|  | ||||
|         //批量打包 | ||||
|         var variableRuntimes = new List<VariableClass>() | ||||
|             { | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.Double, | ||||
|                     RegisterAddress="v1", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.UInt16, | ||||
|                     RegisterAddress="v9", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|                 new VariableClass() | ||||
|                 { | ||||
|                     DataType=DataTypeEnum.Double, | ||||
|                     RegisterAddress="v11", | ||||
|                     IntervalTime="1000", | ||||
|                 }, | ||||
|  | ||||
|             }; | ||||
|  | ||||
|         var deviceVariableSourceReads = device.LoadSourceRead<VariableSourceClass>(variableRuntimes, 125, "1000"); | ||||
|         foreach (var item in deviceVariableSourceReads) | ||||
|         { | ||||
|             var result = await device.ReadAsync(item.AddressObject); | ||||
|             if (result.IsSuccess) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     var result1 = item.VariableRuntimes.PraseStructContent(device, result.Content.Span, exWhenAny: true); | ||||
|                     if (!result1.IsSuccess) | ||||
|                     { | ||||
|                         item.LastErrorMessage = result1.ErrorMessage; | ||||
|                         var time = DateTime.Now; | ||||
|                         item.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false)); | ||||
|                         device.Logger?.Warning(result1.ToString()); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     device.Logger?.LogWarning(ex); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 item.LastErrorMessage = result.ErrorMessage; | ||||
|                 var time = DateTime.Now; | ||||
|                 item.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false)); | ||||
|                 device.Logger?.Warning(result.ToString()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         device.Logger?.Info($"批量读取到的数据:{variableRuntimes.Select(a => new { a.RegisterAddress, a.Value }).ToJsonNetString()}"); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public async Task TestVariableObject() | ||||
|     { | ||||
|  | ||||
|         //获取链路对象 | ||||
|         using var channel = GetChannel(new ChannelOptions() | ||||
|         { | ||||
|             ChannelType = ChannelTypeEnum.TcpClient, | ||||
|             RemoteUrl = "127.0.0.1:102", | ||||
|         }); | ||||
|         //配置其他属性,如日志等 | ||||
|         channel.Config.ConfigureContainer(a => a.AddConsoleLogger()); | ||||
|  | ||||
|         //获取协议对象 | ||||
|         using var device = GetDevice(channel); | ||||
|  | ||||
|  | ||||
|         //使用变量对象读取 | ||||
|         var testS7Object = new TestS7Object(device, 125); | ||||
|         await testS7Object.MultiReadAsync(); | ||||
|         device.Logger?.Info($"批量读取到的数据:{testS7Object.ToJsonNetString()}"); | ||||
|  | ||||
|         //源生成的写入方法 | ||||
|         var write = await testS7Object.WriteDouble1Async(123.456); | ||||
|         device.Logger?.Info($"写入结果:{write.ToJsonNetString()}"); | ||||
|     } | ||||
| } | ||||
| /// <summary> | ||||
| /// 实体类操作PLC数据 | ||||
| /// </summary> | ||||
| [GeneratorVariable] | ||||
| public partial class TestS7Object : VariableObject | ||||
| { | ||||
|     [VariableRuntime(RegisterAddress = "v1")] | ||||
|     public double Double1 { get; set; } | ||||
|     [VariableRuntime(RegisterAddress = "v9")] | ||||
|     public double Double2 { get; set; } | ||||
|  | ||||
|     [VariableRuntime(RegisterAddress = "v17")] | ||||
|     public ushort UShort3 { get; set; } | ||||
|     [VariableRuntime(RegisterAddress = "v19")] | ||||
|     public ushort UShort4 { get; set; } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     public TestS7Object(IDevice device, int maxPack) : base(device, maxPack) | ||||
|     { | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user