mirror of
				https://gitee.com/ThingsGateway/ThingsGateway.git
				synced 2025-11-04 17:43:58 +08:00 
			
		
		
		
	Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					98f3f2d519 | ||
| 
						 | 
					b76b4e8d68 | ||
| 
						 | 
					07285a7c61 | ||
| 
						 | 
					03c0dfef37 | ||
| 
						 | 
					6ef6929c35 | ||
| 
						 | 
					e3c0c173f0 | ||
| 
						 | 
					7d64e058d4 | ||
| 
						 | 
					e97ee9b64b | ||
| 
						 | 
					6a03e39eeb | ||
| 
						 | 
					525ec740b5 | ||
| 
						 | 
					b790cf5f4e | ||
| 
						 | 
					d1248811fd | ||
| 
						 | 
					022d016e8e | ||
| 
						 | 
					f73245e650 | ||
| 
						 | 
					484461fa05 | ||
| 
						 | 
					7e0b7aff2a | ||
| 
						 | 
					6c450dcb09 | ||
| 
						 | 
					227f44283f | ||
| 
						 | 
					74f6e79625 | ||
| 
						 | 
					cec43e2ce8 | ||
| 
						 | 
					7553b258bb | ||
| 
						 | 
					8bdbdc117e | ||
| 
						 | 
					0e206be296 | ||
| 
						 | 
					00b7353433 | ||
| 
						 | 
					44e7a83593 | ||
| 
						 | 
					dd68d555d4 | ||
| 
						 | 
					0456296103 | ||
| 
						 | 
					a1b66277ff | ||
| 
						 | 
					50758b79bc | ||
| 
						 | 
					06a1f902ad | ||
| 
						 | 
					58f8b23b7c | ||
| 
						 | 
					9e7c348b15 | ||
| 
						 | 
					5f5ff8b43b | ||
| 
						 | 
					f626b4e5fc | ||
| 
						 | 
					bc23200e66 | ||
| 
						 | 
					95ab59fd5a | ||
| 
						 | 
					0bbee003b0 | 
@@ -20,11 +20,14 @@ A cross-platform, high-performance edge data collection gateway based on net9.
 | 
			
		||||
## Demo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[Demo](http://47.119.161.158:5000/)
 | 
			
		||||
[Demo](https://demo.thingsgateway.cn/)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Account: **SuperAdmin**
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Password: **111111**
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
**In the upper-right corner, switch to the IoT Gateway module in the personal popup box**
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
 | 
			
		||||
## 演示
 | 
			
		||||
 | 
			
		||||
[ThingsGateway演示地址](http://47.119.161.158:5000/)
 | 
			
		||||
[ThingsGateway演示地址](https://demo.thingsgateway.cn/)
 | 
			
		||||
 | 
			
		||||
账户	:  **SuperAdmin**
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -137,7 +137,7 @@ public sealed class OperDescAttribute : MoAttribute
 | 
			
		||||
            Name = (localizerType == null ? App.CreateLocalizerByType(typeof(OperDescAttribute)) : App.CreateLocalizerByType(localizerType))![Description],
 | 
			
		||||
            Category = LogCateGoryEnum.Operate,
 | 
			
		||||
            ExeStatus = true,
 | 
			
		||||
            OpIp = AppService?.RemoteIpAddress?.MapToIPv4()?.ToString() ?? string.Empty,
 | 
			
		||||
            OpIp = AppService?.RemoteIpAddress ?? string.Empty,
 | 
			
		||||
            OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
 | 
			
		||||
            OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
 | 
			
		||||
            OpTime = DateTime.Now,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
[Route("openapi/auth")]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
[LoggingMonitor]
 | 
			
		||||
[ApiController]
 | 
			
		||||
public class OpenApiController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private readonly IAuthService _authService;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,13 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
[Route("api/[controller]/[action]")]
 | 
			
		||||
[AllowAnonymous]
 | 
			
		||||
[ApiController]
 | 
			
		||||
public class TestController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    public Task Test(string data)
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    public void Test()
 | 
			
		||||
    {
 | 
			
		||||
        for (int i = 0; i < 3; i++)
 | 
			
		||||
        {
 | 
			
		||||
            GC.Collect();
 | 
			
		||||
            GC.WaitForPendingFinalizers();
 | 
			
		||||
        }
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
        GC.Collect();
 | 
			
		||||
        GC.WaitForPendingFinalizers();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ public class SysDict : BaseEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 类型
 | 
			
		||||
    ///</summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "类型", Length = 200)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "类型")]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public virtual DictTypeEnum DictType { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ public class SysOperateLog
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 日志分类
 | 
			
		||||
    ///</summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "日志分类", Length = 200)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "日志分类")]
 | 
			
		||||
    [AutoGenerateColumn(Order = 1, Filterable = true, Sortable = true)]
 | 
			
		||||
    public LogCateGoryEnum Category { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ public class SysPosition : BaseEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分类
 | 
			
		||||
    ///</summary>
 | 
			
		||||
    [SugarColumn(ColumnName = "Category", ColumnDescription = "分类", Length = 200)]
 | 
			
		||||
    [SugarColumn(ColumnName = "Category", ColumnDescription = "分类")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
 | 
			
		||||
    public virtual PositionCategoryEnum Category { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ public class SysRelation : PrimaryKeyEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分类
 | 
			
		||||
    ///</summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "分类", Length = 200)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "分类")]
 | 
			
		||||
    public RelationCategoryEnum Category { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public class SysRole : BaseEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分类
 | 
			
		||||
    ///</summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "分类", Length = 200, IsNullable = false)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "分类", IsNullable = false)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Sortable = true, Filterable = true)]
 | 
			
		||||
    public virtual RoleCategoryEnum Category { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "CreateOrgId"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
    "UserExpire": "User expired, please login again"
 | 
			
		||||
  },
 | 
			
		||||
@@ -24,9 +35,6 @@
 | 
			
		||||
    "LatestLoginTime": "LatestLoginTime",
 | 
			
		||||
    "LatestLoginDevice": "LatestLoginDevice",
 | 
			
		||||
    "LatestLoginAddress": "LatestLoginAddress",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "OrgNames": "OrgNames",
 | 
			
		||||
    "PositionName": "PositionName",
 | 
			
		||||
    "OrgId": "Org",
 | 
			
		||||
@@ -60,9 +68,6 @@
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Name.Required": "{0} is required",
 | 
			
		||||
    "Category": "Category",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "OrgId": "Org",
 | 
			
		||||
    "Global": "Global",
 | 
			
		||||
    "Status": "Status",
 | 
			
		||||
@@ -105,9 +110,6 @@
 | 
			
		||||
    "Category": "Category",
 | 
			
		||||
    "Target": "Target",
 | 
			
		||||
    "NavLinkMatch": "NavLinkMatch",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "ParentId": "Parent",
 | 
			
		||||
    "ResourceDup": "Duplicate name {0} exists",
 | 
			
		||||
    "ResourceParentChoiceSelf": "Parent cannot choose itself",
 | 
			
		||||
@@ -134,9 +136,6 @@
 | 
			
		||||
    "Status": "Status",
 | 
			
		||||
    "OrgId": "Organization",
 | 
			
		||||
    "Remark": "Remarks",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "Dup": "Duplicate position exists with Category {0} and Name {1}",
 | 
			
		||||
    "CodeDup": "Duplicate code {0} exists",
 | 
			
		||||
    "NameDup": "Duplicate name {0} exists",
 | 
			
		||||
@@ -159,9 +158,6 @@
 | 
			
		||||
    "Names": "Names",
 | 
			
		||||
    "Remark": "Remarks",
 | 
			
		||||
    "DirectorId": "Director",
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "Dup": "Duplicate organization exists with Category {0} and Name {1}",
 | 
			
		||||
    "CodeDup": "Duplicate code {0} exists",
 | 
			
		||||
    "NameDup": "Duplicate name {0} exists",
 | 
			
		||||
@@ -358,9 +354,6 @@
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Code": "Code",
 | 
			
		||||
    "Remark": "Remark",
 | 
			
		||||
    "SortCode": "Sort",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "DemoCanotUpdateWebsitePolicy": "DEMO environment does not allow modifying website settings",
 | 
			
		||||
    "DictDup": "Duplicate configuration exists, category {0}, name {1}"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,469 +1,462 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseDataEntity": {
 | 
			
		||||
    "CreateOrgId": "创建机构Id"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.BaseEntity": {
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
 | 
			
		||||
      "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
    },
 | 
			
		||||
    "UserExpire": "用户登录已过期,请重新登录"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysUser": {
 | 
			
		||||
      "Disable": "禁用",
 | 
			
		||||
      "Enable": "启用",
 | 
			
		||||
      "GrantRole": "分配角色",
 | 
			
		||||
      "ExitVerificat": "您已被强制下线",
 | 
			
		||||
      "PasswordEdited": "密码被修改,已退出登录",
 | 
			
		||||
      "Avatar": "头像",
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "Account.Required": " {0} 是必填项",
 | 
			
		||||
      "Password": "密码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "Phone": "手机",
 | 
			
		||||
      "Email": "邮箱",
 | 
			
		||||
      "LastLoginIp": "上次登录ip",
 | 
			
		||||
      "LastLoginDevice": "上次登录设备",
 | 
			
		||||
      "LastLoginTime": "上次登录时间",
 | 
			
		||||
      "LastLoginAddress": "上次登录地点",
 | 
			
		||||
      "LatestLoginIp": "最新登录ip",
 | 
			
		||||
      "LatestLoginTime": "最新登录时间",
 | 
			
		||||
      "LatestLoginDevice": "最新登录设备",
 | 
			
		||||
      "LatestLoginAddress": "最新登录地点",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "OrgNames": "机构名称",
 | 
			
		||||
      "PositionName": "职位名称",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "PositionId": "职位",
 | 
			
		||||
      "DirectorId": "主管",
 | 
			
		||||
      "CheckSelf": "禁止 {0} 自己",
 | 
			
		||||
      "CanotDeleteAdminUser": "不可删除系统内置超管用户",
 | 
			
		||||
      "CanotEditAdminUser": "不可编辑超管用户",
 | 
			
		||||
      "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
      "EmailDup": "存在重复的邮箱 {0}",
 | 
			
		||||
      "AccountDup": "存在重复的账号 {0}",
 | 
			
		||||
      "CanotDeleteSelf": "不可删除自己",
 | 
			
		||||
      "EmailError": "邮箱 {0} 格式错误",
 | 
			
		||||
      "PhoneError": "手机号码 {0} 格式错误",
 | 
			
		||||
      "NoOrg": "组织机构不存在",
 | 
			
		||||
      "DirectorSelf": "不能设置自己为主管",
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysUser": {
 | 
			
		||||
    "Disable": "禁用",
 | 
			
		||||
    "Enable": "启用",
 | 
			
		||||
    "GrantRole": "分配角色",
 | 
			
		||||
    "ExitVerificat": "您已被强制下线",
 | 
			
		||||
    "PasswordEdited": "密码被修改,已退出登录",
 | 
			
		||||
    "Avatar": "头像",
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "Account.Required": " {0} 是必填项",
 | 
			
		||||
    "Password": "密码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "Phone": "手机",
 | 
			
		||||
    "Email": "邮箱",
 | 
			
		||||
    "LastLoginIp": "上次登录ip",
 | 
			
		||||
    "LastLoginDevice": "上次登录设备",
 | 
			
		||||
    "LastLoginTime": "上次登录时间",
 | 
			
		||||
    "LastLoginAddress": "上次登录地点",
 | 
			
		||||
    "LatestLoginIp": "最新登录ip",
 | 
			
		||||
    "LatestLoginTime": "最新登录时间",
 | 
			
		||||
    "LatestLoginDevice": "最新登录设备",
 | 
			
		||||
    "LatestLoginAddress": "最新登录地点",
 | 
			
		||||
    "OrgNames": "机构名称",
 | 
			
		||||
    "PositionName": "职位名称",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
    "PositionId": "职位",
 | 
			
		||||
    "DirectorId": "主管",
 | 
			
		||||
    "CheckSelf": "禁止 {0} 自己",
 | 
			
		||||
    "CanotDeleteAdminUser": "不可删除系统内置超管用户",
 | 
			
		||||
    "CanotEditAdminUser": "不可编辑超管用户",
 | 
			
		||||
    "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
    "EmailDup": "存在重复的邮箱 {0}",
 | 
			
		||||
    "AccountDup": "存在重复的账号 {0}",
 | 
			
		||||
    "CanotDeleteSelf": "不可删除自己",
 | 
			
		||||
    "EmailError": "邮箱 {0} 格式错误",
 | 
			
		||||
    "PhoneError": "手机号码 {0} 格式错误",
 | 
			
		||||
    "NoOrg": "组织机构不存在",
 | 
			
		||||
    "DirectorSelf": "不能设置自己为主管",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      "DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
 | 
			
		||||
      "OldPasswordError": "原密码错误",
 | 
			
		||||
      "ConfirmPasswordDiff": "两次输入的密码不一致",
 | 
			
		||||
      "PasswordLengthLess": "密码长度不能小于 {0} ",
 | 
			
		||||
      "PasswordMustNum ": "密码必须包含数字",
 | 
			
		||||
      "PasswordMustLow": "密码必须包含小写字母",
 | 
			
		||||
      "PasswordMustUpp": "密码必须包含大写字母",
 | 
			
		||||
      "PasswordMustSpecial": "密码必须包含特殊字符"
 | 
			
		||||
    },
 | 
			
		||||
    "DemoCanotUpdatePassword": "DEMO环境不允许修改密码",
 | 
			
		||||
    "OldPasswordError": "原密码错误",
 | 
			
		||||
    "ConfirmPasswordDiff": "两次输入的密码不一致",
 | 
			
		||||
    "PasswordLengthLess": "密码长度不能小于 {0} ",
 | 
			
		||||
    "PasswordMustNum ": "密码必须包含数字",
 | 
			
		||||
    "PasswordMustLow": "密码必须包含小写字母",
 | 
			
		||||
    "PasswordMustUpp": "密码必须包含大写字母",
 | 
			
		||||
    "PasswordMustSpecial": "密码必须包含特殊字符"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysRole": {
 | 
			
		||||
      "Code": "编码",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Name.Required": " {0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "Global": "全局",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysRole": {
 | 
			
		||||
    "Code": "编码",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Global": "全局",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
 | 
			
		||||
      "CanotDeleteAdmin": "不可删除系统内置超管角色",
 | 
			
		||||
      "CanotEditAdmin": "不可编辑超管角色",
 | 
			
		||||
      "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
    "CanotDeleteAdmin": "不可删除系统内置超管角色",
 | 
			
		||||
    "CanotEditAdmin": "不可编辑超管角色",
 | 
			
		||||
    "CanotGrantAdmin": "不能分配超管角色",
 | 
			
		||||
 | 
			
		||||
      "NameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
      "OrgNotNull": "机构不能为空",
 | 
			
		||||
      "SameOrgNameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
      "CannotRoleScopeAll": "机构角色不能选择全局数据范围",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.RoleCategoryEnum": {
 | 
			
		||||
      "Global": "全局",
 | 
			
		||||
      "Org": "机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DataScopeEnum": {
 | 
			
		||||
      "SCOPE_SELF": "仅自己",
 | 
			
		||||
      "SCOPE_ALL": "全部",
 | 
			
		||||
      "SCOPE_ORG": "仅所属组织",
 | 
			
		||||
      "SCOPE_ORG_CHILD": "所属组织及以下",
 | 
			
		||||
      "SCOPE_ORG_DEFINE": "自定义"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DefaultDataScope": {
 | 
			
		||||
      "ScopeCategory": "数据范围",
 | 
			
		||||
      "ScopeDefineOrgIdList": "自定义列表"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysResource": {
 | 
			
		||||
      "Title": "标题",
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Title.Required": "{0} 是必填项",
 | 
			
		||||
      "Href.Required": "{0} 是必填项",
 | 
			
		||||
      "Icon": "图标",
 | 
			
		||||
      "Href": "路径",
 | 
			
		||||
      "Code": "编码",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Target": "跳转类型",
 | 
			
		||||
      "NavLinkMatch": "匹配类型",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "ParentId": "上级菜单",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "ResourceDup": "存在重复的名称 {0}",
 | 
			
		||||
      "ResourceParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ResourceParentNull": "父级不存在 {0}",
 | 
			
		||||
      "NotFoundResource": "系统异常,没找到该菜单",
 | 
			
		||||
      "ModuleIdDiff": "模块与上级菜单不一致",
 | 
			
		||||
      "CanotDeleteSystemResource": "不可删除系统资源 {0}",
 | 
			
		||||
      "ResourceMenuHrefNotNull": "菜单的路径不能为空"
 | 
			
		||||
    },
 | 
			
		||||
    "NameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
    "OrgNotNull": "机构不能为空",
 | 
			
		||||
    "SameOrgNameDup": "存在重复的角色名称 {0}",
 | 
			
		||||
    "CannotRoleScopeAll": "机构角色不能选择全局数据范围",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.RoleCategoryEnum": {
 | 
			
		||||
    "Global": "全局",
 | 
			
		||||
    "Org": "机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DataScopeEnum": {
 | 
			
		||||
    "SCOPE_SELF": "仅自己",
 | 
			
		||||
    "SCOPE_ALL": "全部",
 | 
			
		||||
    "SCOPE_ORG": "仅所属组织",
 | 
			
		||||
    "SCOPE_ORG_CHILD": "所属组织及以下",
 | 
			
		||||
    "SCOPE_ORG_DEFINE": "自定义"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DefaultDataScope": {
 | 
			
		||||
    "ScopeCategory": "数据范围",
 | 
			
		||||
    "ScopeDefineOrgIdList": "自定义列表"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysResource": {
 | 
			
		||||
    "Title": "标题",
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Title.Required": "{0} 是必填项",
 | 
			
		||||
    "Href.Required": "{0} 是必填项",
 | 
			
		||||
    "Icon": "图标",
 | 
			
		||||
    "Href": "路径",
 | 
			
		||||
    "Code": "编码",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Target": "跳转类型",
 | 
			
		||||
    "NavLinkMatch": "匹配类型",
 | 
			
		||||
    "ParentId": "上级菜单",
 | 
			
		||||
    "ResourceDup": "存在重复的名称 {0}",
 | 
			
		||||
    "ResourceParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ResourceParentNull": "父级不存在 {0}",
 | 
			
		||||
    "NotFoundResource": "系统异常,没找到该菜单",
 | 
			
		||||
    "ModuleIdDiff": "模块与上级菜单不一致",
 | 
			
		||||
    "CanotDeleteSystemResource": "不可删除系统资源 {0}",
 | 
			
		||||
    "ResourceMenuHrefNotNull": "菜单的路径不能为空"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOrgCopyInput": {
 | 
			
		||||
      "TargetId": "目标机构",
 | 
			
		||||
      "ContainsChild": "包含下级",
 | 
			
		||||
      "ContainsPosition": "包含职位"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysPosition": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "OrgId.MinValue": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "OrgId": "机构",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
      "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
      "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
      "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
      "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ParentNull": "父级不存在 {0}",
 | 
			
		||||
      "DeleteUserFirst": "请先删除职位下的用户"
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOrgCopyInput": {
 | 
			
		||||
    "TargetId": "目标机构",
 | 
			
		||||
    "ContainsChild": "包含下级",
 | 
			
		||||
    "ContainsPosition": "包含职位"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysPosition": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "OrgId.MinValue": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "OrgId": "机构",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "Dup": "存在重复的岗位 分类 {0} 名称 {1}",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
    "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
    "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
    "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
    "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ParentNull": "父级不存在 {0}",
 | 
			
		||||
    "DeleteUserFirst": "请先删除职位下的用户"
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOrg": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Status": "状态",
 | 
			
		||||
      "ParentId": "上级机构",
 | 
			
		||||
      "Names": "机构全称",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "DirectorId": "主管",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "Dup": "存在重复的机构 分类 {0} 名称 {1}",
 | 
			
		||||
      "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
      "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
      "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
      "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
      "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
      "ParentNull": "父级不存在 {0}",
 | 
			
		||||
      "DeleteUserFirst": "请先删除机构下的用户",
 | 
			
		||||
      "DeleteRoleFirst": "请先删除机构下的角色",
 | 
			
		||||
      "DeletePositionFirst": "请先删除机构下的职位",
 | 
			
		||||
      "RootOrg": "无法创建顶层机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OrgEnum": {
 | 
			
		||||
      "COMPANY": "公司",
 | 
			
		||||
      "DEPT": "部门"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PositionCategoryEnum": {
 | 
			
		||||
      "HIGH": "高层",
 | 
			
		||||
      "MIDDLE": "中层",
 | 
			
		||||
      "LOW": "低层"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOrg": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Status": "状态",
 | 
			
		||||
    "ParentId": "上级机构",
 | 
			
		||||
    "Names": "机构全称",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "DirectorId": "主管",
 | 
			
		||||
    "Dup": "存在重复的机构 分类 {0} 名称 {1}",
 | 
			
		||||
    "CodeDup": "存在重复的编码 {0}",
 | 
			
		||||
    "NameDup": "存在重复的名称 {0}",
 | 
			
		||||
    "CanotContainsSelf": "不可包含自己",
 | 
			
		||||
    "TargetNameDup": "目标节点存在重复的名称 {0}",
 | 
			
		||||
    "ParentChoiceSelf": "父级不能选择自己",
 | 
			
		||||
    "ParentNull": "父级不存在 {0}",
 | 
			
		||||
    "DeleteUserFirst": "请先删除机构下的用户",
 | 
			
		||||
    "DeleteRoleFirst": "请先删除机构下的角色",
 | 
			
		||||
    "DeletePositionFirst": "请先删除机构下的职位",
 | 
			
		||||
    "RootOrg": "无法创建顶层机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OrgEnum": {
 | 
			
		||||
    "COMPANY": "公司",
 | 
			
		||||
    "DEPT": "部门"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PositionCategoryEnum": {
 | 
			
		||||
    "HIGH": "高层",
 | 
			
		||||
    "MIDDLE": "中层",
 | 
			
		||||
    "LOW": "低层"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //controller
 | 
			
		||||
    "ThingsGateway.Admin.Application.AuthController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "AuthController": "登录API",
 | 
			
		||||
      "LoginAsync": "登录",
 | 
			
		||||
      "LogoutAsync": "注销"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.TestController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "TestController": "测试API",
 | 
			
		||||
      "Test": "测试"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OpenApiAuthController": {
 | 
			
		||||
      //auth
 | 
			
		||||
      "OpenApiAuthController": "登录API",
 | 
			
		||||
      "LoginAsync": "登录",
 | 
			
		||||
      "LogoutAsync": "注销"
 | 
			
		||||
    },
 | 
			
		||||
  //controller
 | 
			
		||||
  "ThingsGateway.Admin.Application.AuthController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "AuthController": "登录API",
 | 
			
		||||
    "LoginAsync": "登录",
 | 
			
		||||
    "LogoutAsync": "注销"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.TestController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "TestController": "测试API",
 | 
			
		||||
    "Test": "测试"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OpenApiAuthController": {
 | 
			
		||||
    //auth
 | 
			
		||||
    "OpenApiAuthController": "登录API",
 | 
			
		||||
    "LoginAsync": "登录",
 | 
			
		||||
    "LogoutAsync": "注销"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.FileService": {
 | 
			
		||||
      "FileNullError": "文件不能为空",
 | 
			
		||||
      "FileLengthError": "文件大小不允许超过 {0} M",
 | 
			
		||||
      "FileTypeError": "不支持 {0} 格式"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.FileService": {
 | 
			
		||||
    "FileNullError": "文件不能为空",
 | 
			
		||||
    "FileLengthError": "文件大小不允许超过 {0} M",
 | 
			
		||||
    "FileTypeError": "不支持 {0} 格式"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.UnifyResultProvider": {
 | 
			
		||||
      "TokenOver": "登录已过期,请重新登录",
 | 
			
		||||
      "NoPermission": "禁止访问,没有权限"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.UnifyResultProvider": {
 | 
			
		||||
    "TokenOver": "登录已过期,请重新登录",
 | 
			
		||||
    "NoPermission": "禁止访问,没有权限"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.AuthService": {
 | 
			
		||||
      "TenantNull": "租户不存在",
 | 
			
		||||
      "OrgDisable": "所属公司/部门已停用,请联系管理员",
 | 
			
		||||
      "SingleLoginWarn": "您的账号已在别处登录",
 | 
			
		||||
      "UserNull": "用户 {0} 不存在",
 | 
			
		||||
      "PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
 | 
			
		||||
      "AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
 | 
			
		||||
      "UserDisable": "账号 {0} 已停用",
 | 
			
		||||
      "MustDesc": "密码需要DESC加密后传入",
 | 
			
		||||
      "UserNoModule": "该账号未分配模块,请联系管理员"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.AuthService": {
 | 
			
		||||
    "TenantNull": "租户不存在",
 | 
			
		||||
    "OrgDisable": "所属公司/部门已停用,请联系管理员",
 | 
			
		||||
    "SingleLoginWarn": "您的账号已在别处登录",
 | 
			
		||||
    "UserNull": "用户 {0} 不存在",
 | 
			
		||||
    "PasswordError": "密码错误次数过多,请 {0} 分钟后再试",
 | 
			
		||||
    "AuthErrorMax": "账号密码错误,超过 {0} 次后将锁定 {1} 分钟,错误次数 {2} ",
 | 
			
		||||
    "UserDisable": "账号 {0} 已停用",
 | 
			
		||||
    "MustDesc": "密码需要DESC加密后传入",
 | 
			
		||||
    "UserNoModule": "该账号未分配模块,请联系管理员"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.HardwareInfo": {
 | 
			
		||||
      "Environment": "主机环境",
 | 
			
		||||
      "FrameworkDescription": "NET框架",
 | 
			
		||||
      "OsArchitecture": "系统架构",
 | 
			
		||||
      "UUID": "唯一编码",
 | 
			
		||||
      "UpdateTime": "更新时间"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.HistoryHardwareInfo": {
 | 
			
		||||
      "DriveUsage": "磁盘使用率",
 | 
			
		||||
      "MemoryUsage": "内存",
 | 
			
		||||
      "CpuUsage": "CPU使用率",
 | 
			
		||||
      "Temperature": "温度",
 | 
			
		||||
      "Battery": "电池"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HardwareInfo": {
 | 
			
		||||
    "Environment": "主机环境",
 | 
			
		||||
    "FrameworkDescription": "NET框架",
 | 
			
		||||
    "OsArchitecture": "系统架构",
 | 
			
		||||
    "UUID": "唯一编码",
 | 
			
		||||
    "UpdateTime": "更新时间"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HistoryHardwareInfo": {
 | 
			
		||||
    "DriveUsage": "磁盘使用率",
 | 
			
		||||
    "MemoryUsage": "内存",
 | 
			
		||||
    "CpuUsage": "CPU使用率",
 | 
			
		||||
    "Temperature": "温度",
 | 
			
		||||
    "Battery": "电池"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //oper
 | 
			
		||||
    "ThingsGateway.Admin.Application.OperDescAttribute": {
 | 
			
		||||
      //dict
 | 
			
		||||
      "SaveDict": "修改字典",
 | 
			
		||||
      "DeleteDict": "删除字典",
 | 
			
		||||
      "EditLoginPolicy": "修改登录策略",
 | 
			
		||||
      "EditPasswordPolicy": "修改密码策略",
 | 
			
		||||
      "EditPagePolicy": "修改页面策略",
 | 
			
		||||
      "EditWebsitePolicy": "修改网站设置",
 | 
			
		||||
      //operlog
 | 
			
		||||
      "DeleteOperLog": "删除操作日志",
 | 
			
		||||
      "ExportOperLog": "导出操作日志",
 | 
			
		||||
  //oper
 | 
			
		||||
  "ThingsGateway.Admin.Application.OperDescAttribute": {
 | 
			
		||||
    //dict
 | 
			
		||||
    "SaveDict": "修改字典",
 | 
			
		||||
    "DeleteDict": "删除字典",
 | 
			
		||||
    "EditLoginPolicy": "修改登录策略",
 | 
			
		||||
    "EditPasswordPolicy": "修改密码策略",
 | 
			
		||||
    "EditPagePolicy": "修改页面策略",
 | 
			
		||||
    "EditWebsitePolicy": "修改网站设置",
 | 
			
		||||
    //operlog
 | 
			
		||||
    "DeleteOperLog": "删除操作日志",
 | 
			
		||||
    "ExportOperLog": "导出操作日志",
 | 
			
		||||
 | 
			
		||||
      //resource
 | 
			
		||||
      "SaveResource": "修改资源",
 | 
			
		||||
      "DeleteResource": "删除资源",
 | 
			
		||||
    //resource
 | 
			
		||||
    "SaveResource": "修改资源",
 | 
			
		||||
    "DeleteResource": "删除资源",
 | 
			
		||||
 | 
			
		||||
      //role
 | 
			
		||||
      "SaveRole": "修改角色",
 | 
			
		||||
      "DeleteRole": "删除角色",
 | 
			
		||||
      "RoleGrantResource": "角色授权资源",
 | 
			
		||||
      "RoleGrantUser": "角色授权用户",
 | 
			
		||||
      "RoleGrantApiPermission": "角色授权OpenApi",
 | 
			
		||||
      "GrantApi": "API",
 | 
			
		||||
      "GrantUser": "用户",
 | 
			
		||||
      "GrantRole": "角色",
 | 
			
		||||
      "GrantResource": "资源",
 | 
			
		||||
      //user
 | 
			
		||||
      "SaveUser": "修改用户",
 | 
			
		||||
      "DeleteuSER": "删除用户",
 | 
			
		||||
      "ResetPassword": "重置密码",
 | 
			
		||||
      "UserGrantRole": "用户授权角色",
 | 
			
		||||
      "UserGrantResource": "用户授权资源",
 | 
			
		||||
      "UserGrantApiPermission": "用户授权OpenApi",
 | 
			
		||||
    //role
 | 
			
		||||
    "SaveRole": "修改角色",
 | 
			
		||||
    "DeleteRole": "删除角色",
 | 
			
		||||
    "RoleGrantResource": "角色授权资源",
 | 
			
		||||
    "RoleGrantUser": "角色授权用户",
 | 
			
		||||
    "RoleGrantApiPermission": "角色授权OpenApi",
 | 
			
		||||
    "GrantApi": "API",
 | 
			
		||||
    "GrantUser": "用户",
 | 
			
		||||
    "GrantRole": "角色",
 | 
			
		||||
    "GrantResource": "资源",
 | 
			
		||||
    //user
 | 
			
		||||
    "SaveUser": "修改用户",
 | 
			
		||||
    "DeleteuSER": "删除用户",
 | 
			
		||||
    "ResetPassword": "重置密码",
 | 
			
		||||
    "UserGrantRole": "用户授权角色",
 | 
			
		||||
    "UserGrantResource": "用户授权资源",
 | 
			
		||||
    "UserGrantApiPermission": "用户授权OpenApi",
 | 
			
		||||
 | 
			
		||||
      //usercenter
 | 
			
		||||
      "UpdateUserInfo": "更新个人信息",
 | 
			
		||||
      "WorkbenchInfo": "更新个人工作台",
 | 
			
		||||
      "UpdatePassword": "更新个人密码",
 | 
			
		||||
    //usercenter
 | 
			
		||||
    "UpdateUserInfo": "更新个人信息",
 | 
			
		||||
    "WorkbenchInfo": "更新个人工作台",
 | 
			
		||||
    "UpdatePassword": "更新个人密码",
 | 
			
		||||
 | 
			
		||||
      //session
 | 
			
		||||
      "ExitVerificat": "强退令牌",
 | 
			
		||||
      "ExitSession": "强退会话",
 | 
			
		||||
    //session
 | 
			
		||||
    "ExitVerificat": "强退令牌",
 | 
			
		||||
    "ExitSession": "强退会话",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      "CopyOrg": "复制机构",
 | 
			
		||||
      "DeleteOrg": "删除机构",
 | 
			
		||||
      "SaveOrg": "保存机构",
 | 
			
		||||
    "CopyOrg": "复制机构",
 | 
			
		||||
    "DeleteOrg": "删除机构",
 | 
			
		||||
    "SaveOrg": "保存机构",
 | 
			
		||||
 | 
			
		||||
      "DeletePosition": "删除岗位",
 | 
			
		||||
      "SavePosition": "保存岗位",
 | 
			
		||||
    "DeletePosition": "删除岗位",
 | 
			
		||||
    "SavePosition": "保存岗位",
 | 
			
		||||
 | 
			
		||||
      "NoPermission": "无权限操作",
 | 
			
		||||
    "NoPermission": "无权限操作",
 | 
			
		||||
 | 
			
		||||
      "CopyResource": "复制资源",
 | 
			
		||||
      "ChangeParentResource": "更改父节点"
 | 
			
		||||
    },
 | 
			
		||||
    "CopyResource": "复制资源",
 | 
			
		||||
    "ChangeParentResource": "更改父节点"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //service
 | 
			
		||||
  //service
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.HardwareJob": {
 | 
			
		||||
      "GetHardwareInfoFail": "获取硬件信息出错"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.HardwareJob": {
 | 
			
		||||
    "GetHardwareInfoFail": "获取硬件信息出错"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    //dto
 | 
			
		||||
    "ThingsGateway.Admin.Application.UserSelectorOutput": {
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "OrgId": "机构"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.ResourceTableSearchModel": {
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Href": "路径",
 | 
			
		||||
      "Title": "标题"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.WorkbenchInfo": {
 | 
			
		||||
      "Razor": "主页",
 | 
			
		||||
      "Shortcuts": "快捷方式"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.UpdatePasswordInput": {
 | 
			
		||||
      "Password": "密码",
 | 
			
		||||
      "NewPassword": "新密码",
 | 
			
		||||
      "ConfirmPassword": "确认密码",
 | 
			
		||||
      "Password.Required": " {0} 是必填项",
 | 
			
		||||
      "NewPassword.Required": " {0} 是必填项",
 | 
			
		||||
      "ConfirmPassword.Required": " {0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
  //dto
 | 
			
		||||
  "ThingsGateway.Admin.Application.UserSelectorOutput": {
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "OrgId": "机构"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.ResourceTableSearchModel": {
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Href": "路径",
 | 
			
		||||
    "Title": "标题"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.WorkbenchInfo": {
 | 
			
		||||
    "Razor": "主页",
 | 
			
		||||
    "Shortcuts": "快捷方式"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.UpdatePasswordInput": {
 | 
			
		||||
    "Password": "密码",
 | 
			
		||||
    "NewPassword": "新密码",
 | 
			
		||||
    "ConfirmPassword": "确认密码",
 | 
			
		||||
    "Password.Required": " {0} 是必填项",
 | 
			
		||||
    "NewPassword.Required": " {0} 是必填项",
 | 
			
		||||
    "ConfirmPassword.Required": " {0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.VerificatInfo": {
 | 
			
		||||
      "Expire": "过期时间(分)",
 | 
			
		||||
      "Online": "在线状态",
 | 
			
		||||
      "VerificatTimeout": "超时时间",
 | 
			
		||||
      "Device": "登录设备",
 | 
			
		||||
      "LoginIp": "登录IP",
 | 
			
		||||
      "LoginTime": "登录时间"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.VerificatInfo": {
 | 
			
		||||
    "Expire": "过期时间(分)",
 | 
			
		||||
    "Online": "在线状态",
 | 
			
		||||
    "VerificatTimeout": "超时时间",
 | 
			
		||||
    "Device": "登录设备",
 | 
			
		||||
    "LoginIp": "登录IP",
 | 
			
		||||
    "LoginTime": "登录时间"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SessionOutput": {
 | 
			
		||||
      "Account": "账号",
 | 
			
		||||
      "Online": "在线状态",
 | 
			
		||||
      "LatestLoginIp": "最新登录ip",
 | 
			
		||||
      "LatestLoginTime": "最新登录时间",
 | 
			
		||||
      "VerificatCount": "令牌数量"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SessionOutput": {
 | 
			
		||||
    "Account": "账号",
 | 
			
		||||
    "Online": "在线状态",
 | 
			
		||||
    "LatestLoginIp": "最新登录ip",
 | 
			
		||||
    "LatestLoginTime": "最新登录时间",
 | 
			
		||||
    "VerificatCount": "令牌数量"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysDict": {
 | 
			
		||||
      "Category.Required": "{0} 是必填项",
 | 
			
		||||
      "Name.Required": "{0} 是必填项",
 | 
			
		||||
      "Code.Required": "{0} 是必填项",
 | 
			
		||||
      "Category": "分类",
 | 
			
		||||
      "Name": "名称",
 | 
			
		||||
      "Code": "代码",
 | 
			
		||||
      "Remark": "备注",
 | 
			
		||||
      "SortCode": "排序",
 | 
			
		||||
      "CreateTime": "创建时间",
 | 
			
		||||
      "UpdateTime": "更新时间",
 | 
			
		||||
      "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
 | 
			
		||||
      "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysDict": {
 | 
			
		||||
    "Category.Required": "{0} 是必填项",
 | 
			
		||||
    "Name.Required": "{0} 是必填项",
 | 
			
		||||
    "Code.Required": "{0} 是必填项",
 | 
			
		||||
    "Category": "分类",
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Code": "代码",
 | 
			
		||||
    "Remark": "备注",
 | 
			
		||||
    "DictDup": "存在重复的配置 分类 {0} 名称 {1}",
 | 
			
		||||
    "DemoCanotUpdateWebsitePolicy": "DEMO环境不允许修改网站设置"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.SysOperateLog": {
 | 
			
		||||
      "ClassName": "类名",
 | 
			
		||||
      "ExeMessage": "具体消息",
 | 
			
		||||
      "MethodName": "方法名称",
 | 
			
		||||
      "ParamJson": "请求参数",
 | 
			
		||||
      "ReqMethod": "请求方式",
 | 
			
		||||
      "ReqUrl": "请求地址",
 | 
			
		||||
      "ResultJson": "返回结果",
 | 
			
		||||
      "Category": "日志分类",
 | 
			
		||||
      "ExeStatus": "执行状态",
 | 
			
		||||
      "Name": "日志名称",
 | 
			
		||||
      "OpAccount": "账号",
 | 
			
		||||
      "OpBrowser": "浏览器",
 | 
			
		||||
      "OpIp": "ip",
 | 
			
		||||
      "OpOs": "系统",
 | 
			
		||||
      "OpTime": "操作时间",
 | 
			
		||||
      "VerificatId": "验证Id"
 | 
			
		||||
  "ThingsGateway.Admin.Application.SysOperateLog": {
 | 
			
		||||
    "ClassName": "类名",
 | 
			
		||||
    "ExeMessage": "具体消息",
 | 
			
		||||
    "MethodName": "方法名称",
 | 
			
		||||
    "ParamJson": "请求参数",
 | 
			
		||||
    "ReqMethod": "请求方式",
 | 
			
		||||
    "ReqUrl": "请求地址",
 | 
			
		||||
    "ResultJson": "返回结果",
 | 
			
		||||
    "Category": "日志分类",
 | 
			
		||||
    "ExeStatus": "执行状态",
 | 
			
		||||
    "Name": "日志名称",
 | 
			
		||||
    "OpAccount": "账号",
 | 
			
		||||
    "OpBrowser": "浏览器",
 | 
			
		||||
    "OpIp": "ip",
 | 
			
		||||
    "OpOs": "系统",
 | 
			
		||||
    "OpTime": "操作时间",
 | 
			
		||||
    "VerificatId": "验证Id"
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.OperateLogPageInput": {
 | 
			
		||||
      "SearchDate": "时间范围",
 | 
			
		||||
      "Account": "操作账号",
 | 
			
		||||
      "Category": "分类"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LoginInput": {
 | 
			
		||||
      "Account": "登录账号",
 | 
			
		||||
      "Password": "登录密码",
 | 
			
		||||
      "Account.Required": "{0} 是必填项",
 | 
			
		||||
      "Password.Required": "{0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogoutInput": {
 | 
			
		||||
      "VerificatId.Required": "{0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.AppConfig": {
 | 
			
		||||
      "LoginPolicy": "登录策略",
 | 
			
		||||
      "PasswordPolicy": "密码策略",
 | 
			
		||||
      "PagePolicy": "页面设置",
 | 
			
		||||
      "WebsitePolicy": "网站设置"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.LoginPolicy": {
 | 
			
		||||
      "SingleOpen": "单用户登录开关",
 | 
			
		||||
      "ErrorLockTime": "登录错误锁定时长(分)",
 | 
			
		||||
      "ErrorResetTime": "登录错误次数过期时长(分)",
 | 
			
		||||
      "ErrorCount": "登录错误次数锁定阈值",
 | 
			
		||||
      "VerificatExpireTime": "登录过期时间(分)",
 | 
			
		||||
      "ErrorLockTime.MinValue": " {0} 值太小",
 | 
			
		||||
      "ErrorResetTime.MinValue": " {0} 值太小",
 | 
			
		||||
      "ErrorCount.MinValue": " {0} 值太小",
 | 
			
		||||
      "VerificatExpireTime.MinValue": " {0} 值太小"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PagePolicy": {
 | 
			
		||||
      "Shortcuts": "默认快捷方式",
 | 
			
		||||
      "Razor": "默认主页"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.PasswordPolicy": {
 | 
			
		||||
      "DefaultPassword": "默认用户密码",
 | 
			
		||||
      "DefaultPassword.Required": " {0} 是必填项",
 | 
			
		||||
      "PasswordMinLen": "密码最小长度",
 | 
			
		||||
      "PasswordMinLen.MinValue": " {0} 值太小",
 | 
			
		||||
      "PasswordContainNum": "包含数字",
 | 
			
		||||
      "PasswordContainLower": "包含小写字母",
 | 
			
		||||
      "PasswordContainUpper": "包含大写字母",
 | 
			
		||||
      "PasswordContainChar": "包含特殊字符"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.WebsitePolicy": {
 | 
			
		||||
      "WebStatus": "是否开放",
 | 
			
		||||
      "CloseTip": "关闭提示",
 | 
			
		||||
      "CloseTip.Required": " {0} 是必填项"
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.OperateLogPageInput": {
 | 
			
		||||
    "SearchDate": "时间范围",
 | 
			
		||||
    "Account": "操作账号",
 | 
			
		||||
    "Category": "分类"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LoginInput": {
 | 
			
		||||
    "Account": "登录账号",
 | 
			
		||||
    "Password": "登录密码",
 | 
			
		||||
    "Account.Required": "{0} 是必填项",
 | 
			
		||||
    "Password.Required": "{0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogoutInput": {
 | 
			
		||||
    "VerificatId.Required": "{0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.AppConfig": {
 | 
			
		||||
    "LoginPolicy": "登录策略",
 | 
			
		||||
    "PasswordPolicy": "密码策略",
 | 
			
		||||
    "PagePolicy": "页面设置",
 | 
			
		||||
    "WebsitePolicy": "网站设置"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LoginPolicy": {
 | 
			
		||||
    "SingleOpen": "单用户登录开关",
 | 
			
		||||
    "ErrorLockTime": "登录错误锁定时长(分)",
 | 
			
		||||
    "ErrorResetTime": "登录错误次数过期时长(分)",
 | 
			
		||||
    "ErrorCount": "登录错误次数锁定阈值",
 | 
			
		||||
    "VerificatExpireTime": "登录过期时间(分)",
 | 
			
		||||
    "ErrorLockTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "ErrorResetTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "ErrorCount.MinValue": " {0} 值太小",
 | 
			
		||||
    "VerificatExpireTime.MinValue": " {0} 值太小"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PagePolicy": {
 | 
			
		||||
    "Shortcuts": "默认快捷方式",
 | 
			
		||||
    "Razor": "默认主页"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.PasswordPolicy": {
 | 
			
		||||
    "DefaultPassword": "默认用户密码",
 | 
			
		||||
    "DefaultPassword.Required": " {0} 是必填项",
 | 
			
		||||
    "PasswordMinLen": "密码最小长度",
 | 
			
		||||
    "PasswordMinLen.MinValue": " {0} 值太小",
 | 
			
		||||
    "PasswordContainNum": "包含数字",
 | 
			
		||||
    "PasswordContainLower": "包含小写字母",
 | 
			
		||||
    "PasswordContainUpper": "包含大写字母",
 | 
			
		||||
    "PasswordContainChar": "包含特殊字符"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.WebsitePolicy": {
 | 
			
		||||
    "WebStatus": "是否开放",
 | 
			
		||||
    "CloseTip": "关闭提示",
 | 
			
		||||
    "CloseTip.Required": " {0} 是必填项"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //enum
 | 
			
		||||
    "ThingsGateway.Admin.Application.ResourceCategoryEnum": {
 | 
			
		||||
      "Module": "模块",
 | 
			
		||||
      "Menu": "菜单",
 | 
			
		||||
      "Button": "按钮"
 | 
			
		||||
    },
 | 
			
		||||
  //enum
 | 
			
		||||
  "ThingsGateway.Admin.Application.ResourceCategoryEnum": {
 | 
			
		||||
    "Module": "模块",
 | 
			
		||||
    "Menu": "菜单",
 | 
			
		||||
    "Button": "按钮"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.TargetEnum": {
 | 
			
		||||
      "_self": "本窗口",
 | 
			
		||||
      "_blank": "新窗口",
 | 
			
		||||
      "_parent": "父级窗口",
 | 
			
		||||
      "_top": "顶级窗口"
 | 
			
		||||
    },
 | 
			
		||||
    "ThingsGateway.Admin.Application.DictTypeEnum": {
 | 
			
		||||
      "System": "系统配置",
 | 
			
		||||
      "Define": "业务配置"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.TargetEnum": {
 | 
			
		||||
    "_self": "本窗口",
 | 
			
		||||
    "_blank": "新窗口",
 | 
			
		||||
    "_parent": "父级窗口",
 | 
			
		||||
    "_top": "顶级窗口"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Admin.Application.DictTypeEnum": {
 | 
			
		||||
    "System": "系统配置",
 | 
			
		||||
    "Define": "业务配置"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogCateGoryEnum": {
 | 
			
		||||
      "Login": "登录",
 | 
			
		||||
      "Logout": "注销",
 | 
			
		||||
      "Operate": "操作",
 | 
			
		||||
      "Exception": "异常"
 | 
			
		||||
    },
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogCateGoryEnum": {
 | 
			
		||||
    "Login": "登录",
 | 
			
		||||
    "Logout": "注销",
 | 
			
		||||
    "Operate": "操作",
 | 
			
		||||
    "Exception": "异常"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    "ThingsGateway.Admin.Application.LogEnum": {
 | 
			
		||||
      "SUCCESS": "成功",
 | 
			
		||||
      "FAIL": "失败"
 | 
			
		||||
    }
 | 
			
		||||
  "ThingsGateway.Admin.Application.LogEnum": {
 | 
			
		||||
    "SUCCESS": "成功",
 | 
			
		||||
    "FAIL": "失败"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,26 +22,19 @@ using System.Globalization;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Extension;
 | 
			
		||||
using ThingsGateway.SpecificationDocument;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
internal sealed class ApiPermissionService : IApiPermissionService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider;
 | 
			
		||||
    private readonly SwaggerGeneratorOptions _generatorOptions;
 | 
			
		||||
 | 
			
		||||
    public ApiPermissionService(
 | 
			
		||||
        IOptions<SwaggerGeneratorOptions> generatorOptions,
 | 
			
		||||
        IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider)
 | 
			
		||||
    {
 | 
			
		||||
        _generatorOptions = generatorOptions.Value;
 | 
			
		||||
        _apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider;
 | 
			
		||||
    }
 | 
			
		||||
    private IEnumerable<string> GetDocumentNames()
 | 
			
		||||
    {
 | 
			
		||||
        return _generatorOptions.SwaggerDocs.Keys;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public List<OpenApiPermissionTreeSelector> ApiPermissionTreeSelector()
 | 
			
		||||
@@ -53,37 +46,37 @@ internal sealed class ApiPermissionService : IApiPermissionService
 | 
			
		||||
            permissions = new();
 | 
			
		||||
 | 
			
		||||
            Dictionary<string, OpenApiPermissionTreeSelector> groupOpenApis = new();
 | 
			
		||||
            foreach (var item in GetDocumentNames())
 | 
			
		||||
            {
 | 
			
		||||
                OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = item ?? "Default" };
 | 
			
		||||
                groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var apiDescriptions = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items;
 | 
			
		||||
 | 
			
		||||
            foreach (var item1 in apiDescriptions)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var item in item1.Items)
 | 
			
		||||
                {
 | 
			
		||||
                    if (item.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
 | 
			
		||||
                    {
 | 
			
		||||
                        OpenApiPermissionTreeSelector openApiPermissionTreeSelector = new() { ApiName = controllerActionDescriptor.ControllerName ?? "Default" };
 | 
			
		||||
                        groupOpenApis.TryAdd(openApiPermissionTreeSelector.ApiName, openApiPermissionTreeSelector);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 获取所有需要数据权限的控制器
 | 
			
		||||
            var controllerTypes =
 | 
			
		||||
                App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(RolePermissionAttribute), false));
 | 
			
		||||
 | 
			
		||||
            foreach (var groupOpenApi in groupOpenApis)
 | 
			
		||||
            //foreach (var groupOpenApi in groupOpenApis)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                foreach (var apiDescriptionGroup in apiDescriptions)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    var routes = apiDescriptionGroup.Items.Where(api => api.ActionDescriptor is ControllerActionDescriptor);
 | 
			
		||||
 | 
			
		||||
                    OpenApiPermissionTreeSelector openApiPermissionTreeSelector = groupOpenApi.Value;
 | 
			
		||||
 | 
			
		||||
                    Dictionary<string, OpenApiPermissionTreeSelector> openApiPermissionTreeSelectorDict = new();
 | 
			
		||||
 | 
			
		||||
                    foreach (var route in routes)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!SpecificationDocumentBuilder.CheckApiDescriptionInCurrentGroup(groupOpenApi.Key, route))
 | 
			
		||||
                        {
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var actionDesc = (ControllerActionDescriptor)route.ActionDescriptor;
 | 
			
		||||
                        if (!actionDesc.ControllerTypeInfo.CustomAttributes.Any(a => a.AttributeType == typeof(RolePermissionAttribute)))
 | 
			
		||||
                            continue;
 | 
			
		||||
@@ -116,10 +109,8 @@ internal sealed class ApiPermissionService : IApiPermissionService
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    openApiPermissionTreeSelector.Children.AddRange(openApiPermissionTreeSelectorDict.Values);
 | 
			
		||||
 | 
			
		||||
                    if (openApiPermissionTreeSelector.Children.Any(a => a.Children.Count > 0))
 | 
			
		||||
                        permissions.Add(openApiPermissionTreeSelector);
 | 
			
		||||
                    if (openApiPermissionTreeSelectorDict.Values.Any(a => a.Children.Count > 0))
 | 
			
		||||
                        permissions.AddRange(openApiPermissionTreeSelectorDict.Values);
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
@@ -72,7 +71,7 @@ public class AppService : IAppService
 | 
			
		||||
    }
 | 
			
		||||
    public ClaimsPrincipal? User => App.User;
 | 
			
		||||
 | 
			
		||||
    public IPAddress? RemoteIpAddress => App.HttpContext?.Connection?.RemoteIpAddress;
 | 
			
		||||
    public string? RemoteIpAddress => App.HttpContext?.GetRemoteIpAddressToIPv4();
 | 
			
		||||
 | 
			
		||||
    public int LocalPort => App.HttpContext.Connection.LocalPort;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
@@ -24,7 +23,7 @@ public class HybridAppService : IAppService
 | 
			
		||||
    {
 | 
			
		||||
        var str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0";
 | 
			
		||||
        ClientInfo = Parser.GetDefault().Parse(str);
 | 
			
		||||
        RemoteIpAddress = IPAddress.Parse("127.0.0.1");
 | 
			
		||||
        RemoteIpAddress = "127.0.0.1";
 | 
			
		||||
    }
 | 
			
		||||
    public ClientInfo? ClientInfo { get; }
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +55,7 @@ public class HybridAppService : IAppService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IPAddress? RemoteIpAddress { get; }
 | 
			
		||||
    public string? RemoteIpAddress { get; }
 | 
			
		||||
 | 
			
		||||
    public string GetReturnUrl(string returnUrl)
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
using UAParser;
 | 
			
		||||
@@ -31,7 +30,7 @@ public interface IAppService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// RemoteIpAddress
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IPAddress? RemoteIpAddress { get; }
 | 
			
		||||
    public string? RemoteIpAddress { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// GetReturnUrl
 | 
			
		||||
 
 | 
			
		||||
@@ -96,16 +96,16 @@ public class AuthService : IAuthService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task LoginOutAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (UserManager.UserId == 0)
 | 
			
		||||
        if (UserManager.VerificatId == 0)
 | 
			
		||||
            return;
 | 
			
		||||
        var verificatId = UserManager.UserId;
 | 
			
		||||
        var verificatId = UserManager.VerificatId;
 | 
			
		||||
        //获取用户信息
 | 
			
		||||
        var userinfo = await _sysUserService.GetUserByAccountAsync(UserManager.UserAccount, UserManager.TenantId).ConfigureAwait(false);
 | 
			
		||||
        if (userinfo != null)
 | 
			
		||||
        {
 | 
			
		||||
            var loginEvent = new LoginEvent
 | 
			
		||||
            {
 | 
			
		||||
                Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(),
 | 
			
		||||
                Ip = _appService.RemoteIpAddress,
 | 
			
		||||
                SysUser = userinfo,
 | 
			
		||||
                VerificatId = verificatId
 | 
			
		||||
            };
 | 
			
		||||
@@ -236,7 +236,7 @@ public class AuthService : IAuthService
 | 
			
		||||
        //登录事件参数
 | 
			
		||||
        var logingEvent = new LoginEvent
 | 
			
		||||
        {
 | 
			
		||||
            Ip = _appService.RemoteIpAddress?.MapToIPv4()?.ToString(),
 | 
			
		||||
            Ip = _appService.RemoteIpAddress,
 | 
			
		||||
            Device = App.GetService<IAppService>().ClientInfo?.OS?.ToString(),
 | 
			
		||||
            Expire = expire,
 | 
			
		||||
            SysUser = sysUser,
 | 
			
		||||
 
 | 
			
		||||
@@ -47,12 +47,10 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task<bool> DeleteAsync(IEnumerable<T> models)
 | 
			
		||||
    public async Task<bool> DeleteAsync(IEnumerable<T> models)
 | 
			
		||||
    {
 | 
			
		||||
        if (models.FirstOrDefault() is IPrimaryIdEntity)
 | 
			
		||||
            return DeleteAsync(models.Select(a => ((IPrimaryIdEntity)a).Id));
 | 
			
		||||
        else
 | 
			
		||||
            return Task.FromResult(false);
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -165,4 +163,6 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
 | 
			
		||||
    {
 | 
			
		||||
        return DbContext.Db.GetConnectionScopeWithAttr<T>().CopyNew();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -103,4 +103,46 @@ public static class DbContext
 | 
			
		||||
        Console.WriteLine("【Sql执行时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
 | 
			
		||||
        Console.WriteLine("【Sql语句】:" + msg + Environment.NewLine);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static async Task BulkCopyAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
 | 
			
		||||
    {
 | 
			
		||||
        switch (db.CurrentConnectionConfig.DbType)
 | 
			
		||||
        {
 | 
			
		||||
            case DbType.MySql:
 | 
			
		||||
            case DbType.SqlServer:
 | 
			
		||||
            case DbType.Sqlite:
 | 
			
		||||
            case DbType.Oracle:
 | 
			
		||||
            case DbType.PostgreSQL:
 | 
			
		||||
            case DbType.Dm:
 | 
			
		||||
            case DbType.MySqlConnector:
 | 
			
		||||
            case DbType.Kdbndp:
 | 
			
		||||
                await db.Fastest<TITEM>().PageSize(size).BulkCopyAsync(datas).ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                await db.Insertable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    public static async Task BulkUpdateAsync<TITEM>(this SqlSugarClient db, List<TITEM> datas, int size) where TITEM : class, new()
 | 
			
		||||
    {
 | 
			
		||||
        switch (db.CurrentConnectionConfig.DbType)
 | 
			
		||||
        {
 | 
			
		||||
            case DbType.MySql:
 | 
			
		||||
            case DbType.SqlServer:
 | 
			
		||||
            case DbType.Sqlite:
 | 
			
		||||
            case DbType.Oracle:
 | 
			
		||||
            case DbType.PostgreSQL:
 | 
			
		||||
            case DbType.Dm:
 | 
			
		||||
            case DbType.MySqlConnector:
 | 
			
		||||
            case DbType.Kdbndp:
 | 
			
		||||
                await db.Fastest<TITEM>().PageSize(size).BulkUpdateAsync(datas).ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                await db.Updateable(datas).PageSize(size).ExecuteCommandAsync().ConfigureAwait(false);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
@@ -23,7 +24,7 @@ namespace ThingsGateway.Admin.Application;
 | 
			
		||||
[AppStartup(1000000000)]
 | 
			
		||||
public class Startup : AppStartup
 | 
			
		||||
{
 | 
			
		||||
    public void ConfigureAdminApp(IServiceCollection services)
 | 
			
		||||
    public void Configure(IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        Directory.CreateDirectory("DB");
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +76,7 @@ public class Startup : AppStartup
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void UseAdminCore(IServiceProvider serviceProvider)
 | 
			
		||||
    public void Use(IApplicationBuilder applicationBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        //检查ConfigId
 | 
			
		||||
        var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
 | 
			
		||||
 
 | 
			
		||||
@@ -18,11 +18,10 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.2" />
 | 
			
		||||
		<!--<PackageReference Include="MiniExcel" Version="1.39.0" />-->
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.4" />
 | 
			
		||||
		<PackageReference Include="UAParser" Version="3.1.47" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore" Version="5.1.4.188" />
 | 
			
		||||
		<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
 | 
			
		||||
@@ -31,9 +30,9 @@
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Threading.RateLimiting" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Threading.RateLimiting" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<Content Remove="SeedData\Admin\*.json" />
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ public partial class SessionPage
 | 
			
		||||
        {
 | 
			
		||||
            var op = new DialogOption()
 | 
			
		||||
            {
 | 
			
		||||
                IsScrolling = false,
 | 
			
		||||
                IsScrolling = true,
 | 
			
		||||
                Title = Localizer[nameof(VerificatInfo)],
 | 
			
		||||
                ShowMaximizeButton = true,
 | 
			
		||||
                Class = "dialog-table",
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace ThingsGateway.Admin.Razor;
 | 
			
		||||
[AppStartup(-1)]
 | 
			
		||||
public class Startup : AppStartup
 | 
			
		||||
{
 | 
			
		||||
    public void ConfigureAdminApp(IServiceCollection services)
 | 
			
		||||
    public void Configure(IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddScoped<IMenuService, MenuService>();
 | 
			
		||||
        services.AddBootstrapBlazorTableExportService();
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,10 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.16" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,9 @@
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection;
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
 | 
			
		||||
using Microsoft.AspNetCore.HttpOverrides;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.Controllers;
 | 
			
		||||
using Microsoft.AspNetCore.StaticFiles;
 | 
			
		||||
@@ -18,6 +21,7 @@ using Microsoft.Extensions.Options;
 | 
			
		||||
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
using System.Security.Cryptography.X509Certificates;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Encodings.Web;
 | 
			
		||||
using System.Text.Unicode;
 | 
			
		||||
@@ -291,6 +295,21 @@ public class Startup : AppStartup
 | 
			
		||||
        services.AddAuthorizationCore();
 | 
			
		||||
        services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
 | 
			
		||||
        services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
 | 
			
		||||
 | 
			
		||||
#if NET9_0_OR_GREATER
 | 
			
		||||
        var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
 | 
			
		||||
#else
 | 
			
		||||
        var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
 | 
			
		||||
#endif
 | 
			
		||||
        services.AddDataProtection()
 | 
			
		||||
            .PersistKeysToFileSystem(new DirectoryInfo("../keys"))
 | 
			
		||||
            .ProtectKeysWithCertificate(certificate)
 | 
			
		||||
            .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
 | 
			
		||||
            {
 | 
			
		||||
                EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
 | 
			
		||||
                ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -298,9 +317,9 @@ public class Startup : AppStartup
 | 
			
		||||
    public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
 | 
			
		||||
    {
 | 
			
		||||
        var app = (WebApplication)applicationBuilder;
 | 
			
		||||
        app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
 | 
			
		||||
        app.UseBootstrapBlazor();
 | 
			
		||||
 | 
			
		||||
        app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
 | 
			
		||||
 | 
			
		||||
        // 启用本地化
 | 
			
		||||
        var option = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<!--安装服务守护-->
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
@@ -54,8 +54,8 @@
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
@@ -72,6 +72,9 @@
 | 
			
		||||
		<None Update="pm2-linux.json">
 | 
			
		||||
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
		</None>
 | 
			
		||||
		<None Update="ThingsGateway.pfx">
 | 
			
		||||
		  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
		</None>
 | 
			
		||||
		<None Update="thingsgateway.service">
 | 
			
		||||
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
		</None>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/Admin/ThingsGateway.AdminServer/ThingsGateway.pfx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/Admin/ThingsGateway.AdminServer/ThingsGateway.pfx
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -106,7 +106,7 @@ public static class HttpContextExtensions
 | 
			
		||||
    /// <param name="context"></param>
 | 
			
		||||
    /// <param name="xff">是否优先取 X-Forwarded-For</param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = false)
 | 
			
		||||
    public static string GetRemoteIpAddressToIPv4(this HttpContext context, bool xff = true)
 | 
			
		||||
    {
 | 
			
		||||
        var ipv4 = context.Connection.RemoteIpAddress?.MapToIPv4()?.ToString();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,7 @@ internal sealed class ScheduleHostedService : BackgroundService
 | 
			
		||||
            await BackgroundProcessing(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _logger.LogCritical($"Schedule hosted service is stopped.");
 | 
			
		||||
        _logger.LogInformation($"Schedule hosted service is stopped.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -43,18 +43,18 @@
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.16" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.1" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="8.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.MetadataLoadContext" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="System.Text.Json" Version="9.0.5" />
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ public class TimeTick
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 上次触发时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTime LastTime { get; private set; } = DateTime.Now;
 | 
			
		||||
    public DateTime LastTime { get; private set; } = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否触发时间刻度
 | 
			
		||||
@@ -62,7 +62,7 @@ public class TimeTick
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DateTime GetNextTime(DateTime currentTime, bool setLastTime = true)
 | 
			
		||||
    public DateTime GetNextTime(DateTime currentTime, bool setLastTime = false)
 | 
			
		||||
    {
 | 
			
		||||
        // 在没有 Cron 表达式的情况下,使用固定间隔
 | 
			
		||||
        if (cron == null)
 | 
			
		||||
@@ -86,7 +86,7 @@ public class TimeTick
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DateTime GetNextTime(bool setLastTime = true) => GetNextTime(DateTime.UtcNow, setLastTime);
 | 
			
		||||
    public DateTime GetNextTime(bool setLastTime = false) => GetNextTime(DateTime.UtcNow, setLastTime);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否到达设置的时间间隔
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@
 | 
			
		||||
 | 
			
		||||
	
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.WebView" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.WebView" Version="8.0.16" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.WebView" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.WebView" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,34 @@ public static class JSRuntimeExtensions
 | 
			
		||||
    /// 获取文化信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jsRuntime"></param>
 | 
			
		||||
    public static ValueTask<string> GetCulture(this IJSRuntime jsRuntime) => jsRuntime.InvokeAsync<string>("getCultureLocalStorage");
 | 
			
		||||
    public static async ValueTask<string> GetCulture(this IJSRuntime jsRuntime)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return await jsRuntime.InvokeAsync<string>("getCultureLocalStorage");
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 设置文化信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jsRuntime"></param>
 | 
			
		||||
    /// <param name="cultureName"></param>
 | 
			
		||||
    public static ValueTask SetCulture(this IJSRuntime jsRuntime, string cultureName) => jsRuntime.InvokeVoidAsync("setCultureLocalStorage", cultureName);
 | 
			
		||||
    public static async ValueTask SetCulture(this IJSRuntime jsRuntime, string cultureName)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await jsRuntime.InvokeVoidAsync("setCultureLocalStorage", cultureName);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
@@ -48,7 +49,7 @@ public class Startup : AppStartup
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public void UseService(IServiceProvider serviceProvider)
 | 
			
		||||
    public void Use(IApplicationBuilder applicationBuilder)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.2" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.5.7" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.6.2-beta05" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.4.20</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.4.20</ProPluginVersion>
 | 
			
		||||
		<PluginVersion>10.5.19</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.5.19</ProPluginVersion>
 | 
			
		||||
		<AuthenticationVersion>2.1.7</AuthenticationVersion>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.8.27" />
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.9.6" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,9 @@ namespace ThingsGateway.Debug;
 | 
			
		||||
[AppStartup(100000000)]
 | 
			
		||||
public class Startup : AppStartup
 | 
			
		||||
{
 | 
			
		||||
    public void ConfigureAdminApp(IServiceCollection services)
 | 
			
		||||
    public void Configure(IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        Foundation.LocalizerUtil.SetLocalizerFactory((a) => App.CreateLocalizerByType(a));
 | 
			
		||||
        services.AddScoped<IPlatformService, PlatformService>();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -94,11 +94,11 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
 | 
			
		||||
        return WaitLocks.GetOrAdd(key, (a) => new WaitLock(WaitLock.MaxCount));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task StopAsync()
 | 
			
		||||
    public override Task<Result> StopAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        WaitLocks.ForEach(a => a.Value.SafeDispose());
 | 
			
		||||
        WaitLocks.Clear();
 | 
			
		||||
        return base.StopAsync();
 | 
			
		||||
        return base.StopAsync(token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ConcurrentDictionary<EndPoint, WaitLock> _waitLocks = new();
 | 
			
		||||
 
 | 
			
		||||
@@ -128,17 +128,17 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
 | 
			
		||||
    public Protocol Protocol => new Protocol("Other");
 | 
			
		||||
 | 
			
		||||
    public DateTime LastReceivedTime { get; private set; }
 | 
			
		||||
    public DateTimeOffset LastReceivedTime { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public DateTime LastSentTime { get; private set; }
 | 
			
		||||
    public DateTimeOffset LastSentTime { get; private set; }
 | 
			
		||||
 | 
			
		||||
    public bool IsClient => true;
 | 
			
		||||
 | 
			
		||||
    public bool Online => true;
 | 
			
		||||
 | 
			
		||||
    public Task CloseAsync(string msg)
 | 
			
		||||
    public Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
        return Task.FromResult(Result.Success);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
 | 
			
		||||
            {
 | 
			
		||||
                if (HeartbeatByte.SequenceEqual(e.ByteBlock.AsSegment(0, len)))
 | 
			
		||||
                {
 | 
			
		||||
                    if (DateTime.UtcNow - socket.LastSentTime.ToUniversalTime() < TimeSpan.FromMilliseconds(200))
 | 
			
		||||
                    if (DateTimeOffset.Now - socket.LastSentTime < TimeSpan.FromMilliseconds(200))
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(200).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,10 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
        set
 | 
			
		||||
        {
 | 
			
		||||
            _heartbeat = value;
 | 
			
		||||
            HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
 | 
			
		||||
            if (!_heartbeat.IsNullOrEmpty())
 | 
			
		||||
            {
 | 
			
		||||
                HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private string _heartbeat;
 | 
			
		||||
@@ -59,6 +62,8 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
            return;//此处可判断,如果为服务器,则不用使用心跳。
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        HeartbeatTime = Math.Max(HeartbeatTime, 1000);
 | 
			
		||||
 | 
			
		||||
        if (DtuId.IsNullOrWhiteSpace()) return;
 | 
			
		||||
 | 
			
		||||
        if (client is ITcpClient tcpClient)
 | 
			
		||||
@@ -81,7 +86,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
 | 
			
		||||
 | 
			
		||||
                         try
 | 
			
		||||
                         {
 | 
			
		||||
                             if (DateTime.UtcNow - tcpClient.LastSentTime.ToUniversalTime() < TimeSpan.FromMilliseconds(200))
 | 
			
		||||
                             if (DateTimeOffset.Now - tcpClient.LastSentTime < TimeSpan.FromMilliseconds(200))
 | 
			
		||||
                             {
 | 
			
		||||
                                 await Task.Delay(200).ConfigureAwait(false);
 | 
			
		||||
                             }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,13 +70,13 @@ public static class PluginUtil
 | 
			
		||||
        {
 | 
			
		||||
            action += a =>
 | 
			
		||||
            {
 | 
			
		||||
                a.UseCheckClear()
 | 
			
		||||
                a.UseTcpSessionCheckClear()
 | 
			
		||||
        .SetCheckClearType(CheckClearType.All)
 | 
			
		||||
        .SetTick(TimeSpan.FromMilliseconds(channelOptions.CheckClearTime))
 | 
			
		||||
        .SetOnClose(async (c, t) =>
 | 
			
		||||
        {
 | 
			
		||||
            await c.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
 | 
			
		||||
            c.SafeClose($"{channelOptions.CheckClearTime}ms Timeout");
 | 
			
		||||
            await c.CloseAsync($"{channelOptions.CheckClearTime}ms Timeout").ConfigureAwait(false);
 | 
			
		||||
        });
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -74,8 +74,9 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task CloseAsync(string msg)
 | 
			
		||||
    public override async Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (Online)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
@@ -83,11 +84,12 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
                //await _connectLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                if (Online)
 | 
			
		||||
                {
 | 
			
		||||
                    await base.CloseAsync(msg).ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
 | 
			
		||||
                    if (!Online)
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.OnChannelEvent(Stoped).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
@@ -95,6 +97,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
                //_connectLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return Result.Success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
 | 
			
		||||
    //private readonly WaitLock _connectLock = new WaitLock();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task CloseAsync(string msg)
 | 
			
		||||
    public override async Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (Online)
 | 
			
		||||
        {
 | 
			
		||||
@@ -81,11 +81,12 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
                //await _connectLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                if (Online)
 | 
			
		||||
                {
 | 
			
		||||
                    await base.CloseAsync(msg).ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
 | 
			
		||||
                    if (!Online)
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.OnChannelEvent(Stoped).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
@@ -93,6 +94,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
                //_connectLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return Result.Success;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -94,22 +94,22 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task StopAsync()
 | 
			
		||||
    public override async Task<Result> StopAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (Monitors.Any())
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _connectLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                await _connectLock.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
                if (Monitors.Any())
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    await ClearAsync().ConfigureAwait(false);
 | 
			
		||||
                    var iPHost = Monitors.FirstOrDefault()?.Option.IpHost;
 | 
			
		||||
                    await base.StopAsync().ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.StopAsync(token).ConfigureAwait(false);
 | 
			
		||||
                    if (!Monitors.Any())
 | 
			
		||||
                        Logger?.Info($"{iPHost}{DefaultResource.Localizer["ServiceStoped"]}");
 | 
			
		||||
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
@@ -120,8 +120,10 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await base.StopAsync().ConfigureAwait(false);
 | 
			
		||||
            var result = await base.StopAsync(token).ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
        return Result.Success; ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -192,9 +194,9 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task CloseAsync(string msg)
 | 
			
		||||
    public Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        return StopAsync();
 | 
			
		||||
        return StopAsync(token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -64,10 +64,10 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    public virtual WaitLock GetLock(string key) => WaitLock;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override Task CloseAsync(string msg)
 | 
			
		||||
    public override Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        return base.CloseAsync(msg);
 | 
			
		||||
        return base.CloseAsync(msg, token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,9 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task CloseAsync(string msg)
 | 
			
		||||
    public Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        return StopAsync();
 | 
			
		||||
        return StopAsync(token);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -127,26 +127,28 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public override async Task StopAsync()
 | 
			
		||||
    public override async Task<Result> StopAsync(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        if (Monitor != null)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await _connectLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
                await _connectLock.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
                if (Monitor != null)
 | 
			
		||||
                {
 | 
			
		||||
                    await this.OnChannelEvent(Stoping).ConfigureAwait(false);
 | 
			
		||||
                    await base.StopAsync().ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.StopAsync(token).ConfigureAwait(false);
 | 
			
		||||
                    if (Monitor == null)
 | 
			
		||||
                    {
 | 
			
		||||
                        await this.OnChannelEvent(Stoped).ConfigureAwait(false);
 | 
			
		||||
                        Logger?.Info($"{DefaultResource.Localizer["ServiceStoped"]}");
 | 
			
		||||
                    }
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await base.StopAsync().ConfigureAwait(false);
 | 
			
		||||
                    var result = await base.StopAsync(token).ConfigureAwait(false);
 | 
			
		||||
                    return result;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
@@ -156,7 +158,8 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            await base.StopAsync().ConfigureAwait(false);
 | 
			
		||||
            var result = await base.StopAsync(token).ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,9 +46,9 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
                return;
 | 
			
		||||
            if (channel.Collects.Count > 0)
 | 
			
		||||
            {
 | 
			
		||||
                var device = channel.Collects.First();
 | 
			
		||||
                if (device.GetType() != GetType())
 | 
			
		||||
                    throw new InvalidOperationException("The channel already exists in the device of another type");
 | 
			
		||||
                //var device = channel.Collects.First();
 | 
			
		||||
                //if (device.GetType() != GetType())
 | 
			
		||||
                //    throw new InvalidOperationException("The channel already exists in the device of another type");
 | 
			
		||||
 | 
			
		||||
                if (!SupportMultipleDevice())
 | 
			
		||||
                    throw new InvalidOperationException("The proactive response device does not support multiple devices");
 | 
			
		||||
@@ -97,22 +97,24 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
            Channel.Stoping.Add(ChannelStoping);
 | 
			
		||||
            Channel.Started.Add(ChannelStarted);
 | 
			
		||||
            Channel.ChannelReceived.Add(ChannelReceived);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            SetChannel();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected virtual void SetChannel()
 | 
			
		||||
    {
 | 
			
		||||
        Channel.ChannelOptions.MaxConcurrentCount = 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    ~DeviceBase()
 | 
			
		||||
    {
 | 
			
		||||
        this.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 属性
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int SendDelayTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int Timeout { get; set; } = 3000;
 | 
			
		||||
    #region
 | 
			
		||||
 | 
			
		||||
    private ILog? _deviceLogger;
 | 
			
		||||
 | 
			
		||||
@@ -135,6 +137,18 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
    public bool OnLine => Channel.Online;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    #region 属性
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int SendDelayTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual int Timeout { get; set; } = 3000;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// <inheritdoc cref="IThingsGatewayBitConverter.IsStringReverseByteWord"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -221,6 +235,14 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            if (Channel?.Collects?.Count > 1)
 | 
			
		||||
            {
 | 
			
		||||
                var dataHandlingAdapter = GetDataAdapter();
 | 
			
		||||
                if (adapter.GetType() != dataHandlingAdapter.GetType())
 | 
			
		||||
                {
 | 
			
		||||
                    clientChannel.SetDataHandlingAdapter(dataHandlingAdapter);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -546,7 +568,7 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            waitLock.Release();
 | 
			
		||||
            clientChannel.WaitHandlePool.Destroy(waitData);
 | 
			
		||||
            clientChannel.WaitHandlePool.Destroy(sign);
 | 
			
		||||
            Channel.ChannelReceivedWaitDict.TryRemove(sign, out _);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -944,7 +966,7 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
                        if (tcpServiceChannel.TryGetClient($"ID={dtu.DtuId}", out var client))
 | 
			
		||||
                        {
 | 
			
		||||
                            client.WaitHandlePool?.SafeDispose();
 | 
			
		||||
                            client.SafeClose();
 | 
			
		||||
                            _ = client.CloseAsync();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -150,6 +150,13 @@ public static class LoggerExtensions
 | 
			
		||||
        logger.Log(TouchSocket.Core.LogLevel.Info, null, msg, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 输出提示日志
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static void LogDebug(this ILog logger, string msg)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Log(TouchSocket.Core.LogLevel.Debug, null, msg, null);
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 输出Trace日志
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -83,9 +83,20 @@
 | 
			
		||||
  "ThingsGateway.Foundation.ConverterConfig": {
 | 
			
		||||
    "DataFormat": "DataFormat",
 | 
			
		||||
    "Encoding": "Encoding",
 | 
			
		||||
    "EncodingName": "字符串编码",
 | 
			
		||||
    "EncodingName": "EncodingName",
 | 
			
		||||
    "VariableStringLength": "VariableStringLength",
 | 
			
		||||
    "Stringlength": "Stringlength",
 | 
			
		||||
    "BcdFormat": "BcdFormat格式"
 | 
			
		||||
    "BcdFormat": "BcdFormat"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Foundation.DeviceBase": {
 | 
			
		||||
 | 
			
		||||
    "SendDelayTime": "SendDelayTime",
 | 
			
		||||
    "DataFormat": "DataFormat",
 | 
			
		||||
    "Timeout": "Timeout",
 | 
			
		||||
    "IsStringReverseByteWord": "IsStringReverseByteWord"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Foundation.DtuServiceDeviceBase": {
 | 
			
		||||
    "DtuId": "DtuId(UTF8)"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,18 @@
 | 
			
		||||
    "VariableStringLength": "变长字符串",
 | 
			
		||||
    "Stringlength": "字符串长度",
 | 
			
		||||
    "BcdFormat": "BCD格式"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Foundation.DeviceBase": {
 | 
			
		||||
 | 
			
		||||
    "SendDelayTime": "发送延时",
 | 
			
		||||
    "DataFormat": "解析规则",
 | 
			
		||||
    "Timeout": "读写超时",
 | 
			
		||||
    "IsStringReverseByteWord": "字符串反转"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Foundation.DtuServiceDeviceBase": {
 | 
			
		||||
    "DtuId": "Dtu注册包(UTF8)"
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
@@ -9,9 +9,9 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="TouchSocket" Version="3.0.25" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.SerialPorts" Version="3.0.25" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="TouchSocket" Version="3.1.2" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.2" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,12 +17,12 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.14" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.16" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.16" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.5" />
 | 
			
		||||
		<PackageReference Include="Microsoft.AspNetCore.Components" Version="9.0.5" />
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,7 @@ public class AsyncReadWriteLock
 | 
			
		||||
 | 
			
		||||
    private void ReleaseWriter()
 | 
			
		||||
    {
 | 
			
		||||
        Interlocked.Decrement(ref _writerCount);
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Read(ref _writerCount) == 0)
 | 
			
		||||
        if (Interlocked.Decrement(ref _writerCount) == 0)
 | 
			
		||||
        {
 | 
			
		||||
            var resetEvent = _readerLock;
 | 
			
		||||
            _readerLock = new(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -125,7 +125,9 @@ internal sealed class InternalTableColumn(string fieldName, Type fieldType, stri
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? RequiredErrorMessage { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public bool IsFixedSearchWhenSelect { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    public int Cols { get; set; }
 | 
			
		||||
 | 
			
		||||
    public bool? IgnoreWhenExport { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ using BootstrapBlazor.Components;
 | 
			
		||||
using Mapster;
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
@@ -35,6 +36,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[Route("openApi/control")]
 | 
			
		||||
[RolePermission]
 | 
			
		||||
[LoggingMonitor]
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class ControlController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
@@ -139,7 +141,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await GlobalData.RpcService.InvokeDeviceMethodAsync($"WebApi-{UserManager.UserAccount}-{App.HttpContext.Connection.RemoteIpAddress.MapToIPv4()}", deviceDatas).ConfigureAwait(false);
 | 
			
		||||
        return await GlobalData.RpcService.InvokeDeviceMethodAsync($"WebApi-{UserManager.UserAccount}-{App.HttpContext?.GetRemoteIpAddressToIPv4()}", deviceDatas).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[ApiDescriptionSettings("ThingsGateway.OpenApi", Order = 200)]
 | 
			
		||||
[DisplayName("数据状态")]
 | 
			
		||||
[Route("openApi/runtimeInfo")]
 | 
			
		||||
[ApiController]
 | 
			
		||||
[RolePermission]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class RuntimeInfoController : ControllerBase
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ public abstract class BusinessBaseWithCacheAlarmModel<VarModel, DevModel, AlarmM
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(DevModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
@@ -100,6 +101,7 @@ public abstract class BusinessBaseWithCacheAlarmModel<VarModel, DevModel, AlarmM
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryAlarmModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(AlarmModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryAlarmModelQueue.Clear();
 | 
			
		||||
                    _memoryAlarmModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ public abstract class BusinessBaseWithCacheDeviceModel<VarModel, DevModel> : Bus
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(DevModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
@@ -101,6 +102,7 @@ public abstract class BusinessBaseWithCacheDeviceModel<VarModel, DevModel> : Bus
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryDevModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(DevModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryDevModelQueue.Clear();
 | 
			
		||||
                    _memoryDevModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ public abstract class BusinessBaseWithCacheVariableModel<VarModel> : BusinessBas
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                LogMessage?.LogInformation($"Add {typeof(VarModel).Name} data to file cache, count {data.Count}");
 | 
			
		||||
                foreach (var item in data)
 | 
			
		||||
                {
 | 
			
		||||
                    item.Id = CommonUtils.GetSingleId();
 | 
			
		||||
@@ -147,6 +148,7 @@ public abstract class BusinessBaseWithCacheVariableModel<VarModel> : BusinessBas
 | 
			
		||||
            {
 | 
			
		||||
                if (_memoryVarModelQueue.Count > _businessPropertyWithCache.QueueMaxCount)
 | 
			
		||||
                {
 | 
			
		||||
                    LogMessage?.LogWarning($"{typeof(VarModel).Name} Queue exceeds limit, clear old data. If it doesn't work as expected, increase [QueueMaxCount] or Enable cache");
 | 
			
		||||
                    _memoryVarModelQueue.Clear();
 | 
			
		||||
                    _memoryVarModelQueue.Enqueue(data);
 | 
			
		||||
                    return;
 | 
			
		||||
 
 | 
			
		||||
@@ -38,4 +38,11 @@ public class BusinessPropertyWithCache : BusinessPropertyBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual int QueueMaxCount { get; set; } = 100000;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual bool OnlineFilter { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public bool GroupUpdate { get; set; } = false;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,12 +87,12 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
        // 触发一次设备状态变化和变量值变化事件
 | 
			
		||||
        CollectDevices?.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine)
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                DeviceStatusChange(a.Value, a.Value.Adapt<DeviceBasicData>());
 | 
			
		||||
        });
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline)
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -162,6 +162,8 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
                {
 | 
			
		||||
                    if (_exTTimerTick.IsTickHappen())
 | 
			
		||||
                    {
 | 
			
		||||
                        if (LogMessage.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                            LogMessage?.LogDebug($"Interval {typeof(VarModel).Name} data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                        // 间隔推送全部变量
 | 
			
		||||
                        var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                        VariableTimeInterval(variableRuntimes, variableRuntimes.Adapt<List<VariableBasicData>>());
 | 
			
		||||
@@ -177,6 +179,8 @@ public abstract class BusinessBaseWithCacheIntervalAlarmModel<VarModel, DevModel
 | 
			
		||||
                    {
 | 
			
		||||
                        if (CollectDevices != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (LogMessage.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                                LogMessage?.LogDebug($"Interval {typeof(DevModel).Name} data, count {CollectDevices.Count}");
 | 
			
		||||
 | 
			
		||||
                            // 间隔推送全部设备
 | 
			
		||||
                            foreach (var deviceRuntime in CollectDevices.Select(a => a.Value))
 | 
			
		||||
 
 | 
			
		||||
@@ -85,12 +85,12 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode
 | 
			
		||||
 | 
			
		||||
        CollectDevices?.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine)
 | 
			
		||||
            if (a.Value.DeviceStatus == DeviceStatusEnum.OnLine && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                DeviceStatusChange(a.Value, a.Value.Adapt<DeviceBasicData>());
 | 
			
		||||
        });
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline)
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -152,6 +152,8 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode
 | 
			
		||||
                {
 | 
			
		||||
                    if (_exTTimerTick.IsTickHappen())
 | 
			
		||||
                    {
 | 
			
		||||
                        if (LogMessage.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                            LogMessage?.LogDebug($"Interval  {typeof(VarModel).Name}  data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                        // 上传所有变量信息
 | 
			
		||||
                        var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                        VariableTimeInterval(variableRuntimes, variableRuntimes.Adapt<List<VariableBasicData>>());
 | 
			
		||||
@@ -168,6 +170,8 @@ public abstract class BusinessBaseWithCacheIntervalDeviceModel<VarModel, DevMode
 | 
			
		||||
                    {
 | 
			
		||||
                        if (CollectDevices != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (LogMessage.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                                LogMessage?.LogDebug($"Interval  {typeof(DevModel).Name}  data, count {CollectDevices.Count}");
 | 
			
		||||
                            // 上传所有设备信息
 | 
			
		||||
                            foreach (var deviceRuntime in CollectDevices.Select(a => a.Value))
 | 
			
		||||
                            {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,9 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Text.Encodings.Web;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Json.Extension;
 | 
			
		||||
@@ -37,7 +40,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            List<string> strings = new List<string>();
 | 
			
		||||
 | 
			
		||||
            // 使用正则表达式查找输入字符串中的所有匹配项
 | 
			
		||||
            Regex regex = new Regex(@"\$\{(.+?)\}");
 | 
			
		||||
            Regex regex = TopicRegex();
 | 
			
		||||
            MatchCollection matches = regex.Matches(input);
 | 
			
		||||
 | 
			
		||||
            // 遍历匹配结果,将匹配到的字符串添加到列表中
 | 
			
		||||
@@ -63,7 +66,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
    protected List<TopicJson> GetAlarms(IEnumerable<AlarmModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        List<TopicJson> topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -86,10 +89,12 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                    // 上传内容
 | 
			
		||||
                    if (_businessPropertyWithCacheIntervalScript.IsAlarmList)
 | 
			
		||||
                    {
 | 
			
		||||
 | 
			
		||||
                        // 如果是报警列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                        string json = group.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                        var gList = group.Select(a => a).ToList();
 | 
			
		||||
                        string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                        // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                        topicJsonList.Add(new(topic, json));
 | 
			
		||||
                        topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
@@ -98,7 +103,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        {
 | 
			
		||||
                            string json = JsonExtensions.ToJsonNetString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -108,15 +113,16 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsAlarmList)
 | 
			
		||||
            {
 | 
			
		||||
                string json = data.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = JsonExtensions.ToJsonNetString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json));
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -128,7 +134,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
    protected List<TopicJson> GetDeviceData(IEnumerable<DevModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        List<TopicJson> topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -153,9 +159,10 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsDeviceList)
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果是设备列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            string json = group.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -164,7 +171,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = JsonExtensions.ToJsonNetString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -175,15 +182,16 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsDeviceList)
 | 
			
		||||
            {
 | 
			
		||||
                string json = data.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = JsonExtensions.ToJsonNetString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json));
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -193,7 +201,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
    protected List<TopicJson> GetVariable(IEnumerable<VarModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        List<TopicJson> topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -218,9 +226,10 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果是变量列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            string json = group.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -229,7 +238,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = JsonExtensions.ToJsonNetString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -240,15 +249,16 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
            {
 | 
			
		||||
                string json = data.Select(a => a).ToList().ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                string json = gList.ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = JsonExtensions.ToJsonNetString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -267,7 +277,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            data = item;
 | 
			
		||||
        }
 | 
			
		||||
        List<TopicJson> topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topicJsonList = new List<TopicJson>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -291,10 +301,11 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        // 上传内容
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
                        {
 | 
			
		||||
 | 
			
		||||
                            // 如果是变量列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            string json = group.Select(a => a).GroupBy(a => a.DeviceName, b => b).ToDictionary(a => a.Key, b => b.ToList()).ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicJsonList.Add(new(topic, json, group.Count()));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
@@ -303,7 +314,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                            {
 | 
			
		||||
                                string json = JsonExtensions.ToJsonNetString(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicJsonList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -315,24 +326,58 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
            {
 | 
			
		||||
                string json = data.Select(a => a).GroupBy(a => a.DeviceName, b => b).ToDictionary(a => a.Key, b => b.ToList()).ToJsonNetString(_businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, data.Count()));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    string json = JsonExtensions.ToJsonNetString(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //protected static byte[] Serialize(object value)
 | 
			
		||||
    //{
 | 
			
		||||
    //    var block = new ValueByteBlock(1024 * 64);
 | 
			
		||||
    //    try
 | 
			
		||||
    //    {
 | 
			
		||||
    //        //将数据序列化到内存块
 | 
			
		||||
    //        FastBinaryFormatter.Serialize(ref block, value);
 | 
			
		||||
    //        block.SeekToStart();
 | 
			
		||||
    //        return block.Memory.GetArray().Array;
 | 
			
		||||
    //    }
 | 
			
		||||
    //    finally
 | 
			
		||||
    //    {
 | 
			
		||||
    //        block.Dispose();
 | 
			
		||||
    //    }
 | 
			
		||||
    //}
 | 
			
		||||
    protected static JsonSerializerOptions NoWriteIndentedJsonSerializerOptions = new JsonSerializerOptions
 | 
			
		||||
    {
 | 
			
		||||
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
        WriteIndented = false
 | 
			
		||||
    };
 | 
			
		||||
    protected static JsonSerializerOptions WriteIndentedJsonSerializerOptions = new JsonSerializerOptions
 | 
			
		||||
    {
 | 
			
		||||
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
 | 
			
		||||
        WriteIndented = true
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    protected static byte[] Serialize(object data, bool writeIndented)
 | 
			
		||||
    {
 | 
			
		||||
        if (data == null) return Array.Empty<byte>();
 | 
			
		||||
        byte[] payload = JsonSerializer.SerializeToUtf8Bytes(data, data.GetType(), writeIndented ? WriteIndentedJsonSerializerOptions : NoWriteIndentedJsonSerializerOptions);
 | 
			
		||||
        return payload;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetAlarmTopicArrays(IEnumerable<AlarmModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<AlarmModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel);
 | 
			
		||||
        List<TopicArray> topicJsonList = new List<TopicArray>();
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.AlarmTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -355,18 +400,19 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                    // 上传内容
 | 
			
		||||
                    if (_businessPropertyWithCacheIntervalScript.IsAlarmList)
 | 
			
		||||
                    {
 | 
			
		||||
                        var json = Serialize(group.Select(a => a).ToList().ToList());
 | 
			
		||||
                        var gList = group.Select(a => a).ToList();
 | 
			
		||||
                        var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                        // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                        topicJsonList.Add(new(topic, json));
 | 
			
		||||
                        topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        // 如果不是报警列表,则将每个分组元素分别转换为 JSON 字符串
 | 
			
		||||
                        foreach (var gro in group)
 | 
			
		||||
                        {
 | 
			
		||||
                            var json = Serialize(gro);
 | 
			
		||||
                            var json = Serialize(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -376,42 +422,27 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsAlarmList)
 | 
			
		||||
            {
 | 
			
		||||
                var json = Serialize(data.Select(a => a).ToList());
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = Serialize(group);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json));
 | 
			
		||||
                    var json = Serialize(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.AlarmTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected static ArraySegment<byte> Serialize(object value)
 | 
			
		||||
    {
 | 
			
		||||
        var block = new ValueByteBlock(1024 * 64);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            //将数据序列化到内存块
 | 
			
		||||
            FastBinaryFormatter.Serialize(ref block, value);
 | 
			
		||||
            block.SeekToStart();
 | 
			
		||||
            return block.Memory.GetArray();
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            block.Dispose();
 | 
			
		||||
        }
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetDeviceTopicArray(IEnumerable<DevModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<DevModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel);
 | 
			
		||||
        List<TopicArray> topicJsonList = new List<TopicArray>();
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.DeviceTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -436,18 +467,19 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsDeviceList)
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果是设备列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            var json = Serialize(group.Select(a => a).ToList());
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果不是设备列表,则将每个分组元素分别转换为 JSON 字符串
 | 
			
		||||
                            foreach (var gro in group)
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = Serialize(gro);
 | 
			
		||||
                                var json = Serialize(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -458,25 +490,26 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsDeviceList)
 | 
			
		||||
            {
 | 
			
		||||
                var json = Serialize(data.Select(a => a).ToList());
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = Serialize(group);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json));
 | 
			
		||||
                    var json = Serialize(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.DeviceTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetVariableTopicArray(IEnumerable<VarModel> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<dynamic>? data = Application.DynamicModelExtension.GetDynamicModel<VarModel>(item, _businessPropertyWithCacheIntervalScript.BigTextScriptVariableModel);
 | 
			
		||||
        List<TopicArray> topicJsonList = new List<TopicArray>();
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -501,18 +534,19 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果是变量列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            var json = Serialize(group.Select(a => a).ToList());
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果不是变量列表,则将每个分组元素分别转换为 JSON 字符串
 | 
			
		||||
                            foreach (var gro in group)
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = Serialize(gro);
 | 
			
		||||
                                var json = Serialize(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -523,22 +557,22 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
            {
 | 
			
		||||
                var json = Serialize(data.Select(a => a).ToList());
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = Serialize(group);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                    var json = Serialize(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected List<TopicArray> GetVariableBasicDataTopicArray(IEnumerable<VariableBasicData> item)
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<VariableBasicData>? data = null;
 | 
			
		||||
@@ -551,7 +585,7 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
            data = item;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<TopicArray> topicJsonList = new List<TopicArray>();
 | 
			
		||||
        List<TopicArray> topicArrayList = new List<TopicArray>();
 | 
			
		||||
        var topics = Match(_businessPropertyWithCacheIntervalScript.VariableTopic);
 | 
			
		||||
        if (topics.Count > 0)
 | 
			
		||||
        {
 | 
			
		||||
@@ -576,18 +610,19 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
                        if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果是变量列表,则将整个分组转换为 JSON 字符串
 | 
			
		||||
                            var json = Serialize(group.Select(a => a).ToList());
 | 
			
		||||
                            var gList = group.Select(a => a).ToList();
 | 
			
		||||
                            var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                            // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                            topicJsonList.Add(new(topic, json));
 | 
			
		||||
                            topicArrayList.Add(new(topic, json, gList.Count));
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            // 如果不是变量列表,则将每个分组元素分别转换为 JSON 字符串
 | 
			
		||||
                            foreach (var gro in group)
 | 
			
		||||
                            {
 | 
			
		||||
                                var json = Serialize(gro);
 | 
			
		||||
                                var json = Serialize(gro, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                                // 将主题和 JSON 内容添加到列表中
 | 
			
		||||
                                topicJsonList.Add(new(topic, json));
 | 
			
		||||
                                topicArrayList.Add(new(topic, json, 1));
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -598,21 +633,40 @@ public abstract partial class BusinessBaseWithCacheIntervalScript<VarModel, DevM
 | 
			
		||||
        {
 | 
			
		||||
            if (_businessPropertyWithCacheIntervalScript.IsVariableList)
 | 
			
		||||
            {
 | 
			
		||||
                var json = Serialize(data.Select(a => a).ToList());
 | 
			
		||||
                topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                var gList = data.Select(a => a).ToList();
 | 
			
		||||
                var json = Serialize(gList, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, gList.Count));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var group in data)
 | 
			
		||||
                {
 | 
			
		||||
                    var json = Serialize(group);
 | 
			
		||||
                    topicJsonList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json));
 | 
			
		||||
                    var json = Serialize(group, _businessPropertyWithCacheIntervalScript.JsonFormattingIndented);
 | 
			
		||||
                    topicArrayList.Add(new(_businessPropertyWithCacheIntervalScript.VariableTopic, json, 1));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return topicJsonList;
 | 
			
		||||
        return topicArrayList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    protected string GetDetailLogString(TopicArray topicArray, int queueCount)
 | 
			
		||||
    {
 | 
			
		||||
        if (queueCount > 0)
 | 
			
		||||
            return $"Up Topic:{topicArray.Topic}{Environment.NewLine}PayLoad:{Encoding.UTF8.GetString(topicArray.Json)} {Environment.NewLine} VarModelQueue:{queueCount}";
 | 
			
		||||
        else
 | 
			
		||||
            return $"Up Topic:{topicArray.Topic}{Environment.NewLine}PayLoad:{Encoding.UTF8.GetString(topicArray.Json)}";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected string GetCountLogString(TopicArray topicArray, int queueCount)
 | 
			
		||||
    {
 | 
			
		||||
        if (queueCount > 0)
 | 
			
		||||
            return $"Up Topic:{topicArray.Topic}{Environment.NewLine}Count:{topicArray.Count} {Environment.NewLine} VarModelQueue:{queueCount}";
 | 
			
		||||
        else
 | 
			
		||||
            return $"Up Topic:{topicArray.Topic}{Environment.NewLine}Count:{topicArray.Count}";
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    [GeneratedRegex(@"\$\{(.+?)\}")]
 | 
			
		||||
    private static partial Regex TopicRegex();
 | 
			
		||||
    #endregion 封装方法
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,10 @@ using TouchSocket.Core;
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 抽象类 <see cref="BusinessBaseWithCacheIntervalVariableModel{T}"/>,表示具有缓存间隔功能的业务基类,其中 T 代表变量模型。
 | 
			
		||||
/// 抽象类 <see cref="BusinessBaseWithCacheIntervalVariableModel{VarModel}"/>,表示具有缓存间隔功能的业务基类,其中 T 代表变量模型。
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <typeparam name="T">变量模型类型</typeparam>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalVariableModel<T> : BusinessBaseWithCacheVariableModel<T>
 | 
			
		||||
/// <typeparam name="VarModel">变量模型类型</typeparam>
 | 
			
		||||
public abstract class BusinessBaseWithCacheIntervalVariableModel<VarModel> : BusinessBaseWithCacheVariableModel<VarModel>
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 用于定时触发的时间间隔。
 | 
			
		||||
@@ -73,7 +73,7 @@ public abstract class BusinessBaseWithCacheIntervalVariableModel<T> : BusinessBa
 | 
			
		||||
        // 触发一次变量值变化事件
 | 
			
		||||
        IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
        {
 | 
			
		||||
            if (a.Value.IsOnline)
 | 
			
		||||
            if (a.Value.IsOnline && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                VariableValueChange(a.Value, a.Value.Adapt<VariableBasicData>());
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@@ -110,6 +110,8 @@ public abstract class BusinessBaseWithCacheIntervalVariableModel<T> : BusinessBa
 | 
			
		||||
                {
 | 
			
		||||
                    if (_exTTimerTick.IsTickHappen())
 | 
			
		||||
                    {
 | 
			
		||||
                        if (LogMessage.LogLevel <= LogLevel.Debug)
 | 
			
		||||
                            LogMessage?.LogDebug($"Interval  {typeof(VarModel).Name}  data, count {IdVariableRuntimes.Count}");
 | 
			
		||||
                        //间隔推送全部变量
 | 
			
		||||
                        var variableRuntimes = IdVariableRuntimes.Select(a => a.Value);
 | 
			
		||||
                        VariableTimeInterval(variableRuntimes, variableRuntimes.Adapt<List<VariableBasicData>>());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using BootstrapBlazor.Components;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// <inheritdoc/>
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class BusinessPropertyWithCacheIntervalDBScript : BusinessPropertyWithCacheInterval
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 表定义实体脚本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    [AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 3)]
 | 
			
		||||
    public string? BigTextScriptTableModel { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 数据脚本
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [AutoGenerateColumn(ComponentType = typeof(Textarea), Rows = 3)]
 | 
			
		||||
    public string? BigTextScriptDataModel { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,11 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class BusinessPropertyWithCacheIntervalScript : BusinessPropertyWithCacheInterval
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否显示详细日志
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public bool DetailLog { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 缩进格式化
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,13 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public struct TopicArray
 | 
			
		||||
{
 | 
			
		||||
    public TopicArray(string topic, ArraySegment<byte> json)
 | 
			
		||||
    public TopicArray(string topic, byte[] json, int count)
 | 
			
		||||
    {
 | 
			
		||||
        Topic = topic; Json = json;
 | 
			
		||||
        Topic = topic; Json = json; Count = count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ArraySegment<byte> Json { get; set; }
 | 
			
		||||
    public int Count { get; set; } = 1;
 | 
			
		||||
    public byte[] Json { get; set; }
 | 
			
		||||
    public string Topic { get; set; }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -12,10 +12,11 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public struct TopicJson
 | 
			
		||||
{
 | 
			
		||||
    public TopicJson(string topic, string json)
 | 
			
		||||
    public TopicJson(string topic, string json, int count)
 | 
			
		||||
    {
 | 
			
		||||
        Topic = topic; Json = json;
 | 
			
		||||
        Topic = topic; Json = json; Count = count;
 | 
			
		||||
    }
 | 
			
		||||
    public int Count { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    public string Json { get; set; }
 | 
			
		||||
    public string Topic { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,87 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件配置项
 | 
			
		||||
/// <br></br>
 | 
			
		||||
/// 使用<see cref="DynamicPropertyAttribute"/> 标识所需的配置属性
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectFoundationPackPropertyBase : CollectFoundationPropertyBase
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 最大打包长度
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public ushort MaxPack { get; set; } = 100;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
public abstract class CollectFoundationPropertyBase : CollectPropertyRetryBase
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 读写超时时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual ushort Timeout { get; set; } = 3000;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 帧前时间ms
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual int SendDelayTime { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public bool IsStringReverseByteWord { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认解析顺序
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual DataFormatEnum DataFormat { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件配置项
 | 
			
		||||
/// <br></br>
 | 
			
		||||
/// 使用<see cref="DynamicPropertyAttribute"/> 标识所需的配置属性
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectFoundationDtuPropertyBase : CollectFoundationPropertyBase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认DtuId
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public string? DtuId { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件配置项
 | 
			
		||||
/// <br></br>
 | 
			
		||||
/// 使用<see cref="DynamicPropertyAttribute"/> 标识所需的配置属性
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectFoundationDtuPackPropertyBase : CollectFoundationPackPropertyBase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 默认DtuId
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public string? DtuId { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -25,12 +25,30 @@ public abstract class CollectPropertyBase : DriverPropertyBase
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 离线后恢复运行的间隔时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual int ReIntervalTime { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 失败重试次数,默认3
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public virtual int RetryCount { get; set; } = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件配置项
 | 
			
		||||
/// <br></br>
 | 
			
		||||
/// 使用<see cref="DynamicPropertyAttribute"/> 标识所需的配置属性
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class CollectPropertyRetryBase : CollectPropertyBase
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 离线后恢复运行的间隔时间
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public override int ReIntervalTime { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 失败重试次数,默认3
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public override int RetryCount { get; set; } = 3;
 | 
			
		||||
}
 | 
			
		||||
@@ -140,7 +140,7 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 缓存超时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "缓存超时")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "缓存超时", IsNullable = true, DefaultValue = "500")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    [MinValue(100)]
 | 
			
		||||
    public override int CacheTimeout { get; set; } = 500;
 | 
			
		||||
@@ -148,7 +148,7 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 连接超时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "连接超时")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "连接超时", IsNullable = true, DefaultValue = "3000")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    [MinValue(100)]
 | 
			
		||||
    public override ushort ConnectTimeout { get; set; } = 3000;
 | 
			
		||||
@@ -156,33 +156,36 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 最大并发数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "最大并发数")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "最大并发数", IsNullable = true, DefaultValue = "1")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public override int MaxConcurrentCount { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "最大连接数")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "最大连接数", IsNullable = true, DefaultValue = "10000")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override int MaxClientCount { get; set; } = 10000;
 | 
			
		||||
    [SugarColumn(ColumnDescription = "客户端滑动过期时间")]
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "客户端滑动过期时间", IsNullable = true, DefaultValue = "120000")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override int CheckClearTime { get; set; } = 120000;
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "心跳内容", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override string Heartbeat { get; set; } = "Heartbeat";
 | 
			
		||||
 | 
			
		||||
    #region dtu终端
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "心跳间隔")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "心跳间隔", IsNullable = true, DefaultValue = "60000")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override int HeartbeatTime { get; set; } = 60000;
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "DtuId", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override string DtuId { get; set; }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(ColumnDescription = "Dtu类型")]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "Dtu类型", IsNullable = true, DefaultValue = "0")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public override DtuSeviceType DtuSeviceType { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "通道", Length = 200)]
 | 
			
		||||
    [SugarColumn(ColumnDescription = "通道")]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    [IgnoreExcel]
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[SugarTable("variable", TableDescription = "设备变量表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("index_device", nameof(Variable.DeviceId), OrderByType.Asc)]
 | 
			
		||||
[SugarIndex("unique_variable_name", nameof(Variable.Name), OrderByType.Asc, nameof(Variable.DeviceId), OrderByType.Asc, true)]
 | 
			
		||||
[SugarIndex("unique_deviceid_variable_name", nameof(Variable.Name), OrderByType.Asc, nameof(Variable.DeviceId), OrderByType.Asc, true)]
 | 
			
		||||
public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +51,13 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    [Required]
 | 
			
		||||
    public virtual string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 采集组
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "采集组", IsNullable = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true, Order = 1)]
 | 
			
		||||
    public virtual string CollectGroup { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分组名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,76 @@
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationDtuPackPropertyBase": {
 | 
			
		||||
    "DtuId": "DtuId(UTF8)"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationDtuPropertyBase": {
 | 
			
		||||
    "DtuId": "DtuId(UTF8)"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationPackPropertyBase": {
 | 
			
		||||
 | 
			
		||||
    "MaxPack": "MaxPack"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationPropertyBase": {
 | 
			
		||||
    "DataFormat": "DataFormat",
 | 
			
		||||
    "IsStringReverseByteWord": "IsStringReverseByteWord",
 | 
			
		||||
    "Timeout": "Timeout",
 | 
			
		||||
    "SendDelayTime": "SendDelayTime"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "ReIntervalTime": "ReIntervalTime",
 | 
			
		||||
    "RetryCount": "RetryCount"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyBase": {
 | 
			
		||||
    "ReIntervalTime": "ReIntervalTime",
 | 
			
		||||
    "RetryCount": "RetryCount",
 | 
			
		||||
    "ConcurrentCount": "ConcurrentCount"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCacheIntervalScript": {
 | 
			
		||||
 | 
			
		||||
    "DetailLog": "DetailLog",
 | 
			
		||||
    "JsonFormattingIndented": "JsonFormattingIndented",
 | 
			
		||||
 | 
			
		||||
    "BigTextScriptDeviceModel": "BigTextScriptDeviceModel",
 | 
			
		||||
    "BigTextScriptVariableModel": "BigTextScriptVariableModel",
 | 
			
		||||
    "BigTextScriptAlarmModel": "BigTextScriptAlarmModel",
 | 
			
		||||
 | 
			
		||||
    "IsDeviceList": "IsDeviceList",
 | 
			
		||||
    "IsVariableList": "IsVariableList",
 | 
			
		||||
    "IsAlarmList": "IsAlarmList",
 | 
			
		||||
 | 
			
		||||
    "DeviceTopic": "DeviceTopic",
 | 
			
		||||
    "VariableTopic": "VariableTopic",
 | 
			
		||||
    "AlarmTopic": "AlarmTopic"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCacheInterval": {
 | 
			
		||||
 | 
			
		||||
    "BusinessUpdateEnum": "BusinessUpdateEnum",
 | 
			
		||||
    "BusinessInterval": "BusinessInterval",
 | 
			
		||||
    "IsAllVariable": "IsAllVariable"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCache": {
 | 
			
		||||
 | 
			
		||||
    "QueueMaxCount": "QueueMaxCount",
 | 
			
		||||
    "CacheFileMaxLength": "CacheFileMaxLength",
 | 
			
		||||
    "SplitSize": "SplitSize",
 | 
			
		||||
    "CacheEnable": "CacheEnable",
 | 
			
		||||
 | 
			
		||||
    "GroupUpdate": "GroupUpdate",
 | 
			
		||||
    "OnlineFilter": "OnlineFilter"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessUpdateEnum": {
 | 
			
		||||
    "Change": "Change",
 | 
			
		||||
    "Interval": "Interval",
 | 
			
		||||
    "IntervalOrChange": "IntervalOrChange"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ControlController": {
 | 
			
		||||
@@ -30,55 +98,17 @@
 | 
			
		||||
    "ActiveTime": "ActiveTime",
 | 
			
		||||
    "DeviceVariableCount": "DeviceVariableCount",
 | 
			
		||||
    "LastErrorMessage": "LastErrorMessage",
 | 
			
		||||
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "DeviceStatus": "DeviceStatus",
 | 
			
		||||
    "RedundantType": "RedundantType",
 | 
			
		||||
    "PluginName": "PluginName",
 | 
			
		||||
    "Name.Required": " {0}  cannot be empty",
 | 
			
		||||
    "Description": "Description",
 | 
			
		||||
    "ChannelId": "Channel",
 | 
			
		||||
    "ChannelName": "ChannelName",
 | 
			
		||||
    "IntervalTime": "IntervalTime",
 | 
			
		||||
    "IntervalTime.MinValue": " {0} value is too small",
 | 
			
		||||
 | 
			
		||||
    "PluginName": "PluginName",
 | 
			
		||||
    "PluginName.Required": "{0} cannot be empty",
 | 
			
		||||
 | 
			
		||||
    "LogEnable": "LogEnable",
 | 
			
		||||
    "LogLevel": "LogLevel",
 | 
			
		||||
    "RedundantSwitchType": "RedundantSwitchType",
 | 
			
		||||
    "RedundantScanIntervalTime": "RedundantScanIntervalTime",
 | 
			
		||||
    "RedundantScript": "RedundantScript",
 | 
			
		||||
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
 | 
			
		||||
    "RedundantEnable": "RedundantEnable",
 | 
			
		||||
    "RedundantDeviceId": "RedundantDevice",
 | 
			
		||||
 | 
			
		||||
    "Remark1": "Remark1",
 | 
			
		||||
    "Remark2": "Remark2",
 | 
			
		||||
    "Remark3": "Remark3",
 | 
			
		||||
    "Remark4": "Remark4",
 | 
			
		||||
    "Remark5": "Remark5",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "RedundantDeviceNotNull": "When enabling redundancy, you must select a redundant device",
 | 
			
		||||
    "SaveDevice": "Add/Modify Device",
 | 
			
		||||
    "CopyDevice": "Copy Device",
 | 
			
		||||
    "DeleteDevice": "Delete Device",
 | 
			
		||||
    "ClearDevice": "Clear Device",
 | 
			
		||||
    "ExportDevice": "Export Device",
 | 
			
		||||
    "ImportDevice": "Import Device",
 | 
			
		||||
    "ImportNullError": "Unable to recognize",
 | 
			
		||||
    "RedundantDeviceError": "Redundant device error",
 | 
			
		||||
    "ChannelError": "Channel error",
 | 
			
		||||
    "NotNull": "{0} does not exist",
 | 
			
		||||
    "DeviceNotNull": "Device name does not exist",
 | 
			
		||||
    "NameDump": "Duplicate device name {0}",
 | 
			
		||||
    "PluginName.Required": "{0} cannot be empty"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "DeviceStatus": "DeviceStatus",
 | 
			
		||||
 | 
			
		||||
    "PluginNotNull": "Plugin name does not exist"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@@ -94,59 +124,11 @@
 | 
			
		||||
    "Value": "Value",
 | 
			
		||||
    "AlarmEnable": "AlarmEnable",
 | 
			
		||||
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Description": "Description",
 | 
			
		||||
    "Group": "Group",
 | 
			
		||||
    "DeviceId": "CollectionDevice",
 | 
			
		||||
    "DeviceId.MinValue": "{0} cannot be empty",
 | 
			
		||||
    "DeviceId.Required": "{0} cannot be empty",
 | 
			
		||||
    "IntervalTime": "IntervalTime",
 | 
			
		||||
    "IntervalTime.MinValue": "{0} value is too small",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "ProtectType": "ProtectType",
 | 
			
		||||
    "DataType": "DataType",
 | 
			
		||||
    "ReadExpressions": "ReadExpressions",
 | 
			
		||||
    "WriteExpressions": "WriteExpressions",
 | 
			
		||||
    "RpcWriteEnable": "RpcWriteEnable",
 | 
			
		||||
    "SaveValue": "SaveValue",
 | 
			
		||||
    "DynamicVariable": "DynamicVariable",
 | 
			
		||||
    "ArrayLength": "ArrayLength",
 | 
			
		||||
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
    "BoolOpenAlarmEnable": "BoolOpenAlarmEnable",
 | 
			
		||||
    "BoolOpenRestrainExpressions": "BoolOpenRestrainExpressions",
 | 
			
		||||
    "BoolOpenAlarmText": "BoolOpenAlarmText",
 | 
			
		||||
    "BoolCloseAlarmEnable": "BoolCloseAlarmEnable",
 | 
			
		||||
    "BoolCloseRestrainExpressions": "BoolCloseRestrainExpressions",
 | 
			
		||||
    "BoolCloseAlarmText": "BoolCloseAlarmText",
 | 
			
		||||
    "HAlarmEnable": "HAlarmEnable",
 | 
			
		||||
    "HRestrainExpressions": "HRestrainExpressions",
 | 
			
		||||
    "HAlarmText": "HAlarmText",
 | 
			
		||||
    "HAlarmCode": "HAlarmCode",
 | 
			
		||||
    "HHAlarmEnable": "HHAlarmEnable",
 | 
			
		||||
    "HHRestrainExpressions": "HHRestrainExpressions",
 | 
			
		||||
    "HHAlarmText": "HHAlarmText",
 | 
			
		||||
    "HHAlarmCode": "HHAlarmCode",
 | 
			
		||||
    "LAlarmEnable": "LAlarmEnable",
 | 
			
		||||
    "LRestrainExpressions": "LRestrainExpressions",
 | 
			
		||||
    "LAlarmText": "LAlarmText",
 | 
			
		||||
    "LAlarmCode": "LAlarmCode",
 | 
			
		||||
    "LLAlarmEnable": "LLAlarmEnable",
 | 
			
		||||
    "LLRestrainExpressions": "LLRestrainExpressions",
 | 
			
		||||
    "LLAlarmText": "LLAlarmText",
 | 
			
		||||
    "LLAlarmCode": "LLAlarmCode",
 | 
			
		||||
    "CustomAlarmEnable": "CustomAlarmEnable",
 | 
			
		||||
    "CustomRestrainExpressions": "CustomRestrainExpressions",
 | 
			
		||||
    "CustomAlarmText": "CustomAlarmText",
 | 
			
		||||
    "CustomAlarmCode": "CustomAlarmCode",
 | 
			
		||||
    "Unit": "Unit",
 | 
			
		||||
    "RegisterAddress": "RegisterAddress",
 | 
			
		||||
    "OtherMethod": "OtherMethod",
 | 
			
		||||
    "Remark1": "Remark1",
 | 
			
		||||
    "Remark2": "Remark2",
 | 
			
		||||
    "Remark3": "Remark3",
 | 
			
		||||
    "Remark4": "Remark4",
 | 
			
		||||
    "Remark5": "Remark5",
 | 
			
		||||
    "AlarmCode": "AlarmCode",
 | 
			
		||||
    "RecoveryCode": "RecoveryCode",
 | 
			
		||||
    "AlarmLimit": "AlarmLimit",
 | 
			
		||||
@@ -170,10 +152,7 @@
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Description": "Description",
 | 
			
		||||
    "DeviceId": "CollectionDevice",
 | 
			
		||||
    "DeviceId.MinValue": "{0} cannot be empty",
 | 
			
		||||
    "DeviceId.Required": "{0} cannot be empty",
 | 
			
		||||
    "IntervalTime": "IntervalTime",
 | 
			
		||||
    "IntervalTime.MinValue": "{0} value is too small",
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "ProtectType": "ProtectType",
 | 
			
		||||
    "DataType": "DataType",
 | 
			
		||||
@@ -348,51 +327,21 @@
 | 
			
		||||
    "NameDump": "Duplicate device name {0}"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ChannelRuntime": {
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Name.Required": "{0} cannot be empty",
 | 
			
		||||
    "ChannelType": "ChannelType",
 | 
			
		||||
    "PluginName": "PluginName",
 | 
			
		||||
    "PluginName.Required": "{0} cannot be empty",
 | 
			
		||||
    "LogLevel": "LogLevel",
 | 
			
		||||
 | 
			
		||||
    "Enable": "Enable",
 | 
			
		||||
    "LogEnable": "LogEnable",
 | 
			
		||||
    "RemoteUrl": "RemoteUrl",
 | 
			
		||||
    "BindUrl": "BindUrl",
 | 
			
		||||
    "PortName": "PortName",
 | 
			
		||||
    "BaudRate": "BaudRate",
 | 
			
		||||
    "DataBits": "DataBits",
 | 
			
		||||
    "Parity": "Parity",
 | 
			
		||||
    "StopBits": "StopBits",
 | 
			
		||||
    "DtrEnable": "DtrEnable",
 | 
			
		||||
    "RtsEnable": "RtsEnable",
 | 
			
		||||
    "CacheTimeout": "CacheTimeout",
 | 
			
		||||
    "ConnectTimeout": "ConnectTimeout",
 | 
			
		||||
    "MaxConcurrentCount": "MaxConcurrentCount",
 | 
			
		||||
    "PluginType": "PluginType",
 | 
			
		||||
    "DeviceRuntimeCount": "DeviceRuntimeCount",
 | 
			
		||||
    "MaxConcurrentCount": "MaxConcurrentCount"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "MaxClientCount": "MaxClientCount",
 | 
			
		||||
    "CheckClearTime": "CheckClearTime",
 | 
			
		||||
    "Heartbeat": "Heartbeat",
 | 
			
		||||
    "HeartbeatTime": "HeartbeatTime",
 | 
			
		||||
    "DtuId": "DtuId",
 | 
			
		||||
    "DtuSeviceType": "DtuSeviceType",
 | 
			
		||||
 | 
			
		||||
    "SaveChannel": "Add/Modify Channel",
 | 
			
		||||
    "CopyChannel": "Copy Channel",
 | 
			
		||||
    "DeleteChannel": "Delete Channel",
 | 
			
		||||
    "ClearChannel": "Clear Channel",
 | 
			
		||||
    "ExportChannel": "Export Channel",
 | 
			
		||||
    "ImportChannel": "Import Channel",
 | 
			
		||||
    "ImportNullError": "Unable to recognize",
 | 
			
		||||
    "NotOther": "Not supporting other channel types",
 | 
			
		||||
    "Connect": "Connect",
 | 
			
		||||
    "Confim": "Confim",
 | 
			
		||||
    "Disconnect": "Disconnect",
 | 
			
		||||
    "Channel": "Channel",
 | 
			
		||||
    "NameDump": "Duplicate channel name {0}"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Channel": {
 | 
			
		||||
    "SortCode": "SortCode",
 | 
			
		||||
    "CreateTime": "CreateTime",
 | 
			
		||||
    "CreateUser": "CreateUser",
 | 
			
		||||
    "UpdateTime": "UpdateTime",
 | 
			
		||||
    "UpdateUser": "UpdateUser",
 | 
			
		||||
 | 
			
		||||
    "Name": "Name",
 | 
			
		||||
    "Name.Required": "{0} cannot be empty",
 | 
			
		||||
    "ChannelType": "ChannelType",
 | 
			
		||||
@@ -452,6 +401,7 @@
 | 
			
		||||
    "Name.Required": "{0} cannot be empty",
 | 
			
		||||
    "Description": "Description",
 | 
			
		||||
    "Group": "Group",
 | 
			
		||||
    "CollectGroup": "CollectGroup",
 | 
			
		||||
    "DeviceId": "CollectionDevice",
 | 
			
		||||
    "DeviceId.MinValue": "{0} cannot be empty",
 | 
			
		||||
    "DeviceId.Required": "{0} cannot be empty",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,70 @@
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationDtuPackPropertyBase": {
 | 
			
		||||
    "DtuId": "Dtu注册包(UTF8)"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationDtuPropertyBase": {
 | 
			
		||||
    "DtuId": "Dtu注册包(UTF8)"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationPackPropertyBase": {
 | 
			
		||||
 | 
			
		||||
    "MaxPack": "最大打包长度"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectFoundationPropertyBase": {
 | 
			
		||||
    "DataFormat": "解析规则",
 | 
			
		||||
    "IsStringReverseByteWord": "字符串反转",
 | 
			
		||||
    "Timeout": "读写超时时间",
 | 
			
		||||
    "SendDelayTime": "发送延时"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "ReIntervalTime": "离线恢复时间",
 | 
			
		||||
    "RetryCount": "失败重试次数"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyBase": {
 | 
			
		||||
    "ReIntervalTime": "离线恢复时间",
 | 
			
		||||
    "RetryCount": "失败重试次数",
 | 
			
		||||
    "ConcurrentCount": "最大并发数量"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCacheIntervalScript": {
 | 
			
		||||
 | 
			
		||||
    "DetailLog": "详细日志",
 | 
			
		||||
    "JsonFormattingIndented": "Json缩进格式化",
 | 
			
		||||
 | 
			
		||||
    "BigTextScriptDeviceModel": "设备上传脚本",
 | 
			
		||||
    "BigTextScriptVariableModel": "变量上传脚本",
 | 
			
		||||
    "BigTextScriptAlarmModel": "报警上传脚本",
 | 
			
		||||
 | 
			
		||||
    "IsDeviceList": "设备状态列表上传",
 | 
			
		||||
    "IsVariableList": "变量列表上传",
 | 
			
		||||
    "IsAlarmList": "报警列表上传",
 | 
			
		||||
 | 
			
		||||
    "DeviceTopic": "设备主题",
 | 
			
		||||
    "VariableTopic": "变量主题",
 | 
			
		||||
    "AlarmTopic": "报警主题"
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCacheInterval": {
 | 
			
		||||
 | 
			
		||||
    "BusinessUpdateEnum": "上传模式",
 | 
			
		||||
    "BusinessInterval": "定时上传间隔",
 | 
			
		||||
    "IsAllVariable": "选择全部变量"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessPropertyWithCache": {
 | 
			
		||||
 | 
			
		||||
    "QueueMaxCount": "内存队列最大数量",
 | 
			
		||||
    "CacheFileMaxLength": "缓存文件最大长度(mb)",
 | 
			
		||||
    "SplitSize": "上传每页条数",
 | 
			
		||||
    "CacheEnable": "启用缓存",
 | 
			
		||||
 | 
			
		||||
    "GroupUpdate": "分组上传",
 | 
			
		||||
    "OnlineFilter": "过滤离线变量"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.BusinessUpdateEnum": {
 | 
			
		||||
    "Change": "变化",
 | 
			
		||||
    "Interval": "定时",
 | 
			
		||||
@@ -29,50 +95,16 @@
 | 
			
		||||
    "ActiveTime": "活跃时间",
 | 
			
		||||
    "DeviceVariableCount": "变量数量",
 | 
			
		||||
    "LastErrorMessage": "离线原因",
 | 
			
		||||
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "DeviceStatus": "设备状态",
 | 
			
		||||
    "RedundantType": "冗余状态",
 | 
			
		||||
    "PluginName": "插件名称",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "Description": "描述",
 | 
			
		||||
    "ChannelId": "通道",
 | 
			
		||||
    "ChannelName": "通道",
 | 
			
		||||
    "ChannelId.MinValue": " {0} 不可为空",
 | 
			
		||||
    "ChannelId.Required": " {0} 不可为空",
 | 
			
		||||
    "IntervalTime": "默认执行间隔",
 | 
			
		||||
    "IntervalTime.MinValue": " {0} 值太小",
 | 
			
		||||
    "PluginName": "插件名称",
 | 
			
		||||
    "PluginName.Required": "{0} 插件名称不可为空",
 | 
			
		||||
    "PluginName.Required": "{0} 插件名称不可为空"
 | 
			
		||||
 | 
			
		||||
    "Enable": "设备使能",
 | 
			
		||||
    "LogEnable": "启用调试日志",
 | 
			
		||||
    "LogLevel": "日志等级",
 | 
			
		||||
    "RedundantEnable": "启用冗余",
 | 
			
		||||
    "RedundantDeviceId": "冗余设备",
 | 
			
		||||
    "RedundantSwitchType": "冗余操作模式",
 | 
			
		||||
    "RedundantScanIntervalTime": "冗余检测时间",
 | 
			
		||||
    "RedundantScript": "冗余检测脚本",
 | 
			
		||||
    "Remark1": "备用1",
 | 
			
		||||
    "Remark2": "备用2",
 | 
			
		||||
    "Remark3": "备用3",
 | 
			
		||||
    "Remark4": "备用4",
 | 
			
		||||
    "Remark5": "备用5",
 | 
			
		||||
 | 
			
		||||
    "RedundantDeviceNotNull": "启用冗余时,必须选择备用设备",
 | 
			
		||||
 | 
			
		||||
    "SaveDevice": "添加/修改设备",
 | 
			
		||||
    "CopyDevice": "复制设备",
 | 
			
		||||
    "DeleteDevice": "删除设备",
 | 
			
		||||
    "ClearDevice": "清空设备",
 | 
			
		||||
    "ExportDevice": "导出设备",
 | 
			
		||||
    "ImportDevice": "导入设备",
 | 
			
		||||
 | 
			
		||||
    "DeviceStatus": "设备状态",
 | 
			
		||||
 | 
			
		||||
    "ImportNullError": "无法识别",
 | 
			
		||||
    "RedundantDeviceError": "冗余设备错误",
 | 
			
		||||
    "ChannelError": "通道错误",
 | 
			
		||||
    "NotNull": " {0} 不存在",
 | 
			
		||||
    "DeviceNotNull": "设备名称不存在",
 | 
			
		||||
    "PluginNotNull": "插件不存在"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.VariableRuntime": {
 | 
			
		||||
    "ChangeTime": "变化时间",
 | 
			
		||||
@@ -85,71 +117,14 @@
 | 
			
		||||
    "Value": "实时值",
 | 
			
		||||
    "RuntimeType": "动态类型",
 | 
			
		||||
    "AlarmEnable": "报警使能",
 | 
			
		||||
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "Description": "描述",
 | 
			
		||||
    "Group": "分组",
 | 
			
		||||
    "DeviceId": "采集设备",
 | 
			
		||||
    "DeviceId.MinValue": " {0} 不可为空",
 | 
			
		||||
    "DeviceId.Required": " {0} 不可为空",
 | 
			
		||||
    "IntervalTime": "执行间隔",
 | 
			
		||||
    "IntervalTime.MinValue": " {0} 值太小",
 | 
			
		||||
 | 
			
		||||
    "ArrayLength": "数组长度",
 | 
			
		||||
    "Enable": "变量使能",
 | 
			
		||||
    "ProtectType": "读写权限",
 | 
			
		||||
    "DataType": "数据类型",
 | 
			
		||||
    "ReadExpressions": "读取表达式",
 | 
			
		||||
    "WriteExpressions": "写入表达式",
 | 
			
		||||
    "RpcWriteEnable": "远程写入",
 | 
			
		||||
    "SaveValue": "保存初始值",
 | 
			
		||||
    "DynamicVariable": "动态变量",
 | 
			
		||||
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "BoolOpenAlarmEnable": "布尔开报警使能",
 | 
			
		||||
    "BoolOpenRestrainExpressions": "布尔开报警约束",
 | 
			
		||||
    "BoolOpenAlarmText": "布尔开报警文本",
 | 
			
		||||
 | 
			
		||||
    "BoolCloseAlarmEnable": "布尔关报警使能",
 | 
			
		||||
    "BoolCloseRestrainExpressions": "布尔关报警约束",
 | 
			
		||||
    "BoolCloseAlarmText": "布尔关报警文本",
 | 
			
		||||
 | 
			
		||||
    "HAlarmEnable": "高报使能",
 | 
			
		||||
    "HRestrainExpressions": "高报约束",
 | 
			
		||||
    "HAlarmText": "高报文本",
 | 
			
		||||
    "HAlarmCode": "高限值",
 | 
			
		||||
 | 
			
		||||
    "HHAlarmEnable": "高高报使能",
 | 
			
		||||
    "HHRestrainExpressions": "高高报约束",
 | 
			
		||||
    "HHAlarmText": "高高报文本",
 | 
			
		||||
    "HHAlarmCode": "高高限值",
 | 
			
		||||
 | 
			
		||||
    "LAlarmEnable": "低报使能",
 | 
			
		||||
    "LRestrainExpressions": "低报约束",
 | 
			
		||||
    "LAlarmText": "低报文本",
 | 
			
		||||
    "LAlarmCode": "低限值",
 | 
			
		||||
 | 
			
		||||
    "LLAlarmEnable": "低低报使能",
 | 
			
		||||
    "LLRestrainExpressions": "低低报约束",
 | 
			
		||||
    "LLAlarmText": "低低报文本",
 | 
			
		||||
    "LLAlarmCode": "低低限值",
 | 
			
		||||
 | 
			
		||||
    "CustomAlarmEnable": "自定义报警使能",
 | 
			
		||||
    "CustomRestrainExpressions": "自定义报警约束",
 | 
			
		||||
    "CustomAlarmText": "自定义报警文本",
 | 
			
		||||
    "CustomAlarmCode": "自定义报警限值",
 | 
			
		||||
 | 
			
		||||
    "Unit": "单位",
 | 
			
		||||
    "RegisterAddress": "变量地址",
 | 
			
		||||
    "OtherMethod": "特殊方法",
 | 
			
		||||
 | 
			
		||||
    "Remark1": "备用1",
 | 
			
		||||
    "Remark2": "备用2",
 | 
			
		||||
    "Remark3": "备用3",
 | 
			
		||||
    "Remark4": "备用4",
 | 
			
		||||
    "Remark5": "备用5",
 | 
			
		||||
 | 
			
		||||
    "AlarmCode": "报警值",
 | 
			
		||||
    "RecoveryCode": "恢复值",
 | 
			
		||||
    "AlarmLimit": "报警限值",
 | 
			
		||||
@@ -171,13 +146,9 @@
 | 
			
		||||
    "AlarmEnable": "报警使能",
 | 
			
		||||
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "Description": "描述",
 | 
			
		||||
    "DeviceId": "采集设备",
 | 
			
		||||
    "DeviceId.MinValue": " {0} 不可为空",
 | 
			
		||||
    "DeviceId.Required": " {0} 不可为空",
 | 
			
		||||
    "IntervalTime": "执行间隔",
 | 
			
		||||
    "IntervalTime.MinValue": " {0} 值太小",
 | 
			
		||||
 | 
			
		||||
    "ArrayLength": "数组长度",
 | 
			
		||||
    "Enable": "变量使能",
 | 
			
		||||
@@ -369,58 +340,20 @@
 | 
			
		||||
    "NameDump": "设备名称 {0} 重复"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ChannelRuntime": {
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "ChannelType": "通道类型",
 | 
			
		||||
    "Enable": "启用",
 | 
			
		||||
    "LogEnable": "启用调试日志",
 | 
			
		||||
    "LogLevel": "日志等级",
 | 
			
		||||
    "PluginName": "插件名称",
 | 
			
		||||
    "PluginName.Required": "{0} 插件名称不可为空",
 | 
			
		||||
 | 
			
		||||
    "RemoteUrl": "远程url",
 | 
			
		||||
    "BindUrl": "本地url",
 | 
			
		||||
 | 
			
		||||
    "PortName": "COM口",
 | 
			
		||||
    "BaudRate": "波特率",
 | 
			
		||||
    "DataBits": "数据位",
 | 
			
		||||
    "Parity": "校验位",
 | 
			
		||||
    "StopBits": "停止位",
 | 
			
		||||
    "DtrEnable": "Dtr",
 | 
			
		||||
    "RtsEnable": "Rts",
 | 
			
		||||
    "CacheTimeout": "接收缓存超时",
 | 
			
		||||
    "ConnectTimeout": "连接超时",
 | 
			
		||||
    "MaxConcurrentCount": "最大并发数",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "MaxClientCount": "最大连接数",
 | 
			
		||||
    "CheckClearTime": "客户端连接滑动过期时间",
 | 
			
		||||
    "Heartbeat": "心跳内容(utf8)",
 | 
			
		||||
    "HeartbeatTime": "心跳间隔",
 | 
			
		||||
    "DtuId": "Dtu终端注册包(utf-8)",
 | 
			
		||||
    "DtuSeviceType": "DTU服务类型",
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    "SaveChannel": "添加/修改通道",
 | 
			
		||||
    "CopyChannel": "复制通道",
 | 
			
		||||
    "DeleteChannel": "删除通道",
 | 
			
		||||
    "ClearChannel": "清空通道",
 | 
			
		||||
    "ExportChannel": "导出通道",
 | 
			
		||||
    "ImportChannel": "导入通道",
 | 
			
		||||
 | 
			
		||||
    "ImportNullError": "无法识别",
 | 
			
		||||
 | 
			
		||||
    "NotOther": "不支持其他通道类型",
 | 
			
		||||
    "Connect": "连接",
 | 
			
		||||
    "Confim": "创建",
 | 
			
		||||
    "Disconnect": "断开",
 | 
			
		||||
    "Channel": "通道",
 | 
			
		||||
    "NameDump": "通道名称 {0} 重复",
 | 
			
		||||
 | 
			
		||||
    "DeviceRuntimeCounts": "设备数量"
 | 
			
		||||
    "PluginType": "插件类型",
 | 
			
		||||
    "MaxConcurrentCount": "最大并发数量",
 | 
			
		||||
    "DeviceRuntimeCount": "设备数量"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Channel": {
 | 
			
		||||
    "SortCode": "排序",
 | 
			
		||||
    "CreateTime": "创建时间",
 | 
			
		||||
    "CreateUser": "创建人",
 | 
			
		||||
    "UpdateTime": "更新时间",
 | 
			
		||||
    "UpdateUser": "更新人",
 | 
			
		||||
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "ChannelType": "通道类型",
 | 
			
		||||
@@ -486,7 +419,8 @@
 | 
			
		||||
    "Name": "名称",
 | 
			
		||||
    "Name.Required": " {0} 不可为空",
 | 
			
		||||
    "Description": "描述",
 | 
			
		||||
    "Group": "分组",
 | 
			
		||||
    "Group": "业务组",
 | 
			
		||||
    "CollectGroup": "采集组",
 | 
			
		||||
    "DeviceId": "采集设备",
 | 
			
		||||
    "DeviceId.MinValue": " {0} 不可为空",
 | 
			
		||||
    "DeviceId.Required": " {0} 不可为空",
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    public int? DeviceRuntimeCounts => DeviceRuntimes?.Count;
 | 
			
		||||
    public int? DeviceRuntimeCount => DeviceRuntimes?.Count;
 | 
			
		||||
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
@@ -131,7 +131,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        Config?.SafeDispose();
 | 
			
		||||
        //Config?.SafeDispose();
 | 
			
		||||
 | 
			
		||||
        GlobalData.Channels.TryRemove(Id, out _);
 | 
			
		||||
        DeviceThreadManage = null;
 | 
			
		||||
@@ -146,4 +146,57 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
        return $"{Name}[{base.ToString()}]";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public IChannel GetChannel(TouchSocketConfig config)
 | 
			
		||||
    {
 | 
			
		||||
        lock (GlobalData.Channels)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (DeviceThreadManage?.Channel?.DisposedValue == false)
 | 
			
		||||
                return DeviceThreadManage?.Channel;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            if (ChannelType == ChannelTypeEnum.TcpService
 | 
			
		||||
                || ChannelType == ChannelTypeEnum.SerialPort
 | 
			
		||||
                || ChannelType == ChannelTypeEnum.UdpSession
 | 
			
		||||
                )
 | 
			
		||||
            {
 | 
			
		||||
                //获取相同配置的Tcp服务或Udp服务或COM
 | 
			
		||||
                var same = GlobalData.Channels.FirstOrDefault(a =>
 | 
			
		||||
                 {
 | 
			
		||||
                     if (a.Value == this)
 | 
			
		||||
                         return false;
 | 
			
		||||
                     if (a.Value.DeviceThreadManage?.Channel?.DisposedValue == true || a.Value.DeviceThreadManage?.Channel?.DisposedValue == null)
 | 
			
		||||
                         return false;
 | 
			
		||||
 | 
			
		||||
                     if (a.Value.ChannelType == ChannelType)
 | 
			
		||||
                     {
 | 
			
		||||
                         if (a.Value.ChannelType == ChannelTypeEnum.TcpService)
 | 
			
		||||
                             if (a.Value.BindUrl == BindUrl)
 | 
			
		||||
                                 return true;
 | 
			
		||||
                         if (a.Value.ChannelType == ChannelTypeEnum.UdpSession)
 | 
			
		||||
                             if ((!BindUrl.IsNullOrWhiteSpace()) && a.Value.BindUrl == BindUrl)
 | 
			
		||||
                                 return true;
 | 
			
		||||
                         if (a.Value.ChannelType == ChannelTypeEnum.SerialPort)
 | 
			
		||||
                             if (a.Value.PortName == PortName)
 | 
			
		||||
                                 return true;
 | 
			
		||||
                     }
 | 
			
		||||
                     return false;
 | 
			
		||||
                 }).Value;
 | 
			
		||||
 | 
			
		||||
                if (same != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return same.GetChannel(config);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (DeviceThreadManage?.Channel?.DisposedValue == false)
 | 
			
		||||
                return DeviceThreadManage?.Channel;
 | 
			
		||||
 | 
			
		||||
            var ichannel = config.GetChannel(this);
 | 
			
		||||
            return ichannel;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,5 +19,8 @@ public class VariableMapper : IRegister
 | 
			
		||||
    {
 | 
			
		||||
        config.ForType<Variable, VariableRuntime>()
 | 
			
		||||
        .Map(dest => dest.Value, src => src.InitValue);
 | 
			
		||||
 | 
			
		||||
        config.ForType<VariableRuntime, VariableRuntime>()
 | 
			
		||||
.Ignore(dest => dest.DeviceRuntime);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -216,16 +216,7 @@ public class ChannelRuntimeService : IChannelRuntimeService
 | 
			
		||||
            await RuntimeServiceHelper.InitAsync(newChannelRuntimes, newDeviceRuntimes, _logger).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var startCollectChannelEnable = GlobalData.StartCollectChannelEnable;
 | 
			
		||||
            var startBusinessChannelEnable = GlobalData.StartBusinessChannelEnable;
 | 
			
		||||
 | 
			
		||||
            var collectChannelRuntimes = newChannelRuntimes.Where(x => (x.Enable && x.IsCollect == true && startCollectChannelEnable));
 | 
			
		||||
 | 
			
		||||
            var businessChannelRuntimes = newChannelRuntimes.Where(x => (x.Enable && x.IsCollect == false && startBusinessChannelEnable));
 | 
			
		||||
 | 
			
		||||
            //根据初始冗余属性,筛选启动
 | 
			
		||||
            await GlobalData.ChannelThreadManage.RestartChannelAsync(businessChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
            await GlobalData.ChannelThreadManage.RestartChannelAsync(collectChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
            await GlobalData.ChannelThreadManage.RestartChannelAsync(newChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
 
 | 
			
		||||
@@ -329,8 +329,8 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
 | 
			
		||||
        ManageHelper.CheckChannelCount(insertData.Count);
 | 
			
		||||
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        await db.Fastest<Channel>().PageSize(100000).BulkCopyAsync(insertData).ConfigureAwait(false);
 | 
			
		||||
        await db.Fastest<Channel>().PageSize(100000).BulkUpdateAsync(upData).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
 | 
			
		||||
        DeleteChannelFromCache();
 | 
			
		||||
        return channels.Select(a => a.Id).ToHashSet();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -351,8 +351,8 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
 | 
			
		||||
        ManageHelper.CheckDeviceCount(insertData.Count);
 | 
			
		||||
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        await db.Fastest<Device>().PageSize(100000).BulkCopyAsync(insertData).ConfigureAwait(false);
 | 
			
		||||
        await db.Fastest<Device>().PageSize(100000).BulkUpdateAsync(upData).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
 | 
			
		||||
        DeleteDeviceFromCache();
 | 
			
		||||
        return devices.Select(a => a.Id).ToHashSet();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,6 @@ using System.Collections.Concurrent;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
internal sealed class ChannelThreadManage : IChannelThreadManage
 | 
			
		||||
@@ -115,8 +113,6 @@ internal sealed class ChannelThreadManage : IChannelThreadManage
 | 
			
		||||
    {
 | 
			
		||||
        await PrivateRemoveChannelsAsync(channelRuntimes.Select(a => a.Id)).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        BytePool.Default.Clear();
 | 
			
		||||
 | 
			
		||||
        await channelRuntimes.ParallelForEachAsync(async (channelRuntime, token) =>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
 
 | 
			
		||||
@@ -117,10 +117,9 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
 | 
			
		||||
        // 添加默认日志记录器
 | 
			
		||||
        LogMessage.AddLogger(new EasyLogger(logger.Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
 | 
			
		||||
 | 
			
		||||
        var ichannel = config.GetChannel(channelRuntime);
 | 
			
		||||
 | 
			
		||||
        // 根据配置获取通道实例
 | 
			
		||||
        Channel = ichannel;
 | 
			
		||||
        Channel = channelRuntime.GetChannel(config);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //初始设置输出文本日志
 | 
			
		||||
        SetLog(CurrentChannel.LogLevel);
 | 
			
		||||
@@ -901,7 +900,8 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage
 | 
			
		||||
            await NewDeviceLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await PrivateRemoveDevicesAsync(Drivers.Keys).ConfigureAwait(false);
 | 
			
		||||
            Channel?.SafeDispose();
 | 
			
		||||
            if (Channel?.Collects.Count == 0)
 | 
			
		||||
                Channel?.SafeDispose();
 | 
			
		||||
 | 
			
		||||
            LogMessage?.LogInformation(Localizer["ChannelDispose", CurrentChannel?.Name ?? string.Empty]);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -71,16 +71,7 @@ internal sealed class GatewayMonitorHostedService : BackgroundService, IGatewayM
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var startCollectChannelEnable = GlobalData.StartCollectChannelEnable;
 | 
			
		||||
            var startBusinessChannelEnable = GlobalData.StartBusinessChannelEnable;
 | 
			
		||||
 | 
			
		||||
            var collectChannelRuntimes = channelRuntimes.Where(x => (x.Enable && x.IsCollect == true && startCollectChannelEnable));
 | 
			
		||||
 | 
			
		||||
            var businessChannelRuntimes = channelRuntimes.Where(x => (x.Enable && x.IsCollect == false && startBusinessChannelEnable));
 | 
			
		||||
 | 
			
		||||
            //根据初始冗余属性,筛选启动
 | 
			
		||||
            await ChannelThreadManage.RestartChannelAsync(businessChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
            await ChannelThreadManage.RestartChannelAsync(collectChannelRuntimes).ConfigureAwait(false);
 | 
			
		||||
            await ChannelThreadManage.RestartChannelAsync(channelRuntimes).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -310,7 +310,6 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                item.Value.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
            if (group.Key != null)
 | 
			
		||||
            {
 | 
			
		||||
@@ -321,6 +320,19 @@ internal static class RuntimeServiceHelper
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static void VariableRuntimesDispose(IEnumerable<long> variableIds)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        foreach (var variableId in variableIds)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (GlobalData.IdVariables.TryGetValue(variableId, out var variableRuntime))
 | 
			
		||||
            {
 | 
			
		||||
                variableRuntime.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static void AddCollectChangedDriver(IEnumerable<VariableRuntime> newVariableRuntimes, ConcurrentHashSet<IDriver> changedDriver)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,10 +43,10 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
            //获取变量,先找到原插件线程,然后修改插件线程内的字典,再改动全局字典,最后刷新插件
 | 
			
		||||
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = new();
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.VariableRuntimesDispose(variableIds);
 | 
			
		||||
            RuntimeServiceHelper.AddCollectChangedDriver(newVariableRuntimes, changedDriver);
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.AddCollectChangedDriver(newVariableRuntimes, changedDriver);
 | 
			
		||||
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
@@ -79,9 +79,9 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = new();
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.VariableRuntimesDispose(variableIds);
 | 
			
		||||
            RuntimeServiceHelper.AddCollectChangedDriver(newVariableRuntimes, changedDriver);
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
@@ -111,6 +111,7 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = new();
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
            RuntimeServiceHelper.VariableRuntimesDispose(variableIds);
 | 
			
		||||
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
@@ -144,10 +145,9 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
            var variableIds = newVariableRuntimes.Select(a => a.Id).ToHashSet();
 | 
			
		||||
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = new();
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.VariableRuntimesDispose(variableIds);
 | 
			
		||||
            RuntimeServiceHelper.AddCollectChangedDriver(newVariableRuntimes, changedDriver);
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
@@ -237,10 +237,9 @@ public class VariableRuntimeService : IVariableRuntimeService
 | 
			
		||||
 | 
			
		||||
            ConcurrentHashSet<IDriver> changedDriver = new();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            RuntimeServiceHelper.VariableRuntimesDispose(variableIds);
 | 
			
		||||
            RuntimeServiceHelper.AddCollectChangedDriver(newVariableRuntimes, changedDriver);
 | 
			
		||||
            RuntimeServiceHelper.AddBusinessChangedDriver(variableIds, changedDriver);
 | 
			
		||||
 | 
			
		||||
            if (restart)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -217,9 +217,9 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
 | 
			
		||||
        var result = await db.UseTranAsync(async () =>
 | 
			
		||||
        {
 | 
			
		||||
            await db.Fastest<Channel>().PageSize(100000).BulkCopyAsync(newChannels).ConfigureAwait(false);
 | 
			
		||||
            await db.Fastest<Device>().PageSize(100000).BulkCopyAsync(newDevices).ConfigureAwait(false);
 | 
			
		||||
            await db.Fastest<Variable>().PageSize(100000).BulkCopyAsync(newVariables).ConfigureAwait(false);
 | 
			
		||||
            await db.BulkCopyAsync(newChannels, 100000).ConfigureAwait(false);
 | 
			
		||||
            await db.BulkCopyAsync(newDevices, 100000).ConfigureAwait(false);
 | 
			
		||||
            await db.BulkCopyAsync(newVariables, 100000).ConfigureAwait(false);
 | 
			
		||||
        }).ConfigureAwait(false);
 | 
			
		||||
        if (result.IsSuccess)//如果成功了
 | 
			
		||||
        {
 | 
			
		||||
@@ -486,8 +486,8 @@ internal sealed class VariableService : BaseService<Variable>, IVariableService
 | 
			
		||||
        var insertData = variables.Where(a => !a.IsUp).ToList();
 | 
			
		||||
        ManageHelper.CheckVariableCount(insertData.Count);
 | 
			
		||||
        using var db = GetDB();
 | 
			
		||||
        await db.Fastest<Variable>().PageSize(100000).BulkCopyAsync(insertData).ConfigureAwait(false);
 | 
			
		||||
        await db.Fastest<Variable>().PageSize(100000).BulkUpdateAsync(upData).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkCopyAsync(insertData, 100000).ConfigureAwait(false);
 | 
			
		||||
        await db.BulkUpdateAsync(upData, 100000).ConfigureAwait(false);
 | 
			
		||||
        _dispatchService.Dispatch(new());
 | 
			
		||||
        DeleteVariableCache();
 | 
			
		||||
        return variables.Select(a => a.Id).ToHashSet();
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,12 @@
 | 
			
		||||
 | 
			
		||||
using Mapster;
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using SqlSugar;
 | 
			
		||||
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
@@ -17,14 +23,14 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[AppStartup(-100)]
 | 
			
		||||
public class Startup : AppStartup
 | 
			
		||||
{
 | 
			
		||||
    public void ConfigureAdminApp(IServiceCollection services)
 | 
			
		||||
    public void Configure(IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddConfigurableOptions<ChannelThreadOptions>();
 | 
			
		||||
        services.AddConfigurableOptions<GatewayLogOptions>();
 | 
			
		||||
        services.AddConfigurableOptions<RpcLogOptions>();
 | 
			
		||||
 | 
			
		||||
        //底层多语言配置
 | 
			
		||||
        //Foundation.LocalizerUtil.SetLocalizerFactory((a) => App.CreateLocalizerByType(a));
 | 
			
		||||
        Foundation.LocalizerUtil.SetLocalizerFactory((a) => App.CreateLocalizerByType(a));
 | 
			
		||||
 | 
			
		||||
        TypeAdapterConfig.GlobalSettings.Scan(App.Assemblies.ToArray());
 | 
			
		||||
        // 配置默认全局映射(支持覆盖)
 | 
			
		||||
@@ -62,8 +68,9 @@ public class Startup : AppStartup
 | 
			
		||||
        services.AddGatewayHostedService<IGatewayMonitorHostedService, GatewayMonitorHostedService>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void UseAdminCore(IServiceProvider serviceProvider)
 | 
			
		||||
    public void Use(IApplicationBuilder applicationBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        var serviceProvider = applicationBuilder.ApplicationServices;
 | 
			
		||||
        //检查ConfigId
 | 
			
		||||
        var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
 | 
			
		||||
        foreach (var configId in configIdGroup)
 | 
			
		||||
@@ -71,6 +78,7 @@ public class Startup : AppStartup
 | 
			
		||||
            if (configId.Count() > 1) throw new($"Sqlsugar connect configId: {configId.Key} Duplicate!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //遍历配置
 | 
			
		||||
        DbContext.DbConfigs?.ForEach(it =>
 | 
			
		||||
        {
 | 
			
		||||
@@ -79,6 +87,19 @@ public class Startup : AppStartup
 | 
			
		||||
            if (it.InitTable == true)
 | 
			
		||||
                connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //兼容变量名称唯一键处理
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = DbContext.GetDB<Variable>();
 | 
			
		||||
            if (db.DbMaintenance.IsAnyIndex("unique_variable_name"))
 | 
			
		||||
            {
 | 
			
		||||
                DropIndex(db, "unique_variable_name", "variable");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch { }
 | 
			
		||||
 | 
			
		||||
        var fullName = Assembly.GetExecutingAssembly().FullName;//获取程序集全名
 | 
			
		||||
        CodeFirstUtils.CodeFirst(fullName!);//CodeFirst
 | 
			
		||||
 | 
			
		||||
@@ -88,16 +109,62 @@ public class Startup : AppStartup
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = DbContext.GetDB<Channel>();
 | 
			
		||||
            if (!db.DbMaintenance.IsAnyColumn(nameof(Channel), "LogEnable", false)) return;
 | 
			
		||||
            var tables = db.DbMaintenance.DropColumn(nameof(Channel), "LogEnable");
 | 
			
		||||
            if (db.DbMaintenance.IsAnyColumn(nameof(Channel), "LogEnable", false))
 | 
			
		||||
            {
 | 
			
		||||
                var tables = db.DbMaintenance.DropColumn(nameof(Channel), "LogEnable");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch { }
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = DbContext.GetDB<Device>();
 | 
			
		||||
            if (!db.DbMaintenance.IsAnyColumn(nameof(Device), "LogEnable", false)) return;
 | 
			
		||||
            var tables = db.DbMaintenance.DropColumn(nameof(Device), "LogEnable");
 | 
			
		||||
            if (db.DbMaintenance.IsAnyColumn(nameof(Device), "LogEnable", false))
 | 
			
		||||
            {
 | 
			
		||||
                var tables = db.DbMaintenance.DropColumn(nameof(Device), "LogEnable");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch { }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        serviceProvider.GetService<IHostApplicationLifetime>().ApplicationStarted.Register(() =>
 | 
			
		||||
        {
 | 
			
		||||
            serviceProvider.GetService<ILoggerFactory>().CreateLogger(nameof(ThingsGateway)).LogInformation("ThingsGateway is started...");
 | 
			
		||||
        });
 | 
			
		||||
        serviceProvider.GetService<IHostApplicationLifetime>().ApplicationStopping.Register(() =>
 | 
			
		||||
        {
 | 
			
		||||
            serviceProvider.GetService<ILoggerFactory>().CreateLogger(nameof(ThingsGateway)).LogInformation("ThingsGateway is stopping...");
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除指定表上的索引(自动根据数据库类型生成正确的 DROP INDEX SQL)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="db">数据库连接</param>
 | 
			
		||||
    /// <param name="indexName">索引名</param>
 | 
			
		||||
    /// <param name="tableName">表名(部分数据库需要)</param>
 | 
			
		||||
    private static void DropIndex(SqlSugarClient db, string indexName, string tableName)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(indexName))
 | 
			
		||||
            throw new ArgumentNullException(nameof(indexName));
 | 
			
		||||
 | 
			
		||||
        string dropIndexSql;
 | 
			
		||||
 | 
			
		||||
        switch (db.CurrentConnectionConfig.DbType)
 | 
			
		||||
        {
 | 
			
		||||
            case SqlSugar.DbType.MySql:
 | 
			
		||||
            case SqlSugar.DbType.SqlServer:
 | 
			
		||||
                dropIndexSql = $"DROP INDEX {indexName} ON {tableName};";
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                dropIndexSql = $"DROP INDEX {indexName};";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
        db.Ado.ExecuteCommand(dropIndexSql);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,9 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
 | 
			
		||||
		<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.6" />
 | 
			
		||||
		<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.Dmtp" Version="3.0.25" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.0.25" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.Dmtp" Version="3.1.2" />
 | 
			
		||||
		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.2" />
 | 
			
		||||
		<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" />
 | 
			
		||||
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -105,8 +105,8 @@ public class TcpSessionClientDto
 | 
			
		||||
    public string PluginInfos { get; set; }
 | 
			
		||||
 | 
			
		||||
    [AutoGenerateColumn(Searchable = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public DateTime LastReceivedTime { get; set; }
 | 
			
		||||
    public DateTimeOffset LastReceivedTime { get; set; }
 | 
			
		||||
 | 
			
		||||
    [AutoGenerateColumn(Searchable = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public DateTime LastSentTime { get; set; }
 | 
			
		||||
    public DateTimeOffset LastSentTime { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -99,7 +99,7 @@
 | 
			
		||||
                        </EditorItem>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        <EditorItem Field=@context.DeviceRuntimeCounts FieldExpression=@(()=> context.DeviceRuntimeCounts ) />
 | 
			
		||||
                        <EditorItem Field=@context.DeviceRuntimeCount FieldExpression=@(()=> context.DeviceRuntimeCount ) />
 | 
			
		||||
 | 
			
		||||
                    </FieldItems>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1299,6 +1299,7 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (Disposed) return;
 | 
			
		||||
                await Task.Delay(1000);
 | 
			
		||||
                await OnClickSearch(SearchText);
 | 
			
		||||
 | 
			
		||||
                Value = GetValue(Value);
 | 
			
		||||
@@ -1310,7 +1311,6 @@ EventCallback.Factory.Create<MouseEventArgs>(this, async e =>
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(2000);
 | 
			
		||||
                _isExecuting = false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -129,7 +129,6 @@ public partial class PropertyComponent : IPropertyUIBase
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        var op = new DialogOption()
 | 
			
		||||
        {
 | 
			
		||||
            IsScrolling = true,
 | 
			
		||||
@@ -144,6 +143,147 @@ public partial class PropertyComponent : IPropertyUIBase
 | 
			
		||||
    {
 | 
			
		||||
        {nameof(ScriptCheck.Data),data },
 | 
			
		||||
        {nameof(ScriptCheck.Script),script },
 | 
			
		||||
        {nameof(ScriptCheck.OnGetDemo),()=>
 | 
			
		||||
                {
 | 
			
		||||
                    return
 | 
			
		||||
                    pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptDeviceModel)?
 | 
			
		||||
                    """
 | 
			
		||||
                    using ThingsGateway.Foundation;
 | 
			
		||||
                    
 | 
			
		||||
                    using System.Dynamic;
 | 
			
		||||
                    using TouchSocket.Core;
 | 
			
		||||
                    public class S1 : IDynamicModel
 | 
			
		||||
                    {
 | 
			
		||||
                        public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
 | 
			
		||||
                        {
 | 
			
		||||
                            List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
 | 
			
		||||
                            foreach (var v in datas)
 | 
			
		||||
                            {
 | 
			
		||||
                                var device = (DeviceBasicData)v;
 | 
			
		||||
                                var expando = new ExpandoObject();
 | 
			
		||||
                                var deviceObj = new ExpandoObject();
 | 
			
		||||
 | 
			
		||||
                                deviceObj.TryAdd(nameof(Device.Description), device.Description);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.ActiveTime), device.ActiveTime);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.DeviceStatus), device.DeviceStatus.ToString());
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.LastErrorMessage), device.LastErrorMessage);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.PluginName), device.PluginName);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.Remark1), device.Remark1);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.Remark2), device.Remark2);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.Remark3), device.Remark3);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.Remark4), device.Remark4);
 | 
			
		||||
                                deviceObj.TryAdd(nameof(DeviceBasicData.Remark5), device.Remark5);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                                expando.TryAdd(nameof(Device.Name), deviceObj);
 | 
			
		||||
 | 
			
		||||
                            }
 | 
			
		||||
                            return deviceObjs;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    """
 | 
			
		||||
                    :
 | 
			
		||||
 | 
			
		||||
                    pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptVariableModel)?
 | 
			
		||||
 | 
			
		||||
                    """
 | 
			
		||||
                    using System.Dynamic;
 | 
			
		||||
                    using ThingsGateway.Foundation;
 | 
			
		||||
                    using TouchSocket.Core;
 | 
			
		||||
                    public class S2 : IDynamicModel
 | 
			
		||||
                    {
 | 
			
		||||
                        public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
 | 
			
		||||
                        {
 | 
			
		||||
 | 
			
		||||
                            List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
 | 
			
		||||
                            //按设备名称分组
 | 
			
		||||
                            var groups = datas.Where(a => !string.IsNullOrEmpty(((VariableBasicData)a).DeviceName)).GroupBy(a => ((VariableBasicData)a).DeviceName, a => ((VariableBasicData)a));
 | 
			
		||||
                            foreach (var group in groups)
 | 
			
		||||
                            {
 | 
			
		||||
                                //按采集时间分组
 | 
			
		||||
                                var data = group.GroupBy(a => a.CollectTime.DateTimeToUnixTimestamp());
 | 
			
		||||
                                var deviceObj = new ExpandoObject();
 | 
			
		||||
                                List<ExpandoObject> expandos = new List<ExpandoObject>();
 | 
			
		||||
                                foreach (var item in data)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var expando = new ExpandoObject();
 | 
			
		||||
                                    expando.TryAdd("ts", item.Key);
 | 
			
		||||
                                    var variableObj = new ExpandoObject();
 | 
			
		||||
                                    foreach (var tag in item)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        variableObj.TryAdd(tag.Name, tag.Value);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    expando.TryAdd("values", variableObj);
 | 
			
		||||
 | 
			
		||||
                                    expandos.Add(expando);
 | 
			
		||||
                                }
 | 
			
		||||
                                deviceObj.TryAdd(group.Key, expandos);
 | 
			
		||||
                                deviceObjs.Add(deviceObj);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return deviceObjs;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    """
 | 
			
		||||
                    :
 | 
			
		||||
 | 
			
		||||
                    pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel)?
 | 
			
		||||
 | 
			
		||||
                    """
 | 
			
		||||
                    using System.Dynamic;
 | 
			
		||||
                    using ThingsGateway.Foundation;
 | 
			
		||||
                                        
 | 
			
		||||
                    using TouchSocket.Core;
 | 
			
		||||
                    public class DeviceScript : IDynamicModel
 | 
			
		||||
                    {
 | 
			
		||||
                        public IEnumerable<dynamic> GetList(IEnumerable<object> datas)
 | 
			
		||||
                        {
 | 
			
		||||
 | 
			
		||||
                            List<ExpandoObject> deviceObjs = new List<ExpandoObject>();
 | 
			
		||||
                            //按设备名称分组
 | 
			
		||||
                            var groups = datas.Where(a => !string.IsNullOrEmpty(((AlarmVariable)a).DeviceName)).GroupBy(a => ((AlarmVariable)a).DeviceName, a => ((AlarmVariable)a));
 | 
			
		||||
                            foreach (var group in groups)
 | 
			
		||||
                            {
 | 
			
		||||
                                //按采集时间分组
 | 
			
		||||
                                var data = group.GroupBy(a => a.AlarmTime.DateTimeToUnixTimestamp());
 | 
			
		||||
                                var deviceObj = new ExpandoObject();
 | 
			
		||||
                                List<ExpandoObject> expandos = new List<ExpandoObject>();
 | 
			
		||||
                                foreach (var item in data)
 | 
			
		||||
                                {
 | 
			
		||||
                                    var expando = new ExpandoObject();
 | 
			
		||||
                                    expando.TryAdd("ts", item.Key);
 | 
			
		||||
                                    var variableObj = new ExpandoObject();
 | 
			
		||||
                                    foreach (var tag in item)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        var alarmObj = new ExpandoObject();
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.AlarmCode), tag.AlarmCode);
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.AlarmText), tag.AlarmText);
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.AlarmType), tag.AlarmType);
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.AlarmLimit), tag.AlarmLimit);
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.EventTime), tag.EventTime);
 | 
			
		||||
                                        alarmObj.TryAdd(nameof(tag.EventType), tag.EventType);
 | 
			
		||||
 | 
			
		||||
                                        variableObj.TryAdd(tag.Name, alarmObj);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    expando.TryAdd("alarms", variableObj);
 | 
			
		||||
 | 
			
		||||
                                    expandos.Add(expando);
 | 
			
		||||
                                }
 | 
			
		||||
                                deviceObj.TryAdd(group.Key, expandos);
 | 
			
		||||
                                deviceObjs.Add(deviceObj);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return deviceObjs;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    """
 | 
			
		||||
                    :
 | 
			
		||||
                    ""
 | 
			
		||||
                    ;
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        {nameof(ScriptCheck.ScriptChanged),EventCallback.Factory.Create<string>(this, v =>
 | 
			
		||||
        {
 | 
			
		||||
                 if (pname == nameof(BusinessPropertyWithCacheIntervalScript.BigTextScriptAlarmModel))
 | 
			
		||||
@@ -171,3 +311,7 @@ public partial class PropertyComponent : IPropertyUIBase
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private DialogService DialogService { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,8 @@
 | 
			
		||||
        <Alert Icon="fa-solid fa-circle-check" Color="Color.Success">@(new MarkupString("获取变量类实体,可用方法  <code>GlobalData.GetVariable(\"设备名称1\",\"变量名称1\")</code> "))</Alert>
 | 
			
		||||
        <Alert Icon="fa-solid fa-circle-check" Color="Color.Success">@(new MarkupString("详细说明查看文档对应内容页面"))</Alert>
 | 
			
		||||
 | 
			
		||||
        <Button IsAsync OnClick="GetDemo" class="mt-3" Text="Demo" />
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-6  col-md-6">
 | 
			
		||||
        <BootstrapLabel Value=@Localizer["Input"] ShowLabelTooltip="true" />
 | 
			
		||||
 
 | 
			
		||||
@@ -56,4 +56,13 @@ public partial class ScriptCheck
 | 
			
		||||
    }
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private IStringLocalizer<DeviceEditComponent> Localizer { get; set; }
 | 
			
		||||
 | 
			
		||||
    private async Task GetDemo(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
 | 
			
		||||
    {
 | 
			
		||||
        Script = OnGetDemo?.Invoke();
 | 
			
		||||
        await Change(Script);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Parameter, EditorRequired]
 | 
			
		||||
    public Func<string> OnGetDemo { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@
 | 
			
		||||
                    <EditorItem @bind-Field="@context.Name" Readonly=BatchEditEnable />
 | 
			
		||||
 | 
			
		||||
                    <EditorItem @bind-Field="@context.Description" />
 | 
			
		||||
                    <EditorItem @bind-Field="@context.CollectGroup" />
 | 
			
		||||
                    <EditorItem @bind-Field="@context.Group" />
 | 
			
		||||
 | 
			
		||||
                    <EditorItem @bind-Field="@context.Unit" />
 | 
			
		||||
 
 | 
			
		||||
@@ -212,8 +212,8 @@ public partial class VariableEditComponent
 | 
			
		||||
                {
 | 
			
		||||
                    var component = new BootstrapDynamicComponent(data.VariablePropertyUIType, new Dictionary<string, object?>
 | 
			
		||||
                    {
 | 
			
		||||
                        [nameof(VariableEditComponent.Model)] = Model,
 | 
			
		||||
                        [nameof(DeviceEditComponent.PluginPropertyEditorItems)] = data.EditorItems,
 | 
			
		||||
                        [nameof(IPropertyUIBase.Model)] = Model,
 | 
			
		||||
                        [nameof(IPropertyUIBase.PluginPropertyEditorItems)] = data.EditorItems,
 | 
			
		||||
                    });
 | 
			
		||||
                    VariablePropertyRenderFragments.AddOrUpdate(id, component.Render());
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace ThingsGateway.Gateway.Razor;
 | 
			
		||||
[AppStartup(-1000)]
 | 
			
		||||
public class Startup : AppStartup
 | 
			
		||||
{
 | 
			
		||||
    public void ConfigureAdminApp(IServiceCollection services)
 | 
			
		||||
    public void Configure(IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddBootstrapBlazorWinBoxService();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.Gateway.Application\ThingsGateway.Gateway.Application.csproj" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.3" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.4" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.7" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.1" />
 | 
			
		||||
		<ProjectReference Include="..\..\Admin\ThingsGateway.Admin.Razor\ThingsGateway.Admin.Razor.csproj" />
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user