Compare commits

...

17 Commits

Author SHA1 Message Date
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
2248356998 qq.com
e84f42ce14 10.10.10 2025-08-06 21:42:46 +08:00
2248356998 qq.com
6f814cf6b8 更新questdb restapi启用字段 2025-08-06 21:42:23 +08:00
2248356998 qq.com
e36432e4e9 10.10.9 2025-08-06 19:33:30 +08:00
Diego
ebd71e807b !69 更新依赖 2025-08-06 11:25:31 +00:00
443 changed files with 8199 additions and 7679 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>
<!-- 可选 -->

View File

@@ -39,7 +39,7 @@ public class FileController : ControllerBase
var filePath = Path.Combine(wwwroot, fileName);
// 防止路径穿越攻击
#pragma warning disable CA3003
if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
if (filePath.Contains("..") || !System.IO.File.Exists(filePath))
{
return NotFound();
}
@@ -49,6 +49,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

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

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

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

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

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.9.2" />
</ItemGroup>
<ItemGroup>

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

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

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

View File

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

View File

@@ -306,8 +306,19 @@ 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);
await func!(timer.State).ConfigureAwait(false);
}
else
#endif
{
var func = timer.Method.As<Func<Object?, Task>>(target);
await func!(timer.State).ConfigureAwait(false);
}
}
catch (ThreadAbortException) { throw; }
catch (ThreadInterruptedException) { throw; }

View File

@@ -87,6 +87,8 @@ public class TimerX : ITimer, ITimerx, IDisposable
private DateTime _AbsolutelyNext;
private readonly Cron[]? _crons;
internal bool IsValueTask { get; }
#endregion
// #region 静态
@@ -158,6 +160,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 +235,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 +330,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,6 +3,7 @@
<Import Project="..\..\PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" />

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

@@ -27,16 +27,17 @@
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertDatas">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public int BulkCopy<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public int BulkCopy<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
int result = 0;
// 使用分页方式处理大数据量插入
db.Utilities.PageEach(insertDatas, pageSize, pageItems =>
{
// 同步调用批量插入API并累加结果
result += questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).GetAwaiter().GetResult();
result += questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).GetAwaiter().GetResult();
});
return result;
}
@@ -46,16 +47,17 @@
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertDatas">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
int result = 0;
// 异步分页处理大数据量插入
await db.Utilities.PageEachAsync(insertDatas, pageSize, async pageItems =>
{
// 异步调用批量插入API并累加结果
result += await questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).ConfigureAwait(false);
result += await questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).ConfigureAwait(false);
}).ConfigureAwait(false);
return result;
}

View File

@@ -7,16 +7,12 @@ namespace ThingsGateway.SqlSugar
/// <summary>
/// 绑定RestAPI需要的信息
/// </summary>
public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string httpPort, ref string username, ref string password)
public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string username, ref string password)
{
if (builder.TryGetValue("Host", out object hostValue))
{
host = Convert.ToString(hostValue);
}
if (builder.TryGetValue("HttpPort", out object httpPortValue))
{
httpPort = Convert.ToString(httpPortValue);
}
if (builder.TryGetValue("Username", out object usernameValue))
{
username = Convert.ToString(usernameValue);

View File

@@ -28,16 +28,16 @@ namespace ThingsGateway.SqlSugar
/// 初始化 QuestDbRestAPI 实例
/// </summary>
/// <param name="db">SqlSugar 数据库客户端</param>
public QuestDbRestAPI(ISqlSugarClient db)
/// <param name="httpPort">restApi端口</param>
public QuestDbRestAPI(ISqlSugarClient db, int httpPort = 9000)
{
var builder = new DbConnectionStringBuilder();
builder.ConnectionString = db.CurrentConnectionConfig.ConnectionString;
this.db = db;
string httpPort = String.Empty;
string host = String.Empty;
string username = String.Empty;
string password = String.Empty;
QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref httpPort, ref username, ref password);
QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref username, ref password);
BindHost(host, httpPort, username, password);
}
@@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar
// HTTP GET 请求执行SQL
var result = string.Empty;
var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(authorization))
client.DefaultRequestHeaders.Add("Authorization", authorization);
var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false);
{
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
}
using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false);
result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
return result;
}
@@ -68,34 +73,34 @@ namespace ThingsGateway.SqlSugar
return ExecuteCommandAsync(sql).GetAwaiter().GetResult();
}
/// <summary>
/// 异步批量插入单条数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertData">要插入的数据</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>影响的行数</returns>
public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
if (db.CurrentConnectionConfig.MoreSettings == null)
db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
var sql = db.InsertableT(insertData).ToSqlString();
var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
}
///// <summary>
///// 异步批量插入单条数据
///// </summary>
///// <typeparam name="T">数据类型</typeparam>
///// <param name="insertData">要插入的数据</param>
///// <param name="dateFormat">日期格式字符串</param>
///// <returns>影响的行数</returns>
//public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
//{
// if (db.CurrentConnectionConfig.MoreSettings == null)
// db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
// db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
// var sql = db.InsertableT(insertData).ToSqlString();
// var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
// return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
//}
/// <summary>
/// 同步批量插入单条数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertData">要插入的数据</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>影响的行数</returns>
public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
}
///// <summary>
///// 同步批量插入单条数据
///// </summary>
///// <typeparam name="T">数据类型</typeparam>
///// <param name="insertData">要插入的数据</param>
///// <param name="dateFormat">日期格式字符串</param>
///// <returns>影响的行数</returns>
//public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
//{
// return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
//}
/// <summary>
/// 创建分页批量插入器
@@ -115,9 +120,10 @@ namespace ThingsGateway.SqlSugar
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertList">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public async Task<int> BulkCopyAsync<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public async Task<int> BulkCopyAsync<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
var result = 0;
var fileName = $"{Guid.NewGuid()}.csv";
@@ -126,35 +132,43 @@ namespace ThingsGateway.SqlSugar
{
// 准备多部分表单数据
var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
var list = new List<Hashtable>();
var name = db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
// 获取或创建列信息缓存
var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode();
var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () =>
db.CopyNew().DbMaintenance.GetColumnInfosByTableName(name));
// 构建schema信息
columns.ForEach(d =>
db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName));
var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () =>
{
if (d.DataType == "TIMESTAMP")
var list = new List<Hashtable>();
// 构建schema信息
columns.ForEach(d =>
{
list.Add(new Hashtable()
if (d.DataType == "TIMESTAMP")
{
list.Add(new Hashtable()
{
{ "name", d.DbColumnName },
{ "type", d.DataType },
{ "pattern", dateFormat}
});
}
else
{
list.Add(new Hashtable()
}
else
{
list.Add(new Hashtable()
{
{ "name", d.DbColumnName },
{ "type", d.DataType }
});
}
});
}
});
return list;
}
);
var schema = JsonConvert.SerializeObject(list);
// 写入CSV文件
@@ -170,8 +184,8 @@ namespace ThingsGateway.SqlSugar
// 准备HTTP请求内容
using var httpContent = new MultipartFormDataContent(boundary);
using var fileStream = File.OpenRead(filePath);
if (!string.IsNullOrWhiteSpace(this.authorization))
client.DefaultRequestHeaders.Add("Authorization", this.authorization);
//if (!string.IsNullOrWhiteSpace(this.authorization))
// client.DefaultRequestHeaders.Add("Authorization", this.authorization);
httpContent.Add(new StringContent(schema), "schema");
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
@@ -183,8 +197,8 @@ namespace ThingsGateway.SqlSugar
"multipart/form-data; boundary=" + boundary);
// 发送请求并处理响应
var httpResponseMessage =
await Post(client, name, httpContent).ConfigureAwait(false);
using var httpResponseMessage =
await Post(client, tableName, httpContent).ConfigureAwait(false);
var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);
@@ -266,11 +280,12 @@ namespace ThingsGateway.SqlSugar
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertList">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public int BulkCopy<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public int BulkCopy<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
return BulkCopyAsync(insertList, dateFormat).GetAwaiter().GetResult();
return BulkCopyAsync(insertList, tableName, dateFormat).GetAwaiter().GetResult();
}
/// <summary>
@@ -280,7 +295,7 @@ namespace ThingsGateway.SqlSugar
/// <param name="httpPort">HTTP端口</param>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
private void BindHost(string host, string httpPort, string username, string password)
private void BindHost(string host, int httpPort, string username, string password)
{
url = host;
if (url.EndsWith('/'))

View File

@@ -2,9 +2,9 @@
{
public static class QuestDbSqlSugarClientExtensions
{
public static QuestDbRestAPI RestApi(this ISqlSugarClient db)
public static QuestDbRestAPI RestApi(this ISqlSugarClient db, int httpPort = 9000)
{
return new QuestDbRestAPI(db);
return new QuestDbRestAPI(db, httpPort);
}
}
}

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>
@@ -31,7 +32,7 @@
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="TDengine.Connector" Version="3.1.7" />
<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="System.Reflection.Emit.Lightweight" Version="4.7.0" />

View File

@@ -1,9 +1,9 @@
<Project>
<PropertyGroup>
<PluginVersion>10.10.4</PluginVersion>
<ProPluginVersion>10.10.4</ProPluginVersion>
<DefaultVersion>10.10.7</DefaultVersion>
<PluginVersion>10.10.20</PluginVersion>
<ProPluginVersion>10.10.20</ProPluginVersion>
<DefaultVersion>10.10.20</DefaultVersion>
<AuthenticationVersion>10.10.1</AuthenticationVersion>
<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
<NET8Version>8.0.19</NET8Version>

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

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

@@ -77,7 +77,8 @@ public partial class LogConsole : IDisposable
[Inject]
private ToastService ToastService { get; set; }
[Inject]
ITextFileReadService TextFileReadService { get; set; }
public void Dispose()
{
Disposed = true;
@@ -94,7 +95,7 @@ public partial class LogConsole : IDisposable
if (LogPath != null)
{
var files = TextFileReader.GetFiles(LogPath);
var files = await TextFileReadService.GetLogFilesAsync(LogPath);
if (!files.IsSuccess)
{
Messages = new List<LogMessage>();
@@ -105,7 +106,7 @@ public partial class LogConsole : IDisposable
await Task.Run(async () =>
{
Stopwatch sw = Stopwatch.StartNew();
var result = TextFileReader.LastLog(files.Content.FirstOrDefault());
var result = await TextFileReadService.LastLogDataAsync(files.Content.FirstOrDefault());
if (result.IsSuccess)
{
Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList();
@@ -143,7 +144,7 @@ public partial class LogConsole : IDisposable
{
if (LogPath != null)
{
var files = TextFileReader.GetFiles(LogPath);
var files = await TextFileReadService.GetLogFilesAsync(LogPath);
if (files.IsSuccess)
{
foreach (var item in files.Content)

View File

@@ -26,7 +26,7 @@ public class PlatformService : IPlatformService
public async Task OnLogExport(string logPath)
{
var files = TextFileReader.GetFiles(logPath);
var files = TextFileReader.GetLogFilesAsync(logPath);
if (!files.IsSuccess)
{
return;
@@ -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

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

View File

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

View File

@@ -5,6 +5,7 @@
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<NoPackageAnalysis>true</NoPackageAnalysis>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>

View File

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

View File

@@ -59,7 +59,7 @@ public static class TextFileReader
/// </summary>
/// <param name="directoryPath">目录路径</param>
/// <returns>包含文件信息的列表</returns>
public static OperResult<List<string>> GetFiles(string directoryPath)
public static OperResult<List<string>> GetLogFilesAsync(string directoryPath)
{
OperResult<List<string>> result = new(); // 初始化结果对象
// 检查目录是否存在
@@ -91,7 +91,7 @@ public static class TextFileReader
return result;
}
public static OperResult<List<LogData>> LastLog(string file, int lineCount = 200)
public static OperResult<List<LogData>> LastLogDataAsync(string file, int lineCount = 200)
{
if (!File.Exists(file))
return new OperResult<List<LogData>>("The file path is invalid");
@@ -104,7 +104,7 @@ public static class TextFileReader
{
var fileInfo = new FileInfo(file);
var length = fileInfo.Length;
var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLog)}_{file})";
var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLogDataAsync)}_{file})";
if (_cache.TryGetValue<LogDataCache>(cacheKey, out var cachedData))
{
if (cachedData != null && cachedData.Length == length)

View File

@@ -0,0 +1,24 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Foundation;
public interface ITextFileReadService
{
/// <summary>
/// 获取指定目录下所有文件信息
/// </summary>
/// <param name="directoryPath">目录路径</param>
/// <returns>包含文件信息的列表</returns>
public Task<OperResult<List<string>>> GetLogFilesAsync(string directoryPath);
public Task<OperResult<List<LogData>>> LastLogDataAsync(string file, int lineCount = 200);
}

View File

@@ -0,0 +1,20 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Foundation;
public class TextFileReadService : ITextFileReadService
{
public Task<OperResult<List<string>>> GetLogFilesAsync(string directoryPath) => Task.FromResult(TextFileReader.GetLogFilesAsync(directoryPath));
public Task<OperResult<List<LogData>>> LastLogDataAsync(string file, int lineCount = 200) => Task.FromResult(TextFileReader.LastLogDataAsync(file, lineCount));
}

View File

@@ -6,6 +6,7 @@
<PropertyGroup>
<Description>工业设备通讯协议-基础类库</Description>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

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

View File

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

View File

@@ -12,13 +12,15 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
public class AsyncReadWriteLock
public class AsyncReadWriteLock : IAsyncDisposable
{
private readonly int _writeReadRatio = 3; // 写3次会允许1次读但写入也不会被阻止具体协议取决于插件协议实现
public AsyncReadWriteLock(int writeReadRatio)
public AsyncReadWriteLock(int writeReadRatio, bool writePriority)
{
_writeReadRatio = writeReadRatio;
_writePriority = writePriority;
}
private bool _writePriority;
private AsyncAutoResetEvent _readerLock = new AsyncAutoResetEvent(false); // 控制读计数
private long _writerCount = 0; // 当前活跃的写线程数
private long _readerCount = 0; // 当前被阻塞的读线程数
@@ -33,6 +35,8 @@ public class AsyncReadWriteLock
{
Interlocked.Increment(ref _readerCount);
// 第一个读者需要获取写入锁,防止写操作
await _readerLock.WaitOneAsync(cancellationToken).ConfigureAwait(false);
@@ -52,10 +56,13 @@ public class AsyncReadWriteLock
if (Interlocked.Increment(ref _writerCount) == 1)
{
var cancellationTokenSource = _cancellationTokenSource;
_cancellationTokenSource = new();
await cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false); // 取消读取
cancellationTokenSource.SafeDispose();
if (_writePriority)
{
var cancellationTokenSource = _cancellationTokenSource;
_cancellationTokenSource = new();
await cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false); // 取消读取
cancellationTokenSource.SafeDispose();
}
}
return new Writer(this);
@@ -63,11 +70,16 @@ public class AsyncReadWriteLock
private object lockObject = new();
private void ReleaseWriter()
{
var writerCount = Interlocked.Decrement(ref _writerCount);
// 每次释放写时,总是唤醒至少一个读
_readerLock.Set();
if (writerCount == 0)
{
var resetEvent = _readerLock;
_readerLock = new(false);
//_readerLock = new(false);
Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
resetEvent.SetAll();
}
@@ -83,18 +95,28 @@ public class AsyncReadWriteLock
if (count >= _writeReadRatio)
{
Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
_readerLock.Set();
//_readerLock.Set();
}
}
else
{
_readerLock.Set();
//_readerLock.Set();
}
}
}
}
public async ValueTask DisposeAsync()
{
if (_cancellationTokenSource != null)
{
await _cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false);
_cancellationTokenSource.SafeDispose();
}
_readerLock.SetAll();
}
private int _writeSinceLastReadCount = 0;
private struct Writer : IDisposable
{

View File

@@ -18,7 +18,7 @@ namespace BootstrapBlazor.Components;
/// <param name="fieldName">字段名称</param>
/// <param name="fieldType">字段类型</param>
/// <param name="fieldText">显示文字</param>
internal sealed class InternalTableColumn(string fieldName, Type fieldType, string? fieldText = null) : IEditorItem, ITableColumn
public sealed class DefaultTableColumn(string fieldName, Type fieldType, string? fieldText = null) : IEditorItem, ITableColumn
{
public IEnumerable<KeyValuePair<string, object>>? ComponentParameters { get; set; }
public Type? ComponentType { get; set; }

View File

@@ -12,7 +12,7 @@ using BootstrapBlazor.Components;
namespace ThingsGateway.Gateway.Application;
public class ExportFilter
public class GatewayExportFilter
{
public FilterKeyValueAction FilterKeyValueAction { get; set; }
public string? PluginName { get; set; }

View File

@@ -0,0 +1,57 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
public class LinkedCancellationTokenSourceCache : IDisposable
{
private CancellationTokenSource? _cachedCts;
private CancellationToken _token1;
private CancellationToken _token2;
private readonly object _lock = new();
~LinkedCancellationTokenSourceCache()
{
Dispose();
}
/// <summary>
/// 获取一个 CancellationTokenSource它是由两个 token 链接而成的。
/// 会尝试复用之前缓存的 CTS前提是两个 token 仍然相同且未取消。
/// </summary>
public CancellationTokenSource GetLinkedTokenSource(CancellationToken token1, CancellationToken token2)
{
lock (_lock)
{
// 如果缓存的 CTS 已经取消或 Dispose或者 token 不同,重新创建
if (_cachedCts?.IsCancellationRequested != false ||
!_token1.Equals(token1) || !_token2.Equals(token2))
{
_cachedCts?.Dispose();
_cachedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
_token1 = token1;
_token2 = token2;
}
return _cachedCts;
}
}
public void Dispose()
{
lock (_lock)
{
_cachedCts?.Dispose();
_cachedCts = null!;
}
}
}

View File

@@ -10,6 +10,7 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
private int _interval10MS = 10;
private string _interval;
private readonly Func<object?, CancellationToken, Task> _taskFunc;
private readonly Func<object?, CancellationToken, ValueTask> _valueTaskFunc;
private readonly Action<object?, CancellationToken> _taskAction;
private readonly CancellationToken _token;
private TimerX? _timer;
@@ -28,6 +29,17 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
_taskFunc = taskFunc;
_token = token;
}
public CronScheduledTask(string interval, Func<object?, CancellationToken, ValueTask> taskFunc, object? state, ILog log, CancellationToken token)
{
_interval = interval;
LogMessage = log;
_state = state;
_valueTaskFunc = taskFunc;
_token = token;
}
public CronScheduledTask(string interval, Action<object?, CancellationToken> taskAction, object? state, ILog log, CancellationToken token)
{
_interval = interval;
@@ -51,14 +63,14 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
if (Check()) return;
if (_taskAction != null)
_timer = new TimerX(TimerCallback, _state, _interval, nameof(IScheduledTask)) { Async = true };
else if (_taskFunc != null)
else if (_taskFunc != null || _valueTaskFunc != null)
_timer = new TimerX(TimerCallbackAsync, _state, _interval, nameof(IScheduledTask)) { Async = true };
}
private async Task TimerCallbackAsync(object? state)
private async ValueTask TimerCallbackAsync(object? state)
{
if (Check()) return;
if (_taskFunc == null)
if (_taskFunc == null && _valueTaskFunc == null)
{
Dispose();
return;
@@ -74,7 +86,10 @@ public class CronScheduledTask : DisposeBase, IScheduledTask
try
{
await _taskFunc(state, _token).ConfigureAwait(false);
if (_taskFunc != null)
await _taskFunc(state, _token).ConfigureAwait(false);
else if (_valueTaskFunc != null)
await _valueTaskFunc(state, _token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{

View File

@@ -10,6 +10,7 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
private int _interval10MS = 10;
public int IntervalMS { get; }
private readonly Func<object?, CancellationToken, Task> _taskFunc;
private readonly Func<object?, CancellationToken, ValueTask> _valueTaskFunc;
private readonly CancellationToken _token;
private TimerX? _timer;
private object? _state;
@@ -26,6 +27,14 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
_taskFunc = taskFunc;
_token = token;
}
public ScheduledAsyncTask(int interval, Func<object?, CancellationToken, ValueTask> taskFunc, object? state, ILog log, CancellationToken token)
{
IntervalMS = interval;
LogMessage = log;
_state = state;
_valueTaskFunc = taskFunc;
_token = token;
}
private bool Check()
{
if (_token.IsCancellationRequested)
@@ -42,11 +51,17 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
_timer = new TimerX(DoAsync, _state, IntervalMS, IntervalMS, nameof(IScheduledTask)) { Async = true };
}
private async Task DoAsync(object? state)
private async ValueTask DoAsync(object? state)
{
if (Check())
return;
if (_taskFunc == null && _valueTaskFunc == null)
{
Dispose();
return;
}
Interlocked.Increment(ref _pendingTriggers);
if (Interlocked.Exchange(ref _isRunning, 1) == 1)
@@ -55,9 +70,13 @@ public class ScheduledAsyncTask : DisposeBase, IScheduledTask, IScheduledIntInte
// 减少一个触发次数
Interlocked.Decrement(ref _pendingTriggers);
try
{
await _taskFunc(state, _token).ConfigureAwait(false);
if (_taskFunc != null)
await _taskFunc(state, _token).ConfigureAwait(false);
else if (_valueTaskFunc != null)
await _valueTaskFunc(state, _token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{

View File

@@ -14,6 +14,18 @@ public static class ScheduledTaskHelper
return new CronScheduledTask(interval, func, state, log, cancellationToken);
}
}
public static IScheduledTask GetTask(string interval, Func<object?, CancellationToken, ValueTask> func, object? state, TouchSocket.Core.ILog log, CancellationToken cancellationToken)
{
if (int.TryParse(interval, out int intervalV))
{
var intervalMilliseconds = intervalV < 10 ? 10 : intervalV;
return new ScheduledAsyncTask(intervalMilliseconds, func, state, log, cancellationToken);
}
else
{
return new CronScheduledTask(interval, func, state, log, cancellationToken);
}
}
public static IScheduledTask GetTask(string interval, Action<object?, CancellationToken> action, object? state, TouchSocket.Core.ILog log, CancellationToken cancellationToken)
{
if (int.TryParse(interval, out int intervalV))

View File

@@ -10,7 +10,7 @@
namespace ThingsGateway.Gateway.Application;
public static class ExportString
public static class GatewayExportString
{
/// <summary>
/// 通道名称
@@ -35,13 +35,19 @@ public static class ExportString
/// </summary>
public static string VariableName => Localizer["VariableName"];
/// <summary>
/// 变量报警表名称
/// </summary>
public static string AlarmName => Localizer["AlarmName"];
public static IStringLocalizer localizer;
public static IStringLocalizer Localizer
{
get
{
if (localizer == null)
localizer = App.CreateLocalizerByType(typeof(ExportString));
localizer = App.CreateLocalizerByType(typeof(GatewayExportString));
return localizer;
}
}

View File

@@ -1,43 +0,0 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute;
namespace ThingsGateway.Management;
[ApiDescriptionSettings("ThingsGateway.OpenApi", Order = 200)]
[Route("openApi/autoUpdate")]
[RolePermission]
[RequestAudit]
[ApiController]
[Authorize(AuthenticationSchemes = "Bearer")]
public class AutoUpdateController : ControllerBase
{
private IUpdateZipFileHostedService _updateZipFileService;
public AutoUpdateController(IUpdateZipFileHostedService updateZipFileService)
{
_updateZipFileService = updateZipFileService;
}
/// <summary>
/// 检查更新
/// </summary>
/// <returns></returns>
[HttpPost("update")]
public async Task Update()
{
var data = await _updateZipFileService.GetList().ConfigureAwait(false);
if (data.Count != 0)
await _updateZipFileService.Update(data.OrderByDescending(a => a.Version).FirstOrDefault()).ConfigureAwait(false);
}
}

View File

@@ -15,14 +15,10 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO.Ports;
using ThingsGateway.FriendlyException;
using TouchSocket.Core;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
namespace ThingsGateway.Gateway.Application;
@@ -156,9 +152,9 @@ public class ControlController : ControllerBase, IRpcServer
[HttpPost("batchSaveChannel")]
[DisplayName("保存通道")]
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
public Task<bool> BatchSaveChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<ChannelInput> channels, ItemChangedType type, bool restart = true)
public Task<bool> BatchSaveChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<Channel> channels, ItemChangedType type, bool restart = true)
{
return GlobalData.ChannelRuntimeService.BatchSaveChannelAsync(channels.AdaptListChannel(), type, restart);
return GlobalData.ChannelRuntimeService.BatchSaveChannelAsync(channels, type, restart);
}
/// <summary>
@@ -167,9 +163,9 @@ public class ControlController : ControllerBase, IRpcServer
[HttpPost("batchSaveDevice")]
[DisplayName("保存设备")]
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
public Task<bool> BatchSaveDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<DeviceInput> devices, ItemChangedType type, bool restart = true)
public Task<bool> BatchSaveDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<Device> devices, ItemChangedType type, bool restart = true)
{
return GlobalData.DeviceRuntimeService.BatchSaveDeviceAsync(devices.AdaptListDevice(), type, restart);
return GlobalData.DeviceRuntimeService.BatchSaveDeviceAsync(devices, type, restart);
}
/// <summary>
@@ -178,9 +174,9 @@ public class ControlController : ControllerBase, IRpcServer
[HttpPost("batchSaveVariable")]
[DisplayName("保存变量")]
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
public Task<bool> BatchSaveVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<VariableInput> variables, ItemChangedType type, bool restart = true)
public Task<bool> BatchSaveVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<Variable> variables, ItemChangedType type, bool restart = true)
{
return GlobalData.VariableRuntimeService.BatchSaveVariableAsync(variables.AdaptListVariable(), type, restart, default);
return GlobalData.VariableRuntimeService.BatchSaveVariableAsync(variables, type, restart);
}
/// <summary>
@@ -192,7 +188,7 @@ public class ControlController : ControllerBase, IRpcServer
public Task<bool> DeleteChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
{
if (ids == null || ids.Count == 0) ids = GlobalData.IdChannels.Keys.ToList();
return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart, default);
return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart);
}
/// <summary>
@@ -204,7 +200,7 @@ public class ControlController : ControllerBase, IRpcServer
public Task<bool> DeleteDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
{
if (ids == null || ids.Count == 0) ids = GlobalData.IdDevices.Keys.ToList();
return GlobalData.DeviceRuntimeService.DeleteDeviceAsync(ids, restart, default);
return GlobalData.DeviceRuntimeService.DeleteDeviceAsync(ids, restart);
}
/// <summary>
@@ -216,7 +212,7 @@ public class ControlController : ControllerBase, IRpcServer
public Task<bool> DeleteVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
{
if (ids == null || ids.Count == 0) ids = GlobalData.IdVariables.Keys.ToList();
return GlobalData.VariableRuntimeService.DeleteVariableAsync(ids, restart, default);
return GlobalData.VariableRuntimeService.DeleteVariableAsync(ids, restart);
}
/// <summary>
@@ -227,7 +223,7 @@ public class ControlController : ControllerBase, IRpcServer
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
public Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart = true)
{
return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart, default);
return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart);
}
/// <summary>
@@ -247,552 +243,3 @@ public class ControlController : ControllerBase, IRpcServer
}
}
}
public class ChannelInput
{
/// <summary>
/// 主键Id
/// </summary>
public virtual long Id { get; set; }
/// <summary>
/// 通道名称
/// </summary>
[Required]
public virtual string Name { get; set; }
/// <inheritdoc/>
public virtual ChannelTypeEnum ChannelType { get; set; }
/// <summary>
/// 插件名称
/// </summary>
[Required]
public virtual string PluginName { get; set; }
/// <summary>
/// 使能
/// </summary>
public virtual bool Enable { get; set; } = true;
/// <summary>
/// LogLevel
/// </summary>
public LogLevel LogLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 远程地址,可由<see cref="IPHost"/> 与 <see cref="string"/> 相互转化
/// </summary>
[UriValidation]
public virtual string RemoteUrl { get; set; } = "127.0.0.1:502";
/// <summary>
/// 本地地址,可由<see cref="IPHost.IPHost(string)"/>与<see href="IPHost.ToString()"/>相互转化
/// </summary>
[UriValidation]
public virtual string BindUrl { get; set; }
/// <summary>
/// COM
/// </summary>
public virtual string PortName { get; set; } = "COM1";
/// <summary>
/// 波特率
/// </summary>
public virtual int BaudRate { get; set; } = 9600;
/// <summary>
/// 数据位
/// </summary>
public virtual int DataBits { get; set; } = 8;
/// <summary>
/// 校验位
/// </summary>
public virtual Parity Parity { get; set; }
/// <summary>
/// 停止位
/// </summary>
public virtual StopBits StopBits { get; set; } = StopBits.One;
/// <summary>
/// DtrEnable
/// </summary>
public virtual bool DtrEnable { get; set; }
/// <summary>
/// RtsEnable
/// </summary>
public virtual bool RtsEnable { get; set; }
/// <summary>
/// StreamAsync
/// </summary>
public virtual bool StreamAsync { get; set; } = false;
/// <summary>
/// 缓存超时
/// </summary>
[MinValue(100)]
public virtual int CacheTimeout { get; set; } = 500;
/// <summary>
/// 连接超时
/// </summary>
[MinValue(100)]
public virtual ushort ConnectTimeout { get; set; } = 3000;
/// <summary>
/// 最大并发数
/// </summary>
[MinValue(1)]
public virtual int MaxConcurrentCount { get; set; } = 1;
public virtual int MaxClientCount { get; set; } = 10000;
public virtual int CheckClearTime { get; set; } = 120000;
public virtual string Heartbeat { get; set; } = "Heartbeat";
#region dtu终端
public virtual int HeartbeatTime { get; set; } = 60000;
public virtual string DtuId { get; set; }
#endregion
public virtual DtuSeviceType DtuSeviceType { get; set; }
}
public class DeviceInput : IValidatableObject
{
public long Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[Required]
[RegularExpression(@"^[^.]*$", ErrorMessage = "The field {0} cannot contain a dot ('.')")]
public virtual string Name { get; set; }
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 通道
/// </summary>
[MinValue(1)]
[Required]
public virtual long ChannelId { get; set; }
/// <summary>
/// 默认执行间隔支持corn表达式
/// </summary>
public virtual string IntervalTime { get; set; } = "1000";
/// <summary>
/// 设备使能
/// </summary>
public virtual bool Enable { get; set; } = true;
/// <summary>
/// LogLevel
/// </summary>
public virtual TouchSocket.Core.LogLevel LogLevel { get; set; } = TouchSocket.Core.LogLevel.Info;
/// <summary>
/// 设备属性Json
/// </summary>
public Dictionary<string, string>? DevicePropertys { get; set; } = new();
#region
/// <summary>
/// 启用冗余
/// </summary>
public bool RedundantEnable { get; set; }
/// <summary>
/// 冗余设备Id,只能选择相同驱动
/// </summary>
public long? RedundantDeviceId { get; set; }
/// <summary>
/// 冗余模式
/// </summary>
public virtual RedundantSwitchTypeEnum RedundantSwitchType { get; set; }
/// <summary>
/// 冗余扫描间隔
/// </summary>
[MinValue(30000)]
public virtual int RedundantScanIntervalTime { get; set; } = 30000;
/// <summary>
/// 冗余切换判断脚本返回true则切换冗余设备
/// </summary>
public virtual string RedundantScript { get; set; }
#endregion
#region
/// <summary>
/// 自定义
/// </summary>
public string? Remark1 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark2 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark3 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark4 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark5 { get; set; }
#endregion
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (RedundantEnable && RedundantDeviceId == null)
{
yield return new ValidationResult("When enable redundancy, you must select a redundant device.", new[] { nameof(RedundantEnable), nameof(RedundantDeviceId) });
}
}
}
public class VariableInput : IValidatableObject
{
public virtual long Id { get; set; }
/// <summary>
/// 设备
/// </summary>
[Required]
public virtual long DeviceId { get; set; }
/// <summary>
/// 变量名称
/// </summary>
[Required]
public virtual string Name { get; set; }
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 单位
/// </summary>
public virtual string? Unit { get; set; }
/// <summary>
/// 间隔时间
/// </summary>
public virtual string? IntervalTime { get; set; }
/// <summary>
/// 变量地址,可能带有额外的信息,比如<see cref="DataFormatEnum"/> ,以;分割
/// </summary>
public string? RegisterAddress { get; set; }
/// <summary>
/// 数组长度
/// </summary>
public int? ArrayLength { get; set; }
/// <summary>
/// 其他方法若不为空此时RegisterAddress为方法参数
/// </summary>
public string? OtherMethod { get; set; }
/// <summary>
/// 使能
/// </summary>
public virtual bool Enable { get; set; } = true;
/// <summary>
/// 读写权限
/// </summary>
public virtual ProtectTypeEnum ProtectType { get; set; } = ProtectTypeEnum.ReadWrite;
/// <summary>
/// 数据类型
/// </summary>
public virtual DataTypeEnum DataType { get; set; } = DataTypeEnum.Int16;
/// <summary>
/// 读取表达式
/// </summary>
public virtual string? ReadExpressions { get; set; }
/// <summary>
/// 写入表达式
/// </summary>
public virtual string? WriteExpressions { get; set; }
/// <summary>
/// 是否允许远程Rpc写入
/// </summary>
public virtual bool RpcWriteEnable { get; set; } = true;
/// <summary>
/// 初始值
/// </summary>
public object? InitValue
{
get
{
return _value;
}
set
{
if (value != null)
_value = value?.ToString()?.GetJTokenFromString();
else
_value = null;
}
}
private object? _value;
/// <summary>
/// 保存初始值
/// </summary>
public virtual bool SaveValue { get; set; } = false;
/// <summary>
/// 变量额外属性Json
/// </summary>
public Dictionary<long, Dictionary<string, string>>? VariablePropertys { get; set; }
#region
/// <summary>
/// 报警延时
/// </summary>
public int AlarmDelay { get; set; }
/// <summary>
/// 布尔开报警使能
/// </summary>
public bool BoolOpenAlarmEnable { get; set; }
/// <summary>
/// 布尔开报警约束
/// </summary>
public string? BoolOpenRestrainExpressions { get; set; }
/// <summary>
/// 布尔开报警文本
/// </summary>
public string? BoolOpenAlarmText { get; set; }
/// <summary>
/// 布尔关报警使能
/// </summary>
public bool BoolCloseAlarmEnable { get; set; }
/// <summary>
/// 布尔关报警约束
/// </summary>
public string? BoolCloseRestrainExpressions { get; set; }
/// <summary>
/// 布尔关报警文本
/// </summary>
public string? BoolCloseAlarmText { get; set; }
/// <summary>
/// 高报使能
/// </summary>
public bool HAlarmEnable { get; set; }
/// <summary>
/// 高报约束
/// </summary>
public string? HRestrainExpressions { get; set; }
/// <summary>
/// 高报文本
/// </summary>
public string? HAlarmText { get; set; }
/// <summary>
/// 高限值
/// </summary>
public double? HAlarmCode { get; set; }
/// <summary>
/// 高高报使能
/// </summary>
public bool HHAlarmEnable { get; set; }
/// <summary>
/// 高高报约束
/// </summary>
public string? HHRestrainExpressions { get; set; }
/// <summary>
/// 高高报文本
/// </summary>
public string? HHAlarmText { get; set; }
/// <summary>
/// 高高限值
/// </summary>
public double? HHAlarmCode { get; set; }
/// <summary>
/// 低报使能
/// </summary>
public bool LAlarmEnable { get; set; }
/// <summary>
/// 低报约束
/// </summary>
public string? LRestrainExpressions { get; set; }
/// <summary>
/// 低报文本
/// </summary>
public string? LAlarmText { get; set; }
/// <summary>
/// 低限值
/// </summary>
public double? LAlarmCode { get; set; }
/// <summary>
/// 低低报使能
/// </summary>
public bool LLAlarmEnable { get; set; }
/// <summary>
/// 低低报约束
/// </summary>
public string? LLRestrainExpressions { get; set; }
/// <summary>
/// 低低报文本
/// </summary>
public string? LLAlarmText { get; set; }
/// <summary>
/// 低低限值
/// </summary>
public double? LLAlarmCode { get; set; }
/// <summary>
/// 自定义报警使能
/// </summary>
public bool CustomAlarmEnable { get; set; }
/// <summary>
/// 自定义报警条件约束
/// </summary>
public string? CustomRestrainExpressions { get; set; }
/// <summary>
/// 自定义文本
/// </summary>
public string? CustomAlarmText { get; set; }
/// <summary>
/// 自定义报警条件
/// </summary>
public string? CustomAlarmCode { get; set; }
#endregion
#region
/// <summary>
/// 自定义
/// </summary>
public string? Remark1 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark2 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark3 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark4 { get; set; }
/// <summary>
/// 自定义
/// </summary>
public string? Remark5 { get; set; }
#endregion
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(RegisterAddress) && string.IsNullOrEmpty(OtherMethod))
{
yield return new ValidationResult("Both RegisterAddress and OtherMethod cannot be empty or null.", new[] { nameof(RegisterAddress), nameof(OtherMethod) });
}
if (HHAlarmEnable && HHAlarmCode == null)
{
yield return new ValidationResult("HHAlarmCode cannot be null when HHAlarmEnable is true", new[] { nameof(HHAlarmCode) });
}
if (HAlarmEnable && HAlarmCode == null)
{
yield return new ValidationResult("HAlarmCode cannot be null when HAlarmEnable is true", new[] { nameof(HAlarmCode) });
}
if (LAlarmEnable && LAlarmCode == null)
{
yield return new ValidationResult("LAlarmCode cannot be null when LAlarmEnable is true", new[] { nameof(LAlarmCode) });
}
if (LLAlarmEnable && LLAlarmCode == null)
{
yield return new ValidationResult("LLAlarmCode cannot be null when LLAlarmEnable is true", new[] { nameof(LLAlarmCode) });
}
if (HHAlarmEnable && HAlarmEnable && HHAlarmCode <= HAlarmCode)
{
yield return new ValidationResult("HHAlarmCode must be greater than HAlarmCode", new[] { nameof(HHAlarmCode), nameof(HAlarmCode) });
}
if (HAlarmEnable && LAlarmEnable && HAlarmCode <= LAlarmCode)
{
yield return new ValidationResult("HAlarmCode must be greater than LAlarmCode", new[] { nameof(HAlarmCode), nameof(LAlarmCode) });
}
if (LAlarmEnable && LLAlarmEnable && LAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("LAlarmCode must be greater than LLAlarmCode", new[] { nameof(LAlarmCode), nameof(LLAlarmCode) });
}
if (HHAlarmEnable && LAlarmEnable && HHAlarmCode <= LAlarmCode)
{
yield return new ValidationResult("HHAlarmCode should be greater than or less than LAlarmCode", new[] { nameof(HHAlarmCode), nameof(LAlarmCode) });
}
if (HHAlarmEnable && LLAlarmEnable && HHAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("HHAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HHAlarmCode), nameof(LLAlarmCode) });
}
if (HAlarmEnable && LLAlarmEnable && HAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("HAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HAlarmCode), nameof(LLAlarmCode) });
}
}
}

View File

@@ -45,7 +45,7 @@ public class GatewayExportController : ControllerBase
/// </summary>
/// <returns></returns>
[HttpPost("device")]
public async Task<IActionResult> DownloadDeviceAsync([FromBody] ExportFilter input)
public async Task<IActionResult> DownloadDeviceAsync([FromBody] GatewayExportFilter input)
{
input.QueryPageOptions.IsPage = false;
input.QueryPageOptions.IsVirtualScroll = false;
@@ -58,7 +58,7 @@ public class GatewayExportController : ControllerBase
/// </summary>
/// <returns></returns>
[HttpPost("channel")]
public async Task<IActionResult> DownloadChannelAsync([FromBody] ExportFilter input)
public async Task<IActionResult> DownloadChannelAsync([FromBody] GatewayExportFilter input)
{
input.QueryPageOptions.IsPage = false;
input.QueryPageOptions.IsVirtualScroll = false;
@@ -72,7 +72,7 @@ public class GatewayExportController : ControllerBase
/// </summary>
/// <returns></returns>
[HttpPost("variable")]
public async Task<IActionResult> DownloadVariableAsync([FromBody] ExportFilter input)
public async Task<IActionResult> DownloadVariableAsync([FromBody] GatewayExportFilter input)
{
input.QueryPageOptions.IsPage = false;
input.QueryPageOptions.IsVirtualScroll = false;

View File

@@ -90,7 +90,7 @@ public class RuntimeInfoController : ControllerBase, IRpcServer
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
public async Task<SqlSugarPagedList<AlarmVariable>> GetRealAlarmList([FromQuery][TouchSocket.WebApi.FromBody] AlarmVariablePageInput input)
{
var realAlarmVariables = await GlobalData.GetCurrentUserRealAlarmVariables().ConfigureAwait(false);
var realAlarmVariables = await GlobalData.GetCurrentUserRealAlarmVariablesAsync().ConfigureAwait(false);
var data = realAlarmVariables
.WhereIF(!input.RegisterAddress.IsNullOrEmpty(), a => a.RegisterAddress == input.RegisterAddress)
@@ -145,7 +145,7 @@ public class RuntimeInfoController : ControllerBase, IRpcServer
public SqlSugarPagedList<PluginInfo> GetPluginInfos([FromQuery][TouchSocket.WebApi.FromBody] PluginInfoPageInput input)
{
//指定关键词搜索为插件FullName
return GlobalData.PluginService.GetList().WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
return (GlobalData.PluginService.GetPluginList()).WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
.ToPagedList(input);
}
}

View File

@@ -11,14 +11,19 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ThingsGateway.Admin.Application;
using TouchSocket.Rpc;
namespace ThingsGateway.Gateway.Application;
[Route("api/[controller]/[action]")]
[AllowAnonymous]
[ApiController]
public class TestController : ControllerBase
[TouchSocket.WebApi.Router("/miniapi/[api]/[action]")]
[TouchSocket.WebApi.EnableCors("cors")]
public class TestController : ControllerBase, IRpcServer
{
[HttpGet]
[TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
public void Test()
{
GC.Collect();

View File

@@ -57,6 +57,9 @@ public abstract class BusinessBase : DriverBase
/// </summary>
protected Dictionary<string, List<VariableRuntime>> VariableRuntimeGroups { get; set; } = new();
#if !Management
public override Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
{
LogMessage?.LogInformation("Refresh variable");
@@ -117,6 +120,8 @@ public abstract class BusinessBase : DriverBase
};
}
/// <summary>
/// 间隔执行
/// </summary>
@@ -134,4 +139,5 @@ public abstract class BusinessBase : DriverBase
CurrentDevice?.SetDeviceStatus(TimerX.Now, true);
}
}
#endif
}

View File

@@ -20,6 +20,12 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public abstract class BusinessBaseWithCache : BusinessBase
{
protected sealed override BusinessPropertyBase _businessPropertyBase => _businessPropertyWithCache;
protected abstract BusinessPropertyWithCache _businessPropertyWithCache { get; }
#if !Management
#region
protected abstract bool AlarmModelEnable { get; }
@@ -537,9 +543,7 @@ public abstract class BusinessBaseWithCache : BusinessBase
private CacheDB DBCacheVar;
private CacheDB DBCacheVars;
protected sealed override BusinessPropertyBase _businessPropertyBase => _businessPropertyWithCache;
protected abstract BusinessPropertyWithCache _businessPropertyWithCache { get; }
protected object cacheLock = new();
@@ -976,4 +980,6 @@ public abstract class BusinessBaseWithCache : BusinessBase
}
#endregion
#endif
}

View File

@@ -19,6 +19,7 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
{
#if !Management
protected override bool AlarmModelEnable => true;
protected override bool DevModelEnable => false;
@@ -104,4 +105,6 @@ public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
}
}
}
#endif
}

View File

@@ -29,6 +29,9 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
/// </summary>
protected abstract BusinessPropertyWithCacheInterval _businessPropertyWithCacheInterval { get; }
#if !Management
protected internal override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
{
if (AlarmModelEnable)
@@ -340,5 +343,5 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
}
}
}
#endif
}

View File

@@ -26,6 +26,8 @@ public abstract partial class BusinessBaseWithCacheIntervalScript : BusinessBase
protected abstract BusinessPropertyWithCacheIntervalScript _businessPropertyWithCacheIntervalScript { get; }
#if !Management
public virtual string[] Match(string input)
{
// 生成缓存键,以确保缓存的唯一性
@@ -347,4 +349,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript : BusinessBase
[GeneratedRegex(@"\$\{(.+?)\}")]
private static partial Regex TopicRegex();
#endregion
#endif
}

View File

@@ -15,9 +15,12 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public abstract partial class BusinessBaseWithCacheIntervalScriptAll : BusinessBaseWithCacheIntervalScript
{
#if !Management
protected override bool AlarmModelEnable => true;
protected override bool DevModelEnable => true;
protected override bool VarModelEnable => true;
#endif
}

View File

@@ -15,6 +15,7 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public abstract class BusinessBaseWithCacheIntervalVariable : BusinessBaseWithCacheInterval
{
#if !Management
protected override bool AlarmModelEnable => false;
protected override bool DevModelEnable => false;
@@ -29,5 +30,5 @@ public abstract class BusinessBaseWithCacheIntervalVariable : BusinessBaseWithCa
{
throw new NotImplementedException();
}
#endif
}

View File

@@ -18,6 +18,7 @@ namespace ThingsGateway.Gateway.Application;
/// </summary>
public static class BusinessDatabaseUtil
{
/// <summary>
/// 获取数据库链接
/// </summary>
@@ -45,6 +46,7 @@ public static class BusinessDatabaseUtil
return sqlSugarClient;
}
#if !Management
/// <summary>
/// 按条件获取DB插件中的全部历史报警(不分页)
/// </summary>
@@ -144,4 +146,6 @@ public static class BusinessDatabaseUtil
return new("GetDBHistoryValues Fail", ex);
}
}
#endif
}

View File

@@ -21,6 +21,8 @@ public interface IDBHistoryAlarm
string? Description { get; set; }
string DeviceName { get; set; }
DateTime EventTime { get; set; }
DateTime FinishTime { get; set; }
DateTime ConfirmTime { get; set; }
EventTypeEnum EventType { get; set; }
string Name { get; set; }
string RegisterAddress { get; set; }

View File

@@ -16,7 +16,9 @@ using System.Collections.Concurrent;
using ThingsGateway.Common.Extension;
using ThingsGateway.Extension.Generic;
#if !Management
using ThingsGateway.Gateway.Application.Extensions;
#endif
using ThingsGateway.NewLife.Json.Extension;
using ThingsGateway.NewLife.Threading;
@@ -29,20 +31,31 @@ namespace ThingsGateway.Gateway.Application;
/// 采集插件继承实现不同PLC通讯
/// <para></para>
/// </summary>
public abstract class CollectBase : DriverBase, IRpcDriver
public abstract partial class CollectBase : DriverBase
#if !Management
, IRpcDriver
#endif
{
/// <summary>
/// 插件配置项
/// </summary>
public abstract CollectPropertyBase CollectProperties { get; }
public sealed override object DriverProperties => CollectProperties;
public virtual string GetAddressDescription()
{
return string.Empty;
}
#if !Management
/// <summary>
/// 特殊方法
/// </summary>
public List<DriverMethodInfo>? DriverMethodInfos { get; private set; }
public sealed override object DriverProperties => CollectProperties;
public override async Task AfterVariablesChangedAsync(CancellationToken cancellationToken)
{
LogMessage?.LogInformation("Refresh variable");
@@ -195,13 +208,10 @@ public abstract class CollectBase : DriverBase, IRpcDriver
// 从插件服务中获取当前设备关联的驱动方法信息列表
DriverMethodInfos = GlobalData.PluginService.GetDriverMethodInfos(device.PluginName, this);
ReadWriteLock = new(CollectProperties.DutyCycle);
ReadWriteLock = new(CollectProperties.DutyCycle, CollectProperties.WritePriority);
}
public virtual string GetAddressDescription()
{
return string.Empty;
}
protected virtual bool VariableSourceReadsEnable => true;
protected List<IScheduledTask> VariableTasks = new List<IScheduledTask>();
@@ -278,11 +288,9 @@ public abstract class CollectBase : DriverBase, IRpcDriver
}
}
#region private
#region
async Task ReadVariableMed(object? state, CancellationToken cancellationToken)
async ValueTask ReadVariableMed(object? state, CancellationToken cancellationToken)
{
if (state is not VariableMethod readVariableMethods) return;
if (Pause)
@@ -348,25 +356,25 @@ public abstract class CollectBase : DriverBase, IRpcDriver
}
#endregion
private readonly LinkedCancellationTokenSourceCache _linkedCtsCache = new();
#region
async Task ReadVariableSource(object? state, CancellationToken cancellationToken)
async ValueTask ReadVariableSource(object? state, CancellationToken cancellationToken)
{
var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
if (state is not VariableSourceRead variableSourceRead) return;
if (Pause) return;
if (cancellationToken.IsCancellationRequested) return;
var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
if (readToken.IsCancellationRequested)
{
await ReadVariableSource(state, cancellationToken).ConfigureAwait(false);
return;
}
using var allTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, readToken);
var allTokenSource = _linkedCtsCache.GetLinkedTokenSource(cancellationToken, readToken);
var allToken = allTokenSource.Token;
//if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Trace)
@@ -434,13 +442,15 @@ public abstract class CollectBase : DriverBase, IRpcDriver
variableSourceRead.LastErrorMessage = readResult.ErrorMessage;
CurrentDevice.SetDeviceStatus(TimerX.Now, null, readResult.ErrorMessage);
var time = DateTime.Now;
variableSourceRead.VariableRuntimes.ForEach(a => a.SetValue(null, time, isOnline: false));
foreach (var item in variableSourceRead.VariableRuntimes)
{
item.SetValue(null, time, isOnline: false);
}
}
}
#endregion
#endregion
protected virtual Task TestOnline(object? state, CancellationToken cancellationToken)
{
@@ -468,6 +478,7 @@ public abstract class CollectBase : DriverBase, IRpcDriver
}
}
/// <summary>
/// 连读打包,返回实际通讯包信息<see cref="VariableSourceRead"/>
/// <br></br>每个驱动打包方法不一样,所以需要实现这个接口
@@ -722,4 +733,14 @@ public abstract class CollectBase : DriverBase, IRpcDriver
#endregion
protected override async Task DisposeAsync(bool disposing)
{
await base.DisposeAsync(disposing).ConfigureAwait(false);
_linkedCtsCache?.SafeDispose();
if (ReadWriteLock != null)
await ReadWriteLock.SafeDisposeAsync().ConfigureAwait(false);
}
#endif
}

View File

@@ -33,13 +33,7 @@ public abstract class CollectFoundationBase : CollectBase
/// </summary>
public virtual IDevice? FoundationDevice { get; }
/// <summary>
/// 是否连接成功
/// </summary>
public override bool IsConnected()
{
return FoundationDevice?.OnLine == true;
}
public override string ToString()
{
@@ -53,6 +47,24 @@ public abstract class CollectFoundationBase : CollectBase
await FoundationDevice.SafeDisposeAsync().ConfigureAwait(false);
await base.DisposeAsync(disposing).ConfigureAwait(false);
}
public override string GetAddressDescription()
{
return FoundationDevice?.GetAddressDescription();
}
#if !Management
/// <summary>
/// 是否连接成功
/// </summary>
public override bool IsConnected()
{
return FoundationDevice?.OnLine == true;
}
/// <summary>
/// 开始通讯执行的方法
/// </summary>
@@ -66,10 +78,6 @@ public abstract class CollectFoundationBase : CollectBase
}
}
public override string GetAddressDescription()
{
return FoundationDevice?.GetAddressDescription();
}
protected override async Task TestOnline(object? state, CancellationToken cancellationToken)
{
@@ -216,4 +224,7 @@ public abstract class CollectFoundationBase : CollectBase
// 返回包含操作结果的字典
return new Dictionary<string, OperResult>(operResults);
}
#endif
}

View File

@@ -37,6 +37,12 @@ public abstract class CollectPropertyBase : DriverPropertyBase
/// </summary>
[MinValue(1)]
public virtual int DutyCycle { get; set; } = 3;
/// <summary>
/// 写优先,写入时强制读取消操作
/// </summary>
public virtual bool WritePriority { get; set; } = false;
}
/// <summary>
@@ -56,4 +62,6 @@ public abstract class CollectPropertyRetryBase : CollectPropertyBase
[MinValue(1)]
public override int DutyCycle { get; set; } = 3;
[DynamicProperty(Remark = "写优先,写入时强制读取消操作")]
public override bool WritePriority { get; set; } = false;
}

View File

@@ -37,20 +37,6 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
#region
/// <summary>
/// 当前设备
/// </summary>
public DeviceRuntime? CurrentDevice { get; private set; }
/// <summary>
/// 当前设备Id
/// </summary>
public long DeviceId => CurrentDevice?.Id ?? 0;
/// <summary>
/// 当前设备名称
/// </summary>
public string? DeviceName => CurrentDevice?.Name;
/// <summary>
/// 调试UI Type如果不存在返回null
/// </summary>
@@ -76,25 +62,8 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
/// </summary>
public abstract object DriverProperties { get; }
/// <summary>
/// 是否执行了Start方法
/// </summary>
public bool IsStarted { get; protected set; } = false;
/// <summary>
/// 是否初始化成功,失败时不再执行,等待检测重启
/// </summary>
public bool IsInitSuccess { get; internal set; } = true;
/// <summary>
/// 是否采集插件
/// </summary>
public virtual bool? IsCollectDevice => CurrentDevice?.IsCollect;
/// <summary>
/// 暂停
/// </summary>
public bool Pause => CurrentDevice?.Pause == true;
private List<IEditorItem> pluginPropertyEditorItems;
public List<IEditorItem> PluginPropertyEditorItems
@@ -112,6 +81,82 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
private IStringLocalizer Localizer { get; }
#endregion
public virtual bool GetAuthentication(out DateTime? expireTime)
{
expireTime = null;
return true;
}
public string GetAuthString()
{
if (PluginServiceUtil.IsEducation(GetType()))
{
StringBuilder stringBuilder = new();
var ret = GetAuthentication(out var expireTime);
if (ret)
{
stringBuilder.Append(Localizer["Authorized"]);
}
else
{
stringBuilder.Append(Localizer["Unauthorized"]);
}
stringBuilder.Append(" ");
if (expireTime.HasValue && (DateTime.Now - expireTime.Value).TotalHours > -72)
{
stringBuilder.Append(',');
stringBuilder.Append(Localizer["ExpireTime", expireTime.Value.ToString("yyyy-MM-dd HH")]);
}
return stringBuilder.ToString();
}
return string.Empty;
}
#if !Management
/// <summary>
/// 是否执行了Start方法
/// </summary>
public bool IsStarted { get; protected set; } = false;
/// <summary>
/// 是否初始化成功,失败时不再执行,等待检测重启
/// </summary>
public bool IsInitSuccess { get; internal set; } = true;
/// <summary>
/// 是否采集插件
/// </summary>
public virtual bool? IsCollectDevice => CurrentDevice?.IsCollect;
/// <summary>
/// 当前设备
/// </summary>
public DeviceRuntime? CurrentDevice { get; private set; }
/// <summary>
/// 当前设备Id
/// </summary>
public long DeviceId => CurrentDevice?.Id ?? 0;
/// <summary>
/// 当前设备名称
/// </summary>
public string? DeviceName => CurrentDevice?.Name;
/// <summary>
/// 暂停
/// </summary>
public bool Pause => CurrentDevice?.Pause == true;
protected object pauseLock = new object();
/// <summary>
/// 暂停
@@ -378,38 +423,6 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
#region
public virtual bool GetAuthentication(out DateTime? expireTime)
{
expireTime = null;
return true;
}
public string GetAuthString()
{
if (PluginServiceUtil.IsEducation(GetType()))
{
StringBuilder stringBuilder = new();
var ret = GetAuthentication(out var expireTime);
if (ret)
{
stringBuilder.Append(Localizer["Authorized"]);
}
else
{
stringBuilder.Append(Localizer["Unauthorized"]);
}
stringBuilder.Append(" ");
if (expireTime.HasValue && (DateTime.Now - expireTime.Value).TotalHours > -72)
{
stringBuilder.Append(',');
stringBuilder.Append(Localizer["ExpireTime", expireTime.Value.ToString("yyyy-MM-dd HH")]);
}
return stringBuilder.ToString();
}
return string.Empty;
}
/// <summary>
/// 开始通讯执行的方法
@@ -456,4 +469,7 @@ public abstract class DriverBase : AsyncDisposableObject, IDriver
public abstract Task AfterVariablesChangedAsync(CancellationToken cancellationToken);
#endregion
#endif
}

View File

@@ -37,6 +37,7 @@ public static class DynamicModelExtension
}
}
#if !Management
/// <summary>
/// 获取变量的业务属性值
/// </summary>
@@ -79,6 +80,8 @@ public static class DynamicModelExtension
return null; // 未找到对应的业务设备Id返回null
}
#endif
public static IEnumerable<IGrouping<object[], T>> GroupByKeys<T>(this IEnumerable<T> values, params string[] keys)
{
// 获取动态对象集合中指定键的属性信息

View File

@@ -16,25 +16,31 @@ namespace ThingsGateway.Gateway.Application
{
public interface IDriver : IAsyncDisposable
{
bool DisposedValue { get; }
ChannelRuntime CurrentChannel { get; }
DeviceRuntime? CurrentDevice { get; }
long DeviceId { get; }
string? DeviceName { get; }
Type DriverDebugUIType { get; }
object DriverProperties { get; }
Type DriverPropertyUIType { get; }
Type DriverUIType { get; }
Type DriverVariableAddressUIType { get; }
List<IEditorItem> PluginPropertyEditorItems { get; }
#if !Management
bool? IsCollectDevice { get; }
bool DisposedValue { get; }
ChannelRuntime CurrentChannel { get; }
DeviceRuntime? CurrentDevice { get; }
long DeviceId { get; }
string? DeviceName { get; }
bool IsInitSuccess { get; }
bool IsStarted { get; }
bool Pause { get; }
LoggerGroup LogMessage { get; }
string LogPath { get; }
bool Pause { get; }
string PluginDirectory { get; }
List<IEditorItem> PluginPropertyEditorItems { get; }
Dictionary<long, VariableRuntime> IdVariableRuntimes { get; }
IDeviceThreadManage DeviceThreadManage { get; }
@@ -42,6 +48,10 @@ namespace ThingsGateway.Gateway.Application
void PauseThread(bool pause);
Task SetLogAsync(LogLevel? logLevel = null, bool upDataBase = true);
Task AfterVariablesChangedAsync(CancellationToken cancellationToken);
#endif
string GetAuthString();
bool GetAuthentication(out DateTime? expireTime);
}

View File

@@ -0,0 +1,284 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using System.ComponentModel.DataAnnotations;
namespace ThingsGateway.Gateway.Application;
public class AlarmPropertys : IValidatableObject
{
private int alarmDelay;
private int alarmLevel;
private decimal hAlarmCode = 50;
private decimal lAlarmCode = 10;
private decimal hHAlarmCode = 90;
private decimal lLAlarmCode = 0;
private bool boolOpenAlarmEnable;
private bool boolCloseAlarmEnable;
private bool hAlarmEnable;
private bool hHAlarmEnable;
private bool lLAlarmEnable;
private bool lAlarmEnable;
private bool customAlarmEnable;
private string boolOpenRestrainExpressions;
private string boolOpenAlarmText;
private string boolCloseRestrainExpressions;
private string boolCloseAlarmText;
private string hRestrainExpressions;
private string hAlarmText;
private string hHRestrainExpressions;
private string hHAlarmText;
private string lRestrainExpressions;
private string lAlarmText;
private string lLRestrainExpressions;
private string lLAlarmText;
private string customRestrainExpressions;
private string customAlarmText;
private string customAlarmCode;
#region
/// <summary>
/// 报警等级
/// </summary>
[SugarColumn(ColumnDescription = "报警等级")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public int AlarmLevel { get => alarmLevel; set => alarmLevel = value; }
/// <summary>
/// 报警延时
/// </summary>
[SugarColumn(ColumnDescription = "报警延时")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public int AlarmDelay { get => alarmDelay; set => alarmDelay = value; }
/// <summary>
/// 布尔开报警使能
/// </summary>
[SugarColumn(ColumnDescription = "布尔开报警使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool BoolOpenAlarmEnable { get => boolOpenAlarmEnable; set => boolOpenAlarmEnable = value; }
/// <summary>
/// 布尔开报警约束
/// </summary>
[SugarColumn(ColumnDescription = "布尔开报警约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string BoolOpenRestrainExpressions { get => boolOpenRestrainExpressions; set => boolOpenRestrainExpressions = value; }
/// <summary>
/// 布尔开报警文本
/// </summary>
[SugarColumn(ColumnDescription = "布尔开报警文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string BoolOpenAlarmText { get => boolOpenAlarmText; set => boolOpenAlarmText = value; }
/// <summary>
/// 布尔关报警使能
/// </summary>
[SugarColumn(ColumnDescription = "布尔关报警使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool BoolCloseAlarmEnable { get => boolCloseAlarmEnable; set => boolCloseAlarmEnable = value; }
/// <summary>
/// 布尔关报警约束
/// </summary>
[SugarColumn(ColumnDescription = "布尔关报警约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string BoolCloseRestrainExpressions { get => boolCloseRestrainExpressions; set => boolCloseRestrainExpressions = value; }
/// <summary>
/// 布尔关报警文本
/// </summary>
[SugarColumn(ColumnDescription = "布尔关报警文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string BoolCloseAlarmText { get => boolCloseAlarmText; set => boolCloseAlarmText = value; }
/// <summary>
/// 高报使能
/// </summary>
[SugarColumn(ColumnDescription = "高报使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool HAlarmEnable { get => hAlarmEnable; set => hAlarmEnable = value; }
/// <summary>
/// 高报约束
/// </summary>
[SugarColumn(ColumnDescription = "高报约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string HRestrainExpressions { get => hRestrainExpressions; set => hRestrainExpressions = value; }
/// <summary>
/// 高报文本
/// </summary>
[SugarColumn(ColumnDescription = "高报文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string HAlarmText { get => hAlarmText; set => hAlarmText = value; }
/// <summary>
/// 高限值
/// </summary>
[SugarColumn(ColumnDescription = "高限值", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public decimal HAlarmCode { get => hAlarmCode; set => hAlarmCode = value; }
/// <summary>
/// 高高报使能
/// </summary>
[SugarColumn(ColumnDescription = "高高报使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool HHAlarmEnable { get => hHAlarmEnable; set => hHAlarmEnable = value; }
/// <summary>
/// 高高报约束
/// </summary>
[SugarColumn(ColumnDescription = "高高报约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string HHRestrainExpressions { get => hHRestrainExpressions; set => hHRestrainExpressions = value; }
/// <summary>
/// 高高报文本
/// </summary>
[SugarColumn(ColumnDescription = "高高报文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string HHAlarmText { get => hHAlarmText; set => hHAlarmText = value; }
/// <summary>
/// 高高限值
/// </summary>
[SugarColumn(ColumnDescription = "高高限值", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public decimal HHAlarmCode { get => hHAlarmCode; set => hHAlarmCode = value; }
/// <summary>
/// 低报使能
/// </summary>
[SugarColumn(ColumnDescription = "低报使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool LAlarmEnable { get => lAlarmEnable; set => lAlarmEnable = value; }
/// <summary>
/// 低报约束
/// </summary>
[SugarColumn(ColumnDescription = "低报约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string LRestrainExpressions { get => lRestrainExpressions; set => lRestrainExpressions = value; }
/// <summary>
/// 低报文本
/// </summary>
[SugarColumn(ColumnDescription = "低报文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string LAlarmText { get => lAlarmText; set => lAlarmText = value; }
/// <summary>
/// 低限值
/// </summary>
[SugarColumn(ColumnDescription = "低限值", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public decimal LAlarmCode { get => lAlarmCode; set => lAlarmCode = value; }
/// <summary>
/// 低低报使能
/// </summary>
[SugarColumn(ColumnDescription = "低低报使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool LLAlarmEnable { get => lLAlarmEnable; set => lLAlarmEnable = value; }
/// <summary>
/// 低低报约束
/// </summary>
[SugarColumn(ColumnDescription = "低低报约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string LLRestrainExpressions { get => lLRestrainExpressions; set => lLRestrainExpressions = value; }
/// <summary>
/// 低低报文本
/// </summary>
[SugarColumn(ColumnDescription = "低低报文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string LLAlarmText { get => lLAlarmText; set => lLAlarmText = value; }
/// <summary>
/// 低低限值
/// </summary>
[SugarColumn(ColumnDescription = "低低限值", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public decimal LLAlarmCode { get => lLAlarmCode; set => lLAlarmCode = value; }
/// <summary>
/// 自定义报警使能
/// </summary>
[SugarColumn(ColumnDescription = "自定义报警使能")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public bool CustomAlarmEnable { get => customAlarmEnable; set => customAlarmEnable = value; }
/// <summary>
/// 自定义报警条件约束
/// </summary>
[SugarColumn(ColumnDescription = "自定义报警条件约束", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string CustomRestrainExpressions { get => customRestrainExpressions; set => customRestrainExpressions = value; }
/// <summary>
/// 自定义文本
/// </summary>
[SugarColumn(ColumnDescription = "自定义文本", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string CustomAlarmText { get => customAlarmText; set => customAlarmText = value; }
/// <summary>
/// 自定义报警条件
/// </summary>
[SugarColumn(ColumnDescription = "自定义报警条件", IsNullable = true)]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public string CustomAlarmCode { get => customAlarmCode; set => customAlarmCode = value; }
#endregion
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (HHAlarmEnable && HAlarmEnable && HHAlarmCode <= HAlarmCode)
{
yield return new ValidationResult("HHAlarmCode must be greater than HAlarmCode", new[] { nameof(HHAlarmCode), nameof(HAlarmCode) });
}
if (HAlarmEnable && LAlarmEnable && HAlarmCode <= LAlarmCode)
{
yield return new ValidationResult("HAlarmCode must be greater than LAlarmCode", new[] { nameof(HAlarmCode), nameof(LAlarmCode) });
}
if (LAlarmEnable && LLAlarmEnable && LAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("LAlarmCode must be greater than LLAlarmCode", new[] { nameof(LAlarmCode), nameof(LLAlarmCode) });
}
if (HHAlarmEnable && LAlarmEnable && HHAlarmCode <= LAlarmCode)
{
yield return new ValidationResult("HHAlarmCode should be greater than or less than LAlarmCode", new[] { nameof(HHAlarmCode), nameof(LAlarmCode) });
}
if (HHAlarmEnable && LLAlarmEnable && HHAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("HHAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HHAlarmCode), nameof(LLAlarmCode) });
}
if (HAlarmEnable && LLAlarmEnable && HAlarmCode <= LLAlarmCode)
{
yield return new ValidationResult("HAlarmCode should be greater than or less than LLAlarmCode", new[] { nameof(HAlarmCode), nameof(LLAlarmCode) });
}
}
}

View File

@@ -17,8 +17,10 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 后台日志表
///</summary>
#if !Management
[SugarTable("backend_log", TableDescription = "后台日志表")]
[Tenant(SqlSugarConst.DB_Log)]
#endif
public class BackendLog : PrimaryIdEntity
{
/// <summary>

View File

@@ -17,14 +17,15 @@ using TouchSocket.Core;
using TouchSocket.Sockets;
namespace ThingsGateway.Gateway.Application;
#pragma warning disable CS0649
/// <summary>
/// 通道表
/// </summary>
#if !Management
[SugarTable("channel", TableDescription = "通道表")]
[Tenant(SqlSugarConst.DB_Custom)]
[SugarIndex("unique_channel_name", nameof(Channel.Name), OrderByType.Asc, true)]
#endif
public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IBaseEntity
{
/// <summary>

View File

@@ -17,14 +17,15 @@ using System.ComponentModel.DataAnnotations;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Gateway.Application;
#pragma warning disable CS0649
/// <summary>
/// 设备表
/// </summary>
#if !Management
[SugarTable("device", TableDescription = "设备表")]
[Tenant(SqlSugarConst.DB_Custom)]
[SugarIndex("unique_device_name", nameof(Device.Name), OrderByType.Asc, true)]
#endif
public class Device : BaseDataEntity, IValidatableObject
{
public override string ToString()
@@ -170,6 +171,7 @@ public class Device : BaseDataEntity, IValidatableObject
#endregion
#if !Management
/// <summary>
/// 导入验证专用
/// </summary>
@@ -177,6 +179,9 @@ public class Device : BaseDataEntity, IValidatableObject
[Newtonsoft.Json.JsonIgnore]
internal bool IsUp;
#endif
/// <summary>
/// 额外属性
/// </summary>
@@ -184,6 +189,7 @@ public class Device : BaseDataEntity, IValidatableObject
[Newtonsoft.Json.JsonIgnore]
[MapperIgnore]
public ModelValueValidateForm? ModelValueValidateForm;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (RedundantEnable && RedundantDeviceId == null)

Some files were not shown because too many files have changed in this diff Show More