Compare commits

...

40 Commits

Author SHA1 Message Date
2248356998 qq.com
179ca0aa0e build: 10.11.23
feat: 增加部分demo
feat: 增加管理软件桌面端
2025-09-03 17:40:19 +08:00
Diego
fc09a52da1 build: 10.11.22
refactor:性能优化
2025-09-02 11:13:54 +08:00
Diego
5436b91c89 10.11.21 2025-09-01 18:40:12 +08:00
Diego
d1c46f51a6 添加api接口 2025-09-01 16:00:53 +08:00
Diego
4539d8d198 fix: 字符串反转功能失效 2025-09-01 11:52:03 +08:00
2248356998 qq.com
9ea9529a5f 10.11.19 2025-08-29 15:43:02 +08:00
2248356998 qq.com
4e6be23aac 10.11.17 2025-08-28 17:15:08 +08:00
2248356998 qq.com
2fabbd236b 10.11.16 2025-08-28 17:05:00 +08:00
2248356998 qq.com
163a66530e fix: dlt645校验和 2025-08-28 17:04:27 +08:00
2248356998 qq.com
29073a00c4 fix: dtu连接注册包解析错误 2025-08-28 15:35:43 +08:00
2248356998 qq.com
c6d4d1ecfa fix: dtu连接注册包解析错误 2025-08-28 15:20:00 +08:00
2248356998 qq.com
ba16889cad 10.11.14 2025-08-28 14:21:58 +08:00
2248356998 qq.com
5aaed35b0f 10.11.12 2025-08-28 01:42:59 +08:00
Diego
df067c91eb 10.11.12 2025-08-28 01:39:39 +08:00
2248356998 qq.com
2078b4a60b 补充忽略的文件 2025-08-26 08:54:09 +08:00
2248356998 qq.com
20a2e3ff8e 示例 2025-08-25 20:39:26 +08:00
Diego
61a973b1b5 !73 10.11.10 2025-08-25 12:21:12 +00:00
2248356998 qq.com
cbd72e2081 添加文本日志配置json文件 2025-08-22 16:17:25 +08:00
2248356998 qq.com
4e0377b20c 添加文本日志配置json文件 2025-08-22 16:15:54 +08:00
2248356998 qq.com
fd318d3cdc 移除警告项 2025-08-22 16:12:14 +08:00
2248356998 qq.com
515bdb9700 10.11.7 2025-08-21 23:35:12 +08:00
2248356998 qq.com
46c1780017 10.11.6 2025-08-21 22:40:53 +08:00
2248356998 qq.com
fe78a4c3ca 更新依赖库 2025-08-21 22:05:30 +08:00
2248356998 qq.com
2d7effadf9 10.11.4 2025-08-21 21:13:18 +08:00
Diego
346c560f8b !72 更新依赖项 2025-08-21 11:46:58 +00:00
Diego
8e3bd89f61 修改编译项 2025-08-18 17:30:34 +08:00
Diego
6da142d080 10.10.23 2025-08-18 17:03:40 +08:00
2248356998 qq.com
ff7d029e6f 更新依赖 2025-08-14 20:25:00 +08:00
2248356998 qq.com
21b4695683 10.10.19 2025-08-14 05:47:54 +08:00
Diego
02ad494a26 !71 适配远程客户端 2025-08-12 16:00:53 +00:00
2248356998 qq.com
280366e1b2 添加写优先选项,默认false 2025-08-09 13:07:08 +08:00
2248356998 qq.com
6660ce3e34 适配远程管理客户端 2025-08-08 18:01:24 +08:00
2248356998 qq.com
7499162c1a 适配远程管理客户端 2025-08-08 02:51:42 +08:00
2248356998 qq.com
40208a5cd6 适配远程网关管理客户端 2025-08-08 02:16:05 +08:00
2248356998 qq.com
fa347f4f68 修改报警事件时间字段,增加变量表报警类 2025-08-07 19:19:36 +08:00
Diego
d7df6fc605 10.10.12 2025-08-07 11:24:57 +08:00
2248356998 qq.com
eb4bb2fd48 10.10.11 2025-08-07 10:19:28 +08:00
2248356998 qq.com
faa9858974 10.10.11 2025-08-07 10:18:22 +08:00
2248356998 qq.com
1b3d2dda49 QuestDbRestAPI 2025-08-07 10:11:37 +08:00
Diego
a8a9453611 !70 fix: questdb restapi多实例 2025-08-07 01:58:32 +00:00
470 changed files with 12916 additions and 5283 deletions

1
.gitignore vendored
View File

@@ -364,6 +364,7 @@ FodyWeavers.xsd
/src/*Pro*/
/src/*Pro*
/src/**/*Pro*
/src/*pro*
/src/*pro*/
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json

View File

@@ -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>

View File

@@ -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('/', '_')));
}
}

View File

@@ -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"
},

View File

@@ -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": "用户登录已过期,请重新登录"
},

View File

@@ -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);

View File

@@ -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删除用户数据
}

View File

@@ -5,6 +5,7 @@
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;}" />

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -18,6 +18,7 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
</PropertyGroup>
<ItemGroup>

View File

@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
/// <inheritdoc/>
[ThingsGateway.DependencyInjection.SuppressSniffer]
public static class ResourceUtil
public static class AdminResourceUtil
{
/// <summary>
/// 构造选择项ID/TITLE

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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()));

View File

@@ -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 -->

View File

@@ -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 =>
{

View File

@@ -183,19 +183,22 @@ public class Startup : AppStartup
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
if (!NewLife.Runtime.IsLegacyWindows)
{
#if NET9_0_OR_GREATER
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#else
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
}
}
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)

View File

@@ -5,6 +5,7 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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)

View 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"
}
}

View File

@@ -0,0 +1,13 @@
{
"ThingsGateway.DB.BaseDataEntity": {
"CreateOrgId": "创建机构Id"
},
"ThingsGateway.DB.BaseEntity": {
"CreateTime": "创建时间",
"CreateUser": "创建人",
"SortCode": "排序",
"UpdateTime": "更新时间",
"UpdateUser": "更新人"
}
}

View File

@@ -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" />-->

View File

@@ -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));

View File

@@ -411,7 +411,7 @@ public static class SpecificationDocumentBuilder
// 本地函数
static string DefaultSchemaIdSelector(Type modelType)
{
var modelName = modelType.Name;
var modelName = modelType.FullName;
// 处理泛型类型问题
if (modelType.IsConstructedGenericType)

View File

@@ -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>

View File

@@ -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;
}

View 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();
}

View File

@@ -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();
}
}

View File

@@ -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.0Win8.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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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>

View File

@@ -12,6 +12,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

View File

@@ -168,12 +168,13 @@ public class TimerScheduler : ILogFeature
_period = 60_000;
foreach (var timer in arr)
{
if (!timer.Calling && CheckTime(timer, now))
if ((timer.Reentrant || !timer.Calling) && CheckTime(timer, now))
{
// 必须在主线程设置状态,否则可能异步线程还没来得及设置开始状态,主线程又开始了新的一轮调度
timer.Calling = true;
if (timer.IsAsyncTask)
Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
ExecuteAsync(timer);
//Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
else if (!timer.Async)
Execute(timer);
else
@@ -306,8 +307,23 @@ public class TimerScheduler : ILogFeature
return;
}
var func = timer.Method.As<Func<Object?, Task>>(target);
await func!(timer.State).ConfigureAwait(false);
#if NET6_0_OR_GREATER
if (timer.IsValueTask)
{
var func = timer.Method.As<Func<Object?, ValueTask>>(target);
var task = func!(timer.State);
if (!task.IsCompleted)
await task.ConfigureAwait(false);
}
else
#endif
{
var func = timer.Method.As<Func<Object?, Task>>(target);
var task = func!(timer.State);
if (!task.IsCompleted)
await task.ConfigureAwait(false);
}
}
catch (ThreadAbortException) { throw; }
catch (ThreadInterruptedException) { throw; }

View File

@@ -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()

View File

@@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@@ -8,8 +8,8 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Common.Extension;
using ThingsGateway.NewLife;
using ThingsGateway.Razor.Extension;
namespace ThingsGateway.Razor;

View File

@@ -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]

View File

@@ -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)
{

View File

@@ -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; }
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}

View File

@@ -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
{
}
}
}

View File

@@ -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"
},

View File

@@ -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": "验证内容"
},

View File

@@ -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>

View File

@@ -1,9 +0,0 @@
// 设置 culture
function setCultureLocalStorage(culture) {
localStorage.setItem("culture", culture);
}
// 获取 culture
function getCultureLocalStorage() {
return localStorage.getItem("culture");
}

View 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));
}
}

View File

@@ -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,35 +132,43 @@ 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));
// 构建schema信息
columns.ForEach(d =>
var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () =>
{
if (d.DataType == "TIMESTAMP")
var list = new List<Hashtable>();
// 构建schema信息
columns.ForEach(d =>
{
list.Add(new Hashtable()
if (d.DataType == "TIMESTAMP")
{
list.Add(new Hashtable()
{
{ "name", d.DbColumnName },
{ "type", d.DataType },
{ "pattern", dateFormat}
});
}
else
{
list.Add(new Hashtable()
}
else
{
list.Add(new Hashtable()
{
{ "name", d.DbColumnName },
{ "type", d.DataType }
});
}
});
}
});
return list;
}
);
var schema = JsonConvert.SerializeObject(list);
// 写入CSV文件
@@ -184,8 +197,8 @@ namespace ThingsGateway.SqlSugar
"multipart/form-data; boundary=" + boundary);
// 发送请求并处理响应
var httpResponseMessage =
await Post(client, tableName, httpContent).ConfigureAwait(false);
using var httpResponseMessage =
await Post(client, tableName, httpContent).ConfigureAwait(false);
var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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>();

View File

@@ -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
{

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -93,30 +93,38 @@ public static class ExpressionEvaluatorExtension
public static ReadWriteExpressions GetOrAddScript(string source)
{
var field = $"{CacheKey}-{source}";
var exfield = $"{CacheKey}-Exception-{source}";
var runScript = Instance.Get<ReadWriteExpressions>(field);
if (runScript == null)
{
if (!source.Contains("return"))
var hasValue = Instance.TryGetValue<ReadWriteExpressions>(field, out runScript);
if (!hasValue)
{
source = $"return {source}";//只判断简单脚本中可省略return字符串
}
var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
var _using = new StringBuilder();
var _body = new StringBuilder();
src.ToList().ForEach(l =>
{
if (l.StartsWith("using "))
if (!source.Contains("return"))
{
_using.AppendLine(l);
source = $"return {source}";//只判断简单脚本中可省略return字符串
}
else
var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
var _using = new StringBuilder();
var _body = new StringBuilder();
src.ToList().ForEach(l =>
{
_body.AppendLine(l);
}
});
// 动态加载并执行代码
runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>(
$@"
if (l.StartsWith("using "))
{
_using.AppendLine(l);
}
else
{
_body.AppendLine(l);
}
});
// 动态加载并执行代码
try
{
runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>(
$@"
using System;
using System.Linq;
using System.Collections.Generic;
@@ -137,9 +145,26 @@ public static class ExpressionEvaluatorExtension
}}
}}
");
Instance.Set(field, runScript);
Instance.Set(field, runScript);
}
catch (Exception ex)
{
//如果编译失败应该不重复编译避免oom
Instance.Set<ReadWriteExpressions>(field, null, TimeSpan.FromHours(1));
Instance.Set(exfield, ex, TimeSpan.FromHours(1));
throw;
}
}
}
Instance.SetExpire(field, TimeSpan.FromHours(1));
Instance.SetExpire(exfield, TimeSpan.FromHours(1));
if (runScript == null)
{
throw (Instance.Get<Exception>(exfield) ?? new Exception("compilation error"));
}
return runScript;
}

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Reliability", "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")]

View 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;

View 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; }
}

View 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();
}
}

View File

@@ -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)
{
}
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}
}

View File

@@ -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)

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Razor;
namespace ThingsGateway.Debug;
public class ValueTransformConfig
{

View File

@@ -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">

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 });
}
}
}

View File

@@ -34,5 +34,6 @@ public class Startup : AppStartup
services.AddScoped<IPlatformService, PlatformService>();
services.AddSingleton<ITextFileReadService, TextFileReadService>();
services.AddSingleton<TextFileReadService, TextFileReadService>();
}
}

View File

@@ -4,6 +4,7 @@
<Import Project="..\..\PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>net8.0;</TargetFrameworks>
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
</PropertyGroup>
<ItemGroup>

View File

@@ -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>

View File

@@ -5,6 +5,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
<Version>$(SourceGeneratorVersion)</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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