mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-10-31 23:53:58 +08:00 
			
		
		
		
	Compare commits
	
		
			40 Commits
		
	
	
		
			10.10.10.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 | 
							
								
								
									
										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('/', '_'))); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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": "用户登录已过期,请重新登录" | ||||
|   }, | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
|   | ||||
| @@ -18,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 => | ||||
|         { | ||||
|   | ||||
| @@ -183,6 +183,8 @@ 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); | ||||
| #else | ||||
| @@ -197,6 +199,7 @@ public class Startup : AppStartup | ||||
|                     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> | ||||
|   | ||||
| @@ -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.1" /> | ||||
| 		<PackageReference Include="BootstrapBlazor" Version="9.10.0" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -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": "更新人" | ||||
|   } | ||||
| } | ||||
| @@ -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" />--> | ||||
|   | ||||
| @@ -554,10 +554,9 @@ public static class App | ||||
|         { | ||||
|             types = ass.GetTypes(); | ||||
|         } | ||||
|         catch | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             XTrace.Log.Warn($"Error load `{ass.FullName}` assembly."); | ||||
|             Console.WriteLine($"Error load `{ass.FullName}` assembly."); | ||||
|             XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}"); | ||||
|         } | ||||
|  | ||||
|         return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false)); | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
| 		<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile> | ||||
| 		 | ||||
|  | ||||
|  | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|  | ||||
| #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); | ||||
|             await func!(timer.State).ConfigureAwait(false); | ||||
|                 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; | ||||
| } | ||||
| @@ -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)); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|         } | ||||
| @@ -127,13 +132,16 @@ namespace ThingsGateway.SqlSugar | ||||
|             { | ||||
|                 // 准备多部分表单数据 | ||||
|                 var boundary = "---------------" + DateTime.Now.Ticks.ToString("x"); | ||||
|                 var list = new List<Hashtable>(); | ||||
|  | ||||
|                 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(tableName)); | ||||
|                 var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () => | ||||
|                 { | ||||
|                     var list = new List<Hashtable>(); | ||||
|  | ||||
|                     // 构建schema信息 | ||||
|                     columns.ForEach(d => | ||||
| @@ -156,6 +164,11 @@ namespace ThingsGateway.SqlSugar | ||||
|                         }); | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     return list; | ||||
|                 } | ||||
|  ); | ||||
|  | ||||
|                 var schema = JsonConvert.SerializeObject(list); | ||||
|  | ||||
|                 // 写入CSV文件 | ||||
| @@ -184,7 +197,7 @@ namespace ThingsGateway.SqlSugar | ||||
|                     "multipart/form-data; boundary=" + boundary); | ||||
|  | ||||
|                 // 发送请求并处理响应 | ||||
|                 var httpResponseMessage = | ||||
|                 using var httpResponseMessage = | ||||
|                       await Post(client, tableName, httpContent).ConfigureAwait(false); | ||||
|                 var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|                 var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync); | ||||
|   | ||||
| @@ -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 => new MemoryCache() { Expire = 60 }; | ||||
|         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,15 +1,17 @@ | ||||
| <Project> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<PluginVersion>10.10.7</PluginVersion> | ||||
| 		<ProPluginVersion>10.10.6</ProPluginVersion> | ||||
| 		<DefaultVersion>10.10.10</DefaultVersion> | ||||
| 		<AuthenticationVersion>10.10.1</AuthenticationVersion> | ||||
| 		<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion> | ||||
| 		<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> | ||||
| @@ -28,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,9 +93,15 @@ 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) | ||||
|         { | ||||
|             var hasValue = Instance.TryGetValue<ReadWriteExpressions>(field, out runScript); | ||||
|             if (!hasValue) | ||||
|             { | ||||
|  | ||||
|  | ||||
|                 if (!source.Contains("return")) | ||||
|                 { | ||||
|                     source = $"return {source}";//只判断简单脚本中可省略return字符串 | ||||
| @@ -115,6 +121,8 @@ public static class ExpressionEvaluatorExtension | ||||
|                     } | ||||
|                 }); | ||||
|                 // 动态加载并执行代码 | ||||
|                 try | ||||
|                 { | ||||
|                     runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>( | ||||
| $@" | ||||
|         using System; | ||||
| @@ -139,7 +147,24 @@ public static class ExpressionEvaluatorExtension | ||||
|     "); | ||||
|                     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) | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @@ -89,7 +89,7 @@ public partial class ChannelComponent : ComponentBase | ||||
|                 await Channel.SetupAsync(config); | ||||
|             } | ||||
|  | ||||
|             await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, default); | ||||
|             await Channel.ConnectAsync(default); | ||||
|  | ||||
|             if (OnConnectClick.HasDelegate) | ||||
|                 await OnConnectClick.InvokeAsync(Channel); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|             <EditorItem @bind-Field=Model.EncodingName> | ||||
|                 <EditTemplate Context="value"> | ||||
|                     <div class="col-12 col-sm-4"> | ||||
|                         <Select @bind-Value=value.EncodingName Items="EncodingItems" /> | ||||
|                         <Select @bind-Value=value.EncodingName Items="EncodingItems" IsClearable/> | ||||
|                     </div> | ||||
|                 </EditTemplate> | ||||
|             </EditorItem> | ||||
|   | ||||
| @@ -31,6 +31,6 @@ public partial class ConverterConfigComponent : ComponentBase | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.VariableStringLength), true); | ||||
|         EncodingItems = new List<SelectedItem>() { new SelectedItem("", "none") }.Concat(Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName))).ToList(); | ||||
|         EncodingItems = Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName)).ToList(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,62 @@ | ||||
| @using Microsoft.AspNetCore.Components.Web; | ||||
| @using Microsoft.AspNetCore.Components.Web.Virtualization | ||||
| @using Microsoft.JSInterop; | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.NewLife.Threading | ||||
| @using ThingsGateway.Extension; | ||||
| @using BootstrapBlazor.Components | ||||
| @namespace ThingsGateway.Debug | ||||
|  | ||||
| <div class="w-100" style=@($"height:{HeightString}")> | ||||
|  | ||||
|     <Card HeaderText=@HeaderText class=@("w-100 h-100")> | ||||
|     <HeaderTemplate> | ||||
|         <div class="flex-fill"> | ||||
|         </div> | ||||
|  | ||||
|         @if (LogLevelChanged.HasDelegate) | ||||
|         { | ||||
|             <Select Value="@LogLevel" ValueChanged="LogLevelChanged" IsPopover></Select> | ||||
|         } | ||||
|         <Tooltip class=" col-auto" Title="@RazorLocalizer[Pause?"Play":"Pause"]" Placement="Placement.Bottom"> | ||||
|  | ||||
|             <Button Color="Color.None" style="color: var(--bs-card-title-color);" Icon=@(Pause?"fa fa-play":"fa fa-pause") OnClick="OnPause" /> | ||||
|  | ||||
|         </Tooltip> | ||||
|  | ||||
|         <Tooltip class=" col-auto" Title="@RazorLocalizer["Export"]" Placement="Placement.Bottom"> | ||||
|  | ||||
|             <Button IsAsync Color="Color.None" style="color: var(--bs-card-title-color);" Icon=@("fa fa-sign-out") OnClick="HandleOnExportClick" /> | ||||
|  | ||||
|         </Tooltip> | ||||
|  | ||||
|         <Tooltip class=" col-auto" Title="@RazorLocalizer["Delete"]" Placement="Placement.Bottom"> | ||||
|  | ||||
|             <Button IsAsync Color="Color.None" style="color: var(--bs-card-title-color);" Icon=@("far fa-trash-alt") OnClick="Delete" /> | ||||
|  | ||||
|         </Tooltip> | ||||
|  | ||||
|  | ||||
|     </HeaderTemplate> | ||||
|     <BodyTemplate> | ||||
|                 <div style=@($"height:calc(100% - 50px);overflow-y:scroll;flex-fill;")> | ||||
|             <Virtualize Items="CurrentMessages??new  List<LogMessage>()" Context="itemMessage" ItemSize="60" OverscanCount=2> | ||||
|                 <ItemContent> | ||||
|                     @*       <Tooltip Placement="Placement.Bottom" Title=@itemMessage.Message.Substring(0, Math.Min(itemMessage.Message.Length, 500))> *@ | ||||
|                     <div class=@(itemMessage.Level<(byte)Microsoft.Extensions.Logging.LogLevel.Information?"": | ||||
|                          itemMessage.Level>=(byte)Microsoft.Extensions.Logging.LogLevel.Warning? " red--text text-truncate":"green--text text-truncate") | ||||
|                          title=@itemMessage.Message.Substring(0, Math.Min(itemMessage.Message.Length, 500))> | ||||
|  | ||||
|                         @itemMessage.Message.Substring(0, Math.Min(itemMessage.Message.Length, 150)) | ||||
|  | ||||
|                     </div> | ||||
|                     @* </Tooltip> *@ | ||||
|                 </ItemContent> | ||||
|             </Virtualize> | ||||
|         </div> | ||||
|  | ||||
|     </BodyTemplate> | ||||
| </Card> | ||||
|  | ||||
|  | ||||
| </div> | ||||
| @@ -0,0 +1,203 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Microsoft.AspNetCore.Components.Web; | ||||
|  | ||||
| using System.Diagnostics; | ||||
| using System.Text.RegularExpressions; | ||||
|  | ||||
| using ThingsGateway.Extension; | ||||
| using ThingsGateway.Foundation; | ||||
| using ThingsGateway.NewLife; | ||||
|  | ||||
| using TouchSocket.Core; | ||||
|  | ||||
| namespace ThingsGateway.Debug; | ||||
|  | ||||
| public partial class LocalLogConsole : IDisposable | ||||
| { | ||||
|     private bool Pause; | ||||
|  | ||||
|     public bool Disposed { get; set; } | ||||
|  | ||||
|     [Parameter, EditorRequired] | ||||
|     public LogLevel LogLevel { get; set; } | ||||
|  | ||||
|     [Parameter] | ||||
|     public EventCallback<LogLevel> LogLevelChanged { get; set; } | ||||
|  | ||||
|     [Parameter] | ||||
|     public string HeaderText { get; set; } = "Log"; | ||||
|  | ||||
|     [Parameter] | ||||
|     public string HeightString { get; set; } = "calc(100% - 300px)"; | ||||
|  | ||||
|     [Parameter, EditorRequired] | ||||
|     public string LogPath { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 日志 | ||||
|     /// </summary> | ||||
|     public ICollection<LogMessage> Messages { get; set; } = new List<LogMessage>(); | ||||
|  | ||||
|     private ICollection<LogMessage> CurrentMessages => Pause ? PauseMessagesText : Messages; | ||||
|  | ||||
|     [Inject] | ||||
|     private DownloadService DownloadService { get; set; } | ||||
|     [Inject] | ||||
|     private IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 暂停缓存 | ||||
|     /// </summary> | ||||
|     private ICollection<LogMessage> PauseMessagesText { get; set; } = new List<LogMessage>(); | ||||
|  | ||||
|     [Inject] | ||||
|     private IPlatformService PlatformService { get; set; } | ||||
|  | ||||
|     private string logPath; | ||||
|     protected override async Task OnParametersSetAsync() | ||||
|     { | ||||
|         if (LogPath != logPath) | ||||
|         { | ||||
|             logPath = LogPath; | ||||
|             Messages = new List<LogMessage>(); | ||||
|             await ExecuteAsync(); | ||||
|         } | ||||
|  | ||||
|         await base.OnParametersSetAsync(); | ||||
|     } | ||||
|  | ||||
|     [Inject] | ||||
|     private ToastService ToastService { get; set; } | ||||
|     [Inject] | ||||
|     TextFileReadService TextFileReadService { get; set; } | ||||
|     public void Dispose() | ||||
|     { | ||||
|         Disposed = true; | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
|     private WaitLock WaitLock = new(nameof(LogConsole)); | ||||
|     protected async Task ExecuteAsync() | ||||
|     { | ||||
|         if (WaitLock.Waited) return; | ||||
|         try | ||||
|         { | ||||
|             await WaitLock.WaitAsync(); | ||||
|             await Task.Delay(1000); | ||||
|  | ||||
|             if (LogPath != null) | ||||
|             { | ||||
|                 var files = await TextFileReadService.GetLogFilesAsync(LogPath); | ||||
|                 if (!files.IsSuccess) | ||||
|                 { | ||||
|                     Messages = new List<LogMessage>(); | ||||
|                     await Task.Delay(1000); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     await Task.Run(async () => | ||||
|                     { | ||||
|                         Stopwatch sw = Stopwatch.StartNew(); | ||||
|                         var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault()); | ||||
|                         if (result.IsSuccess) | ||||
|                         { | ||||
|                             Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList(); | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             Messages = new List<LogMessage>(); | ||||
|                         } | ||||
|                         sw.Stop(); | ||||
|                         if (sw.ElapsedMilliseconds > 500) | ||||
|                         { | ||||
|                             await Task.Delay(1000); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             NewLife.Log.XTrace.WriteException(ex); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             WaitLock.Release(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         _ = RunTimerAsync(); | ||||
|         base.OnInitialized(); | ||||
|     } | ||||
|  | ||||
|     private async Task Delete() | ||||
|     { | ||||
|         await TextFileReadService.DeleteLogDataAsync(LogPath); | ||||
|     } | ||||
|  | ||||
|     private async Task HandleOnExportClick(MouseEventArgs args) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (Pause) | ||||
|             { | ||||
|                 using var memoryStream = new MemoryStream(); | ||||
|                 using StreamWriter writer = new(memoryStream); | ||||
|                 foreach (var item in PauseMessagesText) | ||||
|                 { | ||||
|                     await writer.WriteLineAsync(item.Message); | ||||
|                 } | ||||
|                 await writer.FlushAsync(); | ||||
|                 memoryStream.Seek(0, SeekOrigin.Begin); | ||||
|  | ||||
|                 // 定义文件名称规则的正则表达式模式 | ||||
|                 string pattern = @"[\\/:*?""<>|]"; | ||||
|                 // 使用正则表达式将不符合规则的部分替换为下划线 | ||||
|                 string sanitizedFileName = Regex.Replace(HeaderText, pattern, "_"); | ||||
|                 await DownloadService.DownloadFromStreamAsync($"{sanitizedFileName}{DateTime.Now.ToFileDateTimeFormat()}.txt", memoryStream); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (PlatformService != null) | ||||
|                     await PlatformService.OnLogExport(LogPath); | ||||
|             } | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             await ToastService.Warn(ex); | ||||
|         } | ||||
|     } | ||||
|     private Task OnPause() | ||||
|     { | ||||
|         Pause = !Pause; | ||||
|         if (Pause) | ||||
|             PauseMessagesText = Messages.ToList(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private async Task RunTimerAsync() | ||||
|     { | ||||
|         while (!Disposed) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 await ExecuteAsync(); | ||||
|                 await InvokeAsync(StateHasChanged); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 NewLife.Log.XTrace.WriteException(ex); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -95,7 +95,7 @@ public partial class LogConsole : IDisposable | ||||
|  | ||||
|             if (LogPath != null) | ||||
|             { | ||||
|                 var files = await TextFileReadService.GetLogFiles(LogPath); | ||||
|                 var files = await TextFileReadService.GetLogFilesAsync(LogPath); | ||||
|                 if (!files.IsSuccess) | ||||
|                 { | ||||
|                     Messages = new List<LogMessage>(); | ||||
| @@ -106,7 +106,7 @@ public partial class LogConsole : IDisposable | ||||
|                     await Task.Run(async () => | ||||
|                     { | ||||
|                         Stopwatch sw = Stopwatch.StartNew(); | ||||
|                         var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault()); | ||||
|                         var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault()); | ||||
|                         if (result.IsSuccess) | ||||
|                         { | ||||
|                             Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList(); | ||||
| @@ -142,33 +142,7 @@ public partial class LogConsole : IDisposable | ||||
|  | ||||
|     private async Task Delete() | ||||
|     { | ||||
|         if (LogPath != null) | ||||
|         { | ||||
|             var files = await TextFileReadService.GetLogFiles(LogPath); | ||||
|             if (files.IsSuccess) | ||||
|             { | ||||
|                 foreach (var item in files.Content) | ||||
|                 { | ||||
|                     if (File.Exists(item)) | ||||
|                     { | ||||
|                         int error = 0; | ||||
|                         while (error < 3) | ||||
|                         { | ||||
|                             try | ||||
|                             { | ||||
|                                 FileUtil.DeleteFile(item); | ||||
|                                 break; | ||||
|                             } | ||||
|                             catch | ||||
|                             { | ||||
|                                 await Task.Delay(3000); | ||||
|                                 error++; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         await TextFileReadService.DeleteLogDataAsync(LogPath); | ||||
|     } | ||||
|  | ||||
|     private async Task HandleOnExportClick(MouseEventArgs args) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Razor; | ||||
| namespace ThingsGateway.Debug; | ||||
| 
 | ||||
| public class ValueTransformConfig | ||||
| { | ||||
| @@ -1,9 +1,5 @@ | ||||
| @namespace ThingsGateway.Gateway.Razor | ||||
| @using ThingsGateway.Admin.Application | ||||
| @using ThingsGateway.Admin.Razor | ||||
| @namespace ThingsGateway.Debug | ||||
| @using ThingsGateway.Foundation | ||||
| @using ThingsGateway.Gateway.Application | ||||
| @inherits ComponentDefault | ||||
| 
 | ||||
| <ValidateForm class="p-4 h-100" Model="@ValueTransformConfig" OnValidSubmit="OnSave"> | ||||
|     <EditorForm AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=1 LabelWidth=150 Model="ValueTransformConfig"> | ||||
| @@ -14,7 +14,7 @@ using System.Text.RegularExpressions; | ||||
| 
 | ||||
| using ThingsGateway.NewLife.Extension; | ||||
| 
 | ||||
| namespace ThingsGateway.Gateway.Razor; | ||||
| namespace ThingsGateway.Debug; | ||||
| 
 | ||||
| public partial class ValueTransformConfigPage | ||||
| { | ||||
| @@ -205,5 +205,10 @@ public partial class ValueTransformConfigPage | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Inject] | ||||
|     ToastService ToastService { get; set; } | ||||
| 
 | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     #endregion 修改 | ||||
| } | ||||
| @@ -1,4 +1,23 @@ | ||||
| { | ||||
|  | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformType": { | ||||
|     "None": "None", | ||||
|     "Linear": "Linear", | ||||
|     "Sqrt": "Sqrt" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformConfig": { | ||||
|     "TransformType": "TransformType", | ||||
|     "MinMax": "MinMax", | ||||
|     "ClampToRawRange": "ClampToRawRange", | ||||
|     "DecimalPlaces": "DecimalPlaces", | ||||
|     "RawMin": "RawMin", | ||||
|     "RawMax": "RawMax", | ||||
|     "ActualMin": "ActualMin", | ||||
|     "ActualMax": "ActualMax" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ChannelComponent": { | ||||
|     "BaudRate": "Baud Rate", | ||||
|     "BindUrl": "Local Bind IP Address", | ||||
|   | ||||
| @@ -1,4 +1,21 @@ | ||||
| { | ||||
|  | ||||
|   "ThingsGateway.Debug.ValueTransformType": { | ||||
|     "None": "无", | ||||
|     "Linear": "线性", | ||||
|     "Sqrt": "开方" | ||||
|   }, | ||||
|   "ThingsGateway.Debug.ValueTransformConfig": { | ||||
|     "TransformType": "转换方式", | ||||
|     "MinMax": "最小最大值", | ||||
|     "ClampToRawRange": "限制范围", | ||||
|     "DecimalPlaces": "保留小数位", | ||||
|     "RawMin": "原始最小值", | ||||
|     "RawMax": "原始最大值", | ||||
|     "ActualMin": "实际最小值", | ||||
|     "ActualMax": "实际最大值" | ||||
|   }, | ||||
|  | ||||
|   "ThingsGateway.Debug.ChannelComponent": { | ||||
|     "BaudRate": "波特率", | ||||
|     "BindUrl": "本地url", | ||||
|   | ||||
| @@ -40,7 +40,7 @@ public class PlatformService : IPlatformService | ||||
|             await using var jSObject = await JSRuntime.InvokeAsync<IJSObjectReference>("import", $"{WebsiteConst.DefaultResourceUrl}js/downloadFile.js"); | ||||
|             var path = Path.GetRelativePath("wwwroot", item); | ||||
|             string fileName = DateTime.Now.ToFileDateTimeFormat(); | ||||
|             await jSObject.InvokeVoidAsync("blazor_downloadFile", url, fileName, new { FileName = path }); | ||||
|             await jSObject.InvokeAsync<bool>("blazor_downloadFile", url, fileName, new { FileName = path }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -34,5 +34,6 @@ public class Startup : AppStartup | ||||
|  | ||||
|         services.AddScoped<IPlatformService, PlatformService>(); | ||||
|         services.AddSingleton<ITextFileReadService, TextFileReadService>(); | ||||
|         services.AddSingleton<TextFileReadService, TextFileReadService>(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| 	<Import Project="..\..\PackNuget.props" /> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>net8.0;</TargetFrameworks> | ||||
| 		 | ||||
| 		<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>--> | ||||
| 	</PropertyGroup> | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -5,9 +5,10 @@ | ||||
| 		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
| 		<NoPackageAnalysis>true</NoPackageAnalysis> | ||||
| 		<SignAssembly>false</SignAssembly> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" /> | ||||
| 		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" /> | ||||
| 	</ItemGroup> | ||||
| </Project> | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFrameworks>netstandard2.0;</TargetFrameworks> | ||||
| 		<Version>$(SourceGeneratorVersion)</Version> | ||||
| 		 | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -8,6 +8,8 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using System.Buffers; | ||||
|  | ||||
| namespace ThingsGateway.Foundation; | ||||
|  | ||||
| /// <summary> | ||||
| @@ -16,12 +18,12 @@ namespace ThingsGateway.Foundation; | ||||
| public abstract class DDPMessage : MessageBase, IResultMessage | ||||
| { | ||||
|     /// <inheritdoc/> | ||||
|     public override int HeaderLength => 4; | ||||
|     public override long HeaderLength => 4; | ||||
|     public byte Type = 0; | ||||
|     public string Id; | ||||
|     public override FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     { | ||||
|         Id = byteBlock.ToString(byteBlock.Position, 11).Replace("\0", ""); | ||||
|         Id = byteBlock.ToString(byteBlock.BytesRead, 11).Replace("\0", ""); | ||||
|         OperCode = 0; | ||||
|  | ||||
|         Content = GetContent(ref byteBlock); | ||||
| @@ -44,31 +46,31 @@ public abstract class DDPMessage : MessageBase, IResultMessage | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public abstract int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader; | ||||
|     public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader; | ||||
|     public abstract long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader; | ||||
|     public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader; | ||||
| } | ||||
|  | ||||
| public class DDPTcpMessage : DDPMessage | ||||
| { | ||||
|     public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     { | ||||
|         return ReaderExtension.ReadValue<TByteBlock, ushort>(ref byteBlock, EndianType.Big) - 4; | ||||
|     } | ||||
|  | ||||
|     public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     { | ||||
|         return byteBlock.Span.Slice(byteBlock.Position + 11, BodyLength - 12).ToArray(); | ||||
|         return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 11, BodyLength - 12).ToArray(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class DDPUdpMessage : DDPMessage | ||||
| { | ||||
|     public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     { | ||||
|         return byteBlock.Length - 4; | ||||
|         return (byteBlock.BytesRead + byteBlock.BytesRemaining - 4); | ||||
|     } | ||||
|     public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) | ||||
|     { | ||||
|         return byteBlock.Span.Slice(byteBlock.Position + 12, BodyLength - 12).ToArray(); | ||||
|         return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 12, BodyLength - 12).ToArray(); | ||||
|     } | ||||
| } | ||||
| @@ -25,6 +25,7 @@ public class DDPSend : ISendMessage | ||||
|     string Id; | ||||
|     byte Command; | ||||
|     bool Tcp; | ||||
|  | ||||
|     public DDPSend(ReadOnlyMemory<byte> readOnlyMemory, string id, bool tcp, byte command = 0x89) | ||||
|     { | ||||
|         Tcp = tcp; | ||||
| @@ -32,7 +33,8 @@ public class DDPSend : ISendMessage | ||||
|         Id = id; | ||||
|         Command = command; | ||||
|     } | ||||
|     public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockWriter | ||||
|  | ||||
|     public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesWriter | ||||
|     { | ||||
|         WriterExtension.WriteValue(ref byteBlock, (byte)0x7b); | ||||
|         WriterExtension.WriteValue(ref byteBlock, (byte)Command); | ||||
|   | ||||
| @@ -10,8 +10,6 @@ | ||||
|  | ||||
| using System.Runtime.CompilerServices; | ||||
|  | ||||
| using ThingsGateway.NewLife; | ||||
|  | ||||
| using TouchSocket.Resources; | ||||
|  | ||||
| namespace ThingsGateway.Foundation; | ||||
| @@ -33,50 +31,120 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel | ||||
|             DDPAdapter.Config(Config); | ||||
|         } | ||||
|  | ||||
|         // 将当前实例的日志记录器和加载回调设置到适配器中 | ||||
|         DDPAdapter.Logger = Logger; | ||||
|         DDPAdapter.OnLoaded(this); | ||||
|  | ||||
|         DDPAdapter.SendAsyncCallBack = DDPSendAsync; | ||||
|         DDPAdapter.ReceivedAsyncCallBack = DDPHandleReceivedData; | ||||
|         DataHandlingAdapter.SendAsyncCallBack = DefaultSendAsync; | ||||
|         return base.OnTcpConnected(e); | ||||
|     } | ||||
|     protected Task DefaultSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken) | ||||
|     { | ||||
|         return DDPAdapter.SendInputAsync(new DDPSend(memory, Id, true), cancellationToken); | ||||
|     } | ||||
|     protected Task DDPSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken) | ||||
|     { | ||||
|         return base.ProtectedDefaultSendAsync(memory, cancellationToken); | ||||
|     } | ||||
|  | ||||
|     private DDPMessage DDPMessage { get; set; } | ||||
|     private Task DDPHandleReceivedData(IByteBlockReader byteBlock, IRequestInfo requestInfo) | ||||
|     #region 发送 | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 异步发送数据,通过适配器模式灵活处理数据发送。 | ||||
|     /// </summary> | ||||
|     /// <param name="memory">待发送的只读字节内存块。</param> | ||||
|     /// <param name="token">可取消令箭</param> | ||||
|     /// <returns>一个异步任务,表示发送操作。</returns> | ||||
|     protected virtual async Task NewProtectedSendAsync(ReadOnlyMemory<byte> memory, CancellationToken token) | ||||
|     { | ||||
|         if (requestInfo is DDPMessage dDPMessage) | ||||
|             DDPMessage = dDPMessage; | ||||
|         this.ThrowIfDisposed(); | ||||
|         this.ThrowIfClientNotConnected(); | ||||
|  | ||||
|         return EasyTask.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new(); | ||||
|     private WaitLock _waitLock = new(nameof(DDPTcpSessionClientChannel)); | ||||
|         if (!await this.OnTcpSending(memory).ConfigureAwait(false)) return; | ||||
|  | ||||
|     protected override async ValueTask<bool> OnTcpReceiving(IByteBlockReader byteBlock) | ||||
|     { | ||||
|         DDPMessage? message = null; | ||||
|         var transport = this.Transport; | ||||
|         var adapter = this.DataHandlingAdapter; | ||||
|         var locker = transport.SemaphoreSlimForWriter; | ||||
|  | ||||
|         await locker.WaitAsync(token).ConfigureAwait(false); | ||||
|         try | ||||
|         { | ||||
|             await _waitLock.WaitAsync().ConfigureAwait(false); | ||||
|             await DDPAdapter.ReceivedInputAsync(byteBlock).ConfigureAwait(false); | ||||
|  | ||||
|             message = DDPMessage; | ||||
|             DDPMessage = null; | ||||
|             // 如果数据处理适配器未设置,则使用默认发送方式。 | ||||
|             if (adapter == null) | ||||
|             { | ||||
|                 await transport.Output.WriteAsync(memory, token).ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var byteBlock = new ByteBlock(1024); | ||||
|                 var ddpSend = new DDPSend(memory, Id, true); | ||||
|                 ddpSend.Build(ref byteBlock); | ||||
|                 var newMemory = byteBlock.Memory; | ||||
|                 var writer = new PipeBytesWriter(transport.Output); | ||||
|                 adapter.SendInput(ref writer, in newMemory); | ||||
|                 await writer.FlushAsync(token).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             _waitLock.Release(); | ||||
|             locker.Release(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 异步发送请求信息的受保护方法。 | ||||
|     /// | ||||
|     /// 此方法首先检查当前对象是否能够发送请求信息,如果不能,则抛出异常。 | ||||
|     /// 如果可以发送,它将使用数据处理适配器来异步发送输入请求。 | ||||
|     /// </summary> | ||||
|     /// <param name="requestInfo">要发送的请求信息。</param> | ||||
|     /// <param name="token">可取消令箭</param> | ||||
|     /// <returns>返回一个任务,该任务代表异步操作的结果。</returns> | ||||
|     protected virtual async Task NewProtectedSendAsync(IRequestInfo requestInfo, CancellationToken token) | ||||
|     { | ||||
|         // 检查是否具备发送请求的条件,如果不具备则抛出异常 | ||||
|         this.ThrowIfCannotSendRequestInfo(); | ||||
|  | ||||
|         this.ThrowIfDisposed(); | ||||
|         this.ThrowIfClientNotConnected(); | ||||
|  | ||||
|         var transport = this.Transport; | ||||
|         var adapter = this.DataHandlingAdapter; | ||||
|         var locker = transport.SemaphoreSlimForWriter; | ||||
|  | ||||
|         await locker.WaitAsync(token).ConfigureAwait(false); | ||||
|         try | ||||
|         { | ||||
|             var byteBlock = new ByteBlock(1024); | ||||
|             if (requestInfo is not IRequestInfoBuilder requestInfoBuilder) | ||||
|             { | ||||
|                 throw new Exception(); | ||||
|             } | ||||
|             requestInfoBuilder.Build(ref byteBlock); | ||||
|             var ddpSend = new DDPSend(byteBlock.Memory, Id, true); | ||||
|  | ||||
|             var writer = new PipeBytesWriter(transport.Output); | ||||
|             adapter.SendInput(ref writer, ddpSend); | ||||
|             await writer.FlushAsync(token).ConfigureAwait(false); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             locker.Release(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     #endregion 发送 | ||||
|     public override Task SendAsync(IRequestInfo requestInfo, CancellationToken token = default) | ||||
|     { | ||||
|         return NewProtectedSendAsync(requestInfo, token); | ||||
|     } | ||||
|  | ||||
|     public override Task SendAsync(ReadOnlyMemory<byte> memory, CancellationToken token = default) | ||||
|     { | ||||
|         return NewProtectedSendAsync(memory, token); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new(); | ||||
|  | ||||
|     protected override async ValueTask<bool> OnTcpReceiving(IBytesReader byteBlock) | ||||
|     { | ||||
|  | ||||
|         if (DDPAdapter.TryParseRequest(ref byteBlock, out var message)) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (message != null) | ||||
| @@ -86,15 +154,15 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel | ||||
|                 var id = $"ID={message.Id}"; | ||||
|                 if (message.Type == 0x09) | ||||
|                 { | ||||
|                     var reader = new ByteBlockReader(message.Content); | ||||
|                     var reader = new ClassBytesReader(message.Content); | ||||
|  | ||||
|                     if (this.DataHandlingAdapter == null) | ||||
|                     { | ||||
|                         await this.OnTcpReceived(new ReceivedDataEventArgs(reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext); | ||||
|                         await this.OnTcpReceived(new ReceivedDataEventArgs(message.Content, default)).ConfigureAwait(false); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext); | ||||
|                         await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(false); | ||||
|                     } | ||||
|  | ||||
|                     return true; | ||||
| @@ -127,16 +195,16 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         await ResetIdAsync(id).ConfigureAwait(false); | ||||
|                         await ResetIdAsync(id, ClosedToken).ConfigureAwait(false); | ||||
|  | ||||
|                         //发送成功 | ||||
|                         await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false); | ||||
|                         await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false); | ||||
|                         if (log) | ||||
|                             Logger?.Info(string.Format(AppResource.DtuConnected, Id)); | ||||
|                     } | ||||
|                     else if (message.Type == 0x02) | ||||
|                     { | ||||
|                         await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false); | ||||
|                         await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false); | ||||
|                         Logger?.Info(string.Format(AppResource.DtuDisconnecting, Id)); | ||||
|                         await Task.Delay(100).ConfigureAwait(false); | ||||
|                         await this.CloseAsync().ConfigureAwait(false); | ||||
|   | ||||
| @@ -36,16 +36,12 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe | ||||
|             DDPAdapter.Config(Config); | ||||
|         } | ||||
|  | ||||
|         // 将当前实例的日志记录器和加载回调设置到适配器中 | ||||
|         DDPAdapter.Logger = Logger; | ||||
|  | ||||
|         if (DDPAdapter.Owner != null) | ||||
|         { | ||||
|             DDPAdapter.OnLoaded(this); | ||||
|         } | ||||
|  | ||||
|         DDPAdapter.SendCallBackAsync = DDPSendAsync; | ||||
|         DDPAdapter.ReceivedCallBack = DDPHandleReceivedData; | ||||
|         DDPAdapter.SendCallBackAsync = base.ProtectedDefaultSendAsync; | ||||
|         DataHandlingAdapter.SendCallBackAsync = DefaultSendAsync; | ||||
|     } | ||||
|  | ||||
| @@ -62,22 +58,7 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected Task DDPSendAsync(EndPoint endPoint, ReadOnlyMemory<byte> memory, CancellationToken token) | ||||
|     { | ||||
|         //获取endpoint | ||||
|         return base.ProtectedDefaultSendAsync(endPoint, memory, token); | ||||
|     } | ||||
|  | ||||
|     private ConcurrentDictionary<EndPoint, DDPMessage> DDPMessageDict { get; set; } = new(); | ||||
|     private Task DDPHandleReceivedData(EndPoint endPoint, IByteBlockReader byteBlock, IRequestInfo requestInfo) | ||||
|     { | ||||
|         if (requestInfo is DDPMessage dDPMessage) | ||||
|         { | ||||
|             DDPMessageDict.AddOrUpdate(endPoint, dDPMessage); | ||||
|         } | ||||
|  | ||||
|         return EasyTask.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private DeviceUdpDataHandleAdapter<DDPUdpMessage> DDPAdapter = new(); | ||||
|  | ||||
| @@ -98,27 +79,14 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe | ||||
|         return base.StopAsync(token); | ||||
|     } | ||||
|  | ||||
|     private ConcurrentDictionary<EndPoint, WaitLock> _waitLocks = new(); | ||||
|  | ||||
|     protected override async ValueTask<bool> OnUdpReceiving(UdpReceiveingEventArgs e) | ||||
|     { | ||||
|         var byteBlock = e.ByteBlock; | ||||
|         var byteBlock = e.Memory; | ||||
|         var endPoint = e.EndPoint; | ||||
|         DDPMessage? message = null; | ||||
|         var waitLock = _waitLocks.GetOrAdd(endPoint, new WaitLock(nameof(DDPUdpSessionChannel))); | ||||
|         try | ||||
|         { | ||||
|             await waitLock.WaitAsync().ConfigureAwait(false); | ||||
|             await DDPAdapter.ReceivedInput(endPoint, byteBlock).ConfigureAwait(EasyTask.ContinueOnCapturedContext); | ||||
|  | ||||
|             if (DDPMessageDict.TryGetValue(endPoint, out var dDPMessage)) | ||||
|                 message = dDPMessage; | ||||
|             DDPMessageDict.TryRemove(endPoint, out _); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             waitLock.Release(); | ||||
|         } | ||||
|         if (!DDPAdapter.TryParseRequest(endPoint, byteBlock, out var message)) | ||||
|             return true; | ||||
|  | ||||
|         if (message != null) | ||||
|         { | ||||
| @@ -127,15 +95,13 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe | ||||
|                 var id = $"ID={message.Id}"; | ||||
|                 if (message.Type == 0x09) | ||||
|                 { | ||||
|                     var reader = new ByteBlockReader(message.Content); | ||||
|  | ||||
|                     if (this.DataHandlingAdapter == null) | ||||
|                     { | ||||
|                         await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext); | ||||
|                         await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, message.Content, default)).ConfigureAwait(false); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         await this.DataHandlingAdapter.ReceivedInput(endPoint, reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext); | ||||
|                         await this.DataHandlingAdapter.ReceivedInputAsync(endPoint, message.Content).ConfigureAwait(false); | ||||
|                     } | ||||
|  | ||||
|                     return true; | ||||
|   | ||||
| @@ -37,7 +37,9 @@ public static class ChannelOptionsExtensions | ||||
|             for (int i = 0; i < funcs.Count; i++) | ||||
|             { | ||||
|                 var func = funcs[i]; | ||||
|                 await func.Invoke(clientChannel, e, i == funcs.Count - 1).ConfigureAwait(false); | ||||
|                 var task = func.Invoke(clientChannel, e, i == funcs.Count - 1); | ||||
|                 if (!task.IsCompleted) | ||||
|                     await task.ConfigureAwait(false); | ||||
|                 if (e.Handled) | ||||
|                 { | ||||
|                     break; | ||||
| @@ -217,7 +219,7 @@ public static class ChannelOptionsExtensions | ||||
| #if NETSTANDARD || NET6_0_OR_GREATER | ||||
|                 if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) | ||||
|                 { | ||||
|                     config.UseUdpConnReset(); | ||||
|                     config.SetUdpConnReset(true); | ||||
|                 } | ||||
| #endif | ||||
|                 return ddpUdp; | ||||
| @@ -229,7 +231,7 @@ public static class ChannelOptionsExtensions | ||||
| #if NETSTANDARD || NET6_0_OR_GREATER | ||||
|                 if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) | ||||
|                 { | ||||
|                     config.UseUdpConnReset(); | ||||
|                     config.SetUdpConnReset(true); | ||||
|                 } | ||||
| #endif | ||||
|                 return udpSessionChannel; | ||||
|   | ||||
| @@ -8,8 +8,6 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using System.Collections.Concurrent; | ||||
|  | ||||
| namespace ThingsGateway.Foundation; | ||||
|  | ||||
| /// <summary> | ||||
| @@ -59,15 +57,9 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC | ||||
|     /// </summary> | ||||
|     public ChannelEventHandler Stoping { get; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 主动请求时的等待池 | ||||
|     /// </summary> | ||||
|     public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } | ||||
|  | ||||
|     void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue); | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// <summary> | ||||
|   | ||||
| @@ -22,6 +22,8 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn | ||||
|     /// </summary> | ||||
|     DataHandlingAdapter ReadOnlyDataHandlingAdapter { get; } | ||||
|  | ||||
|  | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 通道等待池 | ||||
|     /// </summary> | ||||
| @@ -34,4 +36,6 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn | ||||
|     /// </summary> | ||||
|     /// <param name="adapter">适配器</param> | ||||
|     void SetDataHandlingAdapter(DataHandlingAdapter adapter); | ||||
|  | ||||
|     void SetDataHandlingAdapterLogger(ILog log); | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user