Compare commits

...

113 Commits

Author SHA1 Message Date
2248356998 qq.com
1bad65378f build: 10.11.101 2025-10-14 11:46:26 +08:00
2248356998 qq.com
db3affc67e build: 10.11.100
feat(VariablePage): 优化UI性能
2025-10-14 10:18:09 +08:00
2248356998 qq.com
5ee8b50a92 build: 10.11.99 2025-10-13 22:11:05 +08:00
2248356998 qq.com
301beda2a2 build: 10.11.98 2025-10-13 21:10:39 +08:00
2248356998 qq.com
628b51a353 build: 10.11.97 2025-10-12 15:15:42 +08:00
2248356998 qq.com
f03445bc83 更新依赖 2025-10-12 00:25:57 +08:00
2248356998 qq.com
55a2ff5487 build: 10.11.96 2025-10-11 13:38:28 +08:00
2248356998 qq.com
0fef7dcf3b 更新依赖 2025-10-10 21:41:24 +08:00
2248356998 qq.com
19d9702606 fix: sqlite 批量更新 2025-10-10 14:19:43 +08:00
2248356998 qq.com
a8a9774932 fix: orm 序列化 2025-10-10 12:26:35 +08:00
2248356998 qq.com
aad0f0e8c3 fix: orm批量插入 2025-10-10 11:19:14 +08:00
2248356998 qq.com
e74eae50a7 build: 10.11.87 2025-10-09 20:58:16 +08:00
2248356998 qq.com
3b16d7019f feat: 优化orm 批量插入 2025-10-09 20:57:18 +08:00
2248356998 qq.com
3e038028c2 feat: 优化orm 批量插入 2025-10-09 19:19:18 +08:00
2248356998 qq.com
b1d8041f7e feat: 优化orm BulkCopy 2025-10-09 19:05:33 +08:00
2248356998 qq.com
53a98b26cd fix: 数据库插件保留天数逻辑错误 2025-10-09 08:52:02 +08:00
2248356998 qq.com
42c740fa1b 更新依赖 2025-10-07 22:25:38 +08:00
2248356998 qq.com
556819c90c 10.11.83 2025-09-30 15:25:15 +08:00
2248356998 qq.com
2522333a9c 更新依赖 2025-09-29 23:16:34 +08:00
2248356998 qq.com
bd4ce7c09b 适配net10 2025-09-29 17:47:24 +08:00
2248356998 qq.com
156ed88bd6 10.11.80 2025-09-29 17:09:12 +08:00
2248356998 qq.com
2416226eb0 10.11.78 2025-09-28 17:01:19 +08:00
2248356998 qq.com
976323a716 适配net10 2025-09-28 13:06:39 +08:00
2248356998 qq.com
3c9e397403 10.11.76 2025-09-28 00:33:56 +08:00
2248356998 qq.com
79406ad4a0 更新依赖 2025-09-27 23:59:38 +08:00
2248356998 qq.com
20c44f10ca 10.11.73 2025-09-27 20:18:56 +08:00
2248356998 qq.com
31d6b2a9e6 添加FastMapper 2025-09-26 15:24:37 +08:00
2248356998 qq.com
68e5a9c546 build: 10.11.71 2025-09-26 11:51:05 +08:00
2248356998 qq.com
b2ea9f99b9 适配net10 2025-09-26 10:58:57 +08:00
2248356998 qq.com
6d7d0e468a fix: opcua连接状态 2025-09-24 11:03:23 +08:00
2248356998 qq.com
ff1f632de2 build: 10.11.67 sqldb插件 2025-09-23 22:38:05 +08:00
2248356998 qq.com
fc3d7015ee sqldb插件 清理数据cron表达式错误 2025-09-23 22:36:45 +08:00
2248356998 qq.com
40c5acb522 调整UI 2025-09-23 13:41:20 +08:00
2248356998 qq.com
f9cc1cbb05 10.11.65 2025-09-23 12:02:17 +08:00
2248356998 qq.com
cf6e8b58f0 更新内存变量 2025-09-23 01:22:02 +08:00
2248356998 qq.com
615e3bb24c 修改 脚本调试项目 依赖项 2025-09-22 14:58:03 +08:00
2248356998 qq.com
4a7534b210 添加 脚本调试项目 2025-09-22 10:36:28 +08:00
2248356998 qq.com
58e099cb93 10.11.58 2025-09-21 20:53:50 +08:00
2248356998 qq.com
a94a9c953c 10.11.57 2025-09-19 14:32:34 +08:00
Diego
35e1ffa3e9 添加文件 2025-09-18 17:54:29 +08:00
Diego
4921642151 10.11.56 2025-09-18 17:15:57 +08:00
2248356998 qq.com
d71ee29da8 10.11.54 2025-09-17 20:50:41 +08:00
2248356998 qq.com
901aa2d59f 10.11.52 2025-09-17 11:07:45 +08:00
2248356998 qq.com
764957c014 10.11.51 2025-09-16 22:19:01 +08:00
Diego
0e3898218b 10.11.44 2025-09-16 19:13:19 +08:00
Diego
61f13cef3c 10.11.43 2025-09-15 17:35:49 +08:00
2248356998 qq.com
0b663d9e01 fix: modbusRtu 0x10 2025-09-14 11:18:41 +08:00
2248356998 qq.com
6c95c6209f 10.11.41 2025-09-13 14:22:13 +08:00
2248356998 qq.com
4d223d2622 10.11.40 2025-09-13 13:00:52 +08:00
yunqi
e8d7e91b64 !74 modify dlt645_2007 buildcmd
* modify dlt645_2007 buildcmd
2025-09-13 04:59:10 +00:00
2248356998 qq.com
8175f541ec 更新基准测试 2025-09-12 23:53:36 +08:00
2248356998 qq.com
0adbdb926b 更新基准测试 2025-09-12 23:49:18 +08:00
Diego
42adee9980 更新依赖 2025-09-11 22:22:39 +08:00
2248356998 qq.com
427a7404bc 更新UI 2025-09-11 19:05:14 +08:00
2248356998 qq.com
3658199e0a 10.11.37 2025-09-11 18:30:49 +08:00
Diego
82eedee50a 更新依赖 2025-09-11 18:12:40 +08:00
2248356998 qq.com
6a18fc3e06 10.11.36 2025-09-10 12:59:54 +08:00
2248356998 qq.com
c37e314ed6 更新依赖 2025-09-09 21:53:39 +08:00
2248356998 qq.com
a937a85d90 添加项目 2025-09-08 21:51:37 +08:00
2248356998 qq.com
35dd4ae9d3 更新依赖 2025-09-08 21:27:49 +08:00
2248356998 qq.com
0b829ac85c 10.11.33 2025-09-08 21:16:37 +08:00
2248356998 qq.com
aa247422d2 10.11.31 2025-09-08 17:56:19 +08:00
2248356998 qq.com
2e00e8c135 更新依赖 2025-09-08 08:46:52 +08:00
2248356998 qq.com
34dd2cf0a7 更新依赖 2025-09-07 21:52:41 +08:00
2248356998 qq.com
8404e20c5e build: 10.11.27
refactor: 更新依赖
2025-09-06 23:34:32 +08:00
2248356998 qq.com
662aa162e9 fix: sugar无实体curd 2025-09-05 13:16:18 +08:00
2248356998 qq.com
5927738c32 10.11.25 2025-09-05 11:30:33 +08:00
2248356998 qq.com
3c9f97a5c3 10.11.24 2025-09-04 22:39:21 +08:00
2248356998 qq.com
179ca0aa0e build: 10.11.23
feat: 增加部分demo
feat: 增加管理软件桌面端
2025-09-03 17:40:19 +08:00
Diego
fc09a52da1 build: 10.11.22
refactor:性能优化
2025-09-02 11:13:54 +08:00
Diego
5436b91c89 10.11.21 2025-09-01 18:40:12 +08:00
Diego
d1c46f51a6 添加api接口 2025-09-01 16:00:53 +08:00
Diego
4539d8d198 fix: 字符串反转功能失效 2025-09-01 11:52:03 +08:00
2248356998 qq.com
9ea9529a5f 10.11.19 2025-08-29 15:43:02 +08:00
2248356998 qq.com
4e6be23aac 10.11.17 2025-08-28 17:15:08 +08:00
2248356998 qq.com
2fabbd236b 10.11.16 2025-08-28 17:05:00 +08:00
2248356998 qq.com
163a66530e fix: dlt645校验和 2025-08-28 17:04:27 +08:00
2248356998 qq.com
29073a00c4 fix: dtu连接注册包解析错误 2025-08-28 15:35:43 +08:00
2248356998 qq.com
c6d4d1ecfa fix: dtu连接注册包解析错误 2025-08-28 15:20:00 +08:00
2248356998 qq.com
ba16889cad 10.11.14 2025-08-28 14:21:58 +08:00
2248356998 qq.com
5aaed35b0f 10.11.12 2025-08-28 01:42:59 +08:00
Diego
df067c91eb 10.11.12 2025-08-28 01:39:39 +08:00
2248356998 qq.com
2078b4a60b 补充忽略的文件 2025-08-26 08:54:09 +08:00
2248356998 qq.com
20a2e3ff8e 示例 2025-08-25 20:39:26 +08:00
Diego
61a973b1b5 !73 10.11.10 2025-08-25 12:21:12 +00:00
2248356998 qq.com
cbd72e2081 添加文本日志配置json文件 2025-08-22 16:17:25 +08:00
2248356998 qq.com
4e0377b20c 添加文本日志配置json文件 2025-08-22 16:15:54 +08:00
2248356998 qq.com
fd318d3cdc 移除警告项 2025-08-22 16:12:14 +08:00
2248356998 qq.com
515bdb9700 10.11.7 2025-08-21 23:35:12 +08:00
2248356998 qq.com
46c1780017 10.11.6 2025-08-21 22:40:53 +08:00
2248356998 qq.com
fe78a4c3ca 更新依赖库 2025-08-21 22:05:30 +08:00
2248356998 qq.com
2d7effadf9 10.11.4 2025-08-21 21:13:18 +08:00
Diego
346c560f8b !72 更新依赖项 2025-08-21 11:46:58 +00:00
Diego
8e3bd89f61 修改编译项 2025-08-18 17:30:34 +08:00
Diego
6da142d080 10.10.23 2025-08-18 17:03:40 +08:00
2248356998 qq.com
ff7d029e6f 更新依赖 2025-08-14 20:25:00 +08:00
2248356998 qq.com
21b4695683 10.10.19 2025-08-14 05:47:54 +08:00
Diego
02ad494a26 !71 适配远程客户端 2025-08-12 16:00:53 +00:00
2248356998 qq.com
280366e1b2 添加写优先选项,默认false 2025-08-09 13:07:08 +08:00
2248356998 qq.com
6660ce3e34 适配远程管理客户端 2025-08-08 18:01:24 +08:00
2248356998 qq.com
7499162c1a 适配远程管理客户端 2025-08-08 02:51:42 +08:00
2248356998 qq.com
40208a5cd6 适配远程网关管理客户端 2025-08-08 02:16:05 +08:00
2248356998 qq.com
fa347f4f68 修改报警事件时间字段,增加变量表报警类 2025-08-07 19:19:36 +08:00
Diego
d7df6fc605 10.10.12 2025-08-07 11:24:57 +08:00
2248356998 qq.com
eb4bb2fd48 10.10.11 2025-08-07 10:19:28 +08:00
2248356998 qq.com
faa9858974 10.10.11 2025-08-07 10:18:22 +08:00
2248356998 qq.com
1b3d2dda49 QuestDbRestAPI 2025-08-07 10:11:37 +08:00
Diego
a8a9453611 !70 fix: questdb restapi多实例 2025-08-07 01:58:32 +00:00
2248356998 qq.com
e84f42ce14 10.10.10 2025-08-06 21:42:46 +08:00
2248356998 qq.com
6f814cf6b8 更新questdb restapi启用字段 2025-08-06 21:42:23 +08:00
2248356998 qq.com
e36432e4e9 10.10.9 2025-08-06 19:33:30 +08:00
Diego
ebd71e807b !69 更新依赖 2025-08-06 11:25:31 +00:00
Diego
34000d8d7d !68 10.10.7 2025-08-05 09:22:11 +00:00
1135 changed files with 27810 additions and 14304 deletions

2
.gitignore vendored
View File

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

View File

@@ -11,6 +11,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- 避免 DLL 被打包到 lib/ -->
<EnableSourceGenerator>true</EnableSourceGenerator>
<!-- 可选 -->
@@ -26,6 +27,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" Private="false" />
</ItemGroup>
</Project>

View File

@@ -37,9 +37,8 @@ public class FileController : ControllerBase
var root = Directory.GetCurrentDirectory();
var wwwroot = Path.Combine(root, "wwwroot");
var filePath = Path.Combine(wwwroot, fileName);
// 防止路径穿越攻击
#pragma warning disable CA3003
if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
{
return NotFound();
}
@@ -49,6 +48,6 @@ public class FileController : ControllerBase
Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
}
}

View File

@@ -38,12 +38,14 @@ public class VerificatInfo : PrimaryIdEntity
[AutoGenerateColumn(Filterable = true, Sortable = true)]
[SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
[IgnoreExcel]
[System.ComponentModel.DataAnnotations.Key]
public override long Id { get; set; }
/// <summary>
/// 登录IP
/// </summary>
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 200)]
[SugarColumn(IsNullable = true)]
public string LoginIp { get; set; }
/// <summary>
@@ -77,5 +79,6 @@ public class VerificatInfo : PrimaryIdEntity
/// 登录设备
/// </summary>
[AutoGenerateColumn(Filterable = true, Sortable = true, Width = 100)]
[SugarColumn(IsNullable = true)]
public string Device { get; set; }
}

View File

@@ -21,16 +21,7 @@
"UserNoModule": "This account has not been assigned a module. Please contact the administrator",
"UserNull": "User {0} does not exist"
},
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "CreateOrgId"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"CreateTime": "CreateTime",
"CreateUser": "CreateUser",
"SortCode": "SortCode",
"UpdateTime": "UpdateTime",
"UpdateUser": "UpdateUser"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "User expired, please login again"
},

View File

@@ -21,16 +21,7 @@
"UserNoModule": "该账号未分配模块,请联系管理员",
"UserNull": "用户 {0} 不存在"
},
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "创建机构Id"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"CreateTime": "创建时间",
"CreateUser": "创建人",
"SortCode": "排序",
"UpdateTime": "更新时间",
"UpdateUser": "更新人"
},
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
"UserExpire": "用户登录已过期,请重新登录"
},

View File

@@ -11,6 +11,7 @@
using Riok.Mapperly.Abstractions;
namespace ThingsGateway.Admin.Application;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class AdminMapper
{

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
namespace ThingsGateway.Admin.Application;
public class USheetDatas
{

View File

@@ -145,7 +145,7 @@ public class AdminOAuthHandler<TOptions>(
var loginEvent = new LoginEvent
{
Ip = appService.RemoteIpAddress,
Device = appService.UserAgent?.Platform,
Device = appService.UserAgent?.Platform ?? "Unknown",
Expire = expire,
SysUser = sysUser,
VerificatId = CommonUtils.GetSingleId()
@@ -156,7 +156,7 @@ public class AdminOAuthHandler<TOptions>(
//生成verificat信息
var verificatInfo = new VerificatInfo
{
Device = loginEvent.Device,
Device = loginEvent.Device ?? "Unknown",
Expire = loginEvent.Expire,
VerificatTimeout = tokenTimeout,
Id = loginEvent.VerificatId,

View File

@@ -26,7 +26,7 @@
"Module": 2,
"Title": "权限管理",
"Code": "System",
"NavLinkMatch": "All",
"NavLinkMatch": "Prefix",
"Category": "MENU",
"Target": "_self",
"Href": null,
@@ -47,7 +47,7 @@
"ParentId": 0,
"Module": 2,
"Title": "系统运维",
"NavLinkMatch": "All",
"NavLinkMatch": "Prefix",
"Code": "System",
"Category": "MENU",
"Target": "_self",

View File

@@ -235,7 +235,7 @@ public class AuthService : IAuthService
var logingEvent = new LoginEvent
{
Ip = _appService.RemoteIpAddress,
Device = _appService.UserAgent?.Platform,
Device = _appService.UserAgent?.Platform ?? "Unknown",
Expire = expire,
SysUser = sysUser,
VerificatId = verificatId
@@ -344,7 +344,7 @@ public class AuthService : IAuthService
//生成verificat信息
var verificatInfo = new VerificatInfo
{
Device = loginEvent.Device,
Device = loginEvent.Device ?? "Unknown",
Expire = loginEvent.Expire,
VerificatTimeout = tokenTimeout,
Id = loginEvent.VerificatId,

View File

@@ -9,6 +9,7 @@
//------------------------------------------------------------------------------
namespace ThingsGateway.Admin.Application;
internal sealed class NoticeService : INoticeService
{
private IEventService<AppMessage>? MessageDispatchService { get; set; }

View File

@@ -282,7 +282,7 @@ internal sealed class SysRoleService : BaseService<SysRole>, ISysRoleService
if (sysRole != null)
{
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
#region

View File

@@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity
/// <summary>
/// 主键Id
/// </summary>
[System.ComponentModel.DataAnnotations.Key]
public override long Id { get; set; }
/// <summary>

View File

@@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
/// 获取用户拥有的资源
/// </summary>
/// <param name="id">用户id</param>
public async Task<GrantResourceData> OwnResourceAsync(long id)
public Task<GrantResourceData> OwnResourceAsync(long id)
{
return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false);
return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource);
}
/// <summary>
@@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下
using var db = GetDB();
//重置密码
if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
{
Password = password
}, it => it.Id == id).ConfigureAwait(false))
}, it => it.Id == id).ConfigureAwait(false)) > 0)
{
DeleteUserFromCache(id);//从cache删除用户信息
var verificatInfoIds = _verificatInfoService.GetListByUserId(id);
@@ -550,7 +550,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
if (sysUser != null)
{
var resources = await _sysResourceService.GetAllAsync().ConfigureAwait(false);
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu).Where(a => menuIds.Contains(a.Id));
var menusList = resources.Where(a => a.Category == ResourceCategoryEnum.Menu && menuIds.Contains(a.Id));
#region

View File

@@ -185,12 +185,12 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
using var db = GetDB();
//更新指定字段
var result = await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
var result = (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
{
Email = input.Email,
Phone = input.Phone,
Avatar = input.Avatar,
}, it => it.Id == UserManager.UserId).ConfigureAwait(false);
}, it => it.Id == UserManager.UserId).ConfigureAwait(false)) > 0;
if (result)
_userService.DeleteUserFromCache(UserManager.UserId);//cache删除用户数据
}

View File

@@ -119,7 +119,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void Add(VerificatInfo verificatInfo)
{
using var db = GetDB();
db.Insertable<VerificatInfo>(verificatInfo).ExecuteCommand();
db.InsertableT<VerificatInfo>(verificatInfo).ExecuteCommand();
VerificatInfoService.RemoveCache(verificatInfo.Id);
if (verificatInfo != null)
VerificatInfoService.SetCahce(verificatInfo);
@@ -132,7 +132,7 @@ internal sealed class VerificatInfoService : BaseService<VerificatInfo>, IVerifi
public void Update(VerificatInfo verificatInfo)
{
using var db = GetDB();
db.Updateable<VerificatInfo>(verificatInfo).ExecuteCommand();
db.UpdateableT<VerificatInfo>(verificatInfo).ExecuteCommand();
VerificatInfoService.RemoveCache(verificatInfo.Id);
if (verificatInfo != null)
VerificatInfoService.SetCahce(verificatInfo);

View File

@@ -5,9 +5,10 @@
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
@@ -19,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
<PackageReference Include="Rougamo.Fody" Version="5.0.2" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
@@ -27,10 +28,10 @@
<PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET9Version)" />
<PackageReference Include="System.Formats.Asn1" Version="$(NET9Version)" />
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET9Version)" />
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(NET10Version)" />
<PackageReference Include="System.Formats.Asn1" Version="$(NET10Version)" />
<PackageReference Include="System.Threading.RateLimiting" Version="$(NET10Version)" />
</ItemGroup>
<ItemGroup>
<Content Remove="SeedData\Admin\*.json" />

View File

@@ -0,0 +1,57 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Admin.Application;
public static class USheetDataHelpers
{
public static USheetDatas GetUSheetDatas(Dictionary<string, object> data)
{
var uSheetDatas = new USheetDatas();
foreach (var a in data)
{
var value = (a.Value as IEnumerable<Dictionary<string, object>>).ToList();
var uSheetData = new USheetData();
uSheetData.id = a.Key;
uSheetData.name = a.Key;
for (int row1 = 0; row1 < value.Count; row1++)
{
if (row1 == 0)
{
Dictionary<int, USheetCelldata> usheetColldata = new();
int col = 0;
foreach (var colData in value[row1])
{
usheetColldata.Add(col, new USheetCelldata() { v = colData.Key });
col++;
}
uSheetData.cellData.Add(row1, usheetColldata);
}
{
Dictionary<int, USheetCelldata> usheetColldata = new();
int col = 0;
foreach (var colData in value[row1])
{
usheetColldata.Add(col, new USheetCelldata() { v = colData.Value });
col++;
}
uSheetData.cellData.Add(row1 + 1, usheetColldata);
}
}
uSheetData.rowCount = uSheetData.cellData.Count + 100;
uSheetData.columnCount = uSheetData.cellData.FirstOrDefault().Value?.Count ?? 0;
uSheetDatas.sheets.Add(a.Key, uSheetData);
}
return uSheetDatas;
}
}

View File

@@ -4,9 +4,9 @@
<div class="tg-table h-100">
<Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
DataService="DataService" CreateItemCallback="CreateItemCallback!"
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" BeforeShowEditDialogCallback="BeforeShowEditDialogCallback!"
<Table TItem="TItem" IsBordered="true" IsStriped="true" TableSize="TableSize.Compact" SelectedRows=SelectedRows SelectedRowsChanged=privateSelectedRowsChanged IsMultipleSelect="IsMultipleSelect" @ref="Instance" SearchTemplate="SearchTemplate"
DataService="DataService" CreateItemCallback="CreateItemCallback!" RenderMode=RenderMode
IsPagination="IsPagination" PageItemsSource="PageItemsSource" IsFixedHeader="IsFixedHeader" IndentSize=24 RowHeight=RowHeight ShowSearchText="ShowSearchText" ShowSearchButton="ShowSearchButton" DisableEditButtonCallback="DisableEditButtonCallback" DisableDeleteButtonCallback="DisableDeleteButtonCallback" BeforeShowEditDialogCallback=" BeforeShowEditDialogCallback!"
IsTree="IsTree" OnTreeExpand="OnTreeExpand!" TreeNodeConverter="TreeNodeConverter!" TreeIcon="fa-solid fa-circle-chevron-right" TreeExpandIcon="fa-solid fa-circle-chevron-right fa-rotate-90" IsAutoQueryFirstRender=IsAutoQueryFirstRender
ShowDefaultButtons="ShowDefaultButtons" ShowAdvancedSearch="ShowAdvancedSearch" ShowResetButton=ShowResetButton
ShowEmpty="ShowEmpty" EmptyText="@EmptyText" EmptyImage="@($"{WebsiteConst.DefaultResourceUrl}images/empty.svg")" SortString="@SortString" EditDialogSize="EditDialogSize"
@@ -14,7 +14,7 @@
ShowSkeleton="true" ShowLoading="ShowLoading" ShowSearch="ShowSearch" SearchModel=@SearchModel ShowLineNo
SearchMode=SearchMode ShowExportPdfButton=ShowExportPdfButton ExportButtonText=@ExportButtonText
ShowExportButton=@ShowExportButton Items=Items ClickToSelect=ClickToSelect ScrollMode=ScrollMode
ShowExportCsvButton=@ShowExportCsvButton SelectedRowsChanged=SelectedRowsChanged ShowCardView=ShowCardView
ShowExportCsvButton=@ShowExportCsvButton ShowCardView=ShowCardView
FixedExtendButtonsColumn=FixedExtendButtonsColumn FixedMultipleColumn=FixedMultipleColumn FixedDetailRowHeaderColumn=FixedDetailRowHeaderColumn FixedLineNoColumn=FixedLineNoColumn
IsAutoRefresh=IsAutoRefresh AutoRefreshInterval=AutoRefreshInterval
AllowDragColumn=@AllowDragColumn Height=@Height ShowRefresh=ShowRefresh
@@ -29,7 +29,7 @@
ShowMultiFilterHeader=ShowMultiFilterHeader
ShowFilterHeader=ShowFilterHeader
ShowColumnList=ShowColumnList ExtendButtonColumnWidth="@ExtendButtonColumnWidth"
CustomerSearchModel="CustomerSearchModel" SelectedRows="SelectedRows" ModelEqualityComparer="ModelEqualityComparer!"
CustomerSearchModel="CustomerSearchModel" ModelEqualityComparer="ModelEqualityComparer!"
ShowExtendEditButtonCallback="ShowExtendEditButtonCallback!" ShowExtendDeleteButtonCallback="ShowExtendDeleteButtonCallback!"
DisableExtendEditButton="DisableExtendEditButton!" DisableExtendDeleteButton="DisableExtendDeleteButton!"
DisableExtendEditButtonCallback="DisableExtendEditButtonCallback!" DisableExtendDeleteButtonCallback="DisableExtendDeleteButtonCallback!"
@@ -41,6 +41,7 @@
DoubleClickToEdit="DoubleClickToEdit"
OnDoubleClickCellCallback="OnDoubleClickCellCallback"
OnDoubleClickRowCallback="OnDoubleClickRowCallback"
RowContentTemplate="RowContentTemplate"
OnClickRowCallback="OnClickRowCallback">
</Table>
</div>

View File

@@ -13,6 +13,32 @@ namespace ThingsGateway.Admin.Razor;
[CascadingTypeParameter(nameof(TItem))]
public partial class AdminTable<TItem> where TItem : class, new()
{
/// <inheritdoc cref="Table{TItem}.RenderMode"/>
[Parameter]
public TableRenderMode RenderMode { get; set; }
public List<ITableColumn> Columns => Instance?.Columns;
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
[Parameter]
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
[Parameter]
public List<TItem> SelectedRows { get; set; } = new();
private async Task privateSelectedRowsChanged(List<TItem> items)
{
SelectedRows = items;
if (SelectedRowsChanged.HasDelegate)
await SelectedRowsChanged.InvokeAsync(items);
}
/// <inheritdoc cref="Table{TItem}.DoubleClickToEdit"/>
[Parameter]
public bool DoubleClickToEdit { get; set; } = false;
@@ -22,6 +48,10 @@ public partial class AdminTable<TItem> where TItem : class, new()
/// <inheritdoc cref="Table{TItem}.OnDoubleClickRowCallback"/>
[Parameter]
public Func<TItem, Task>? OnDoubleClickRowCallback { get; set; }
/// <inheritdoc cref="Table{TItem}.RowContentTemplate"/>
[Parameter]
public RenderFragment<TableRowContext<TItem>>? RowContentTemplate { get; set; }
/// <inheritdoc cref="Table{TItem}.OnClickRowCallback"/>
[Parameter]
public Func<TItem, Task>? OnClickRowCallback { get; set; }
@@ -210,14 +240,6 @@ public partial class AdminTable<TItem> where TItem : class, new()
[Parameter]
public RenderFragment<TItem>? SearchTemplate { get; set; }
/// <inheritdoc cref="Table{TItem}.SelectedRows"/>
[Parameter]
public List<TItem>? SelectedRows { get; set; } = new List<TItem>();
/// <inheritdoc cref="Table{TItem}.SelectedRowsChanged"/>
[Parameter]
public EventCallback<List<TItem>> SelectedRowsChanged { get; set; }
/// <inheritdoc cref="Table{TItem}.SetRowClassFormatter"/>
[Parameter]
public Func<TItem, string?>? SetRowClassFormatter { get; set; }
@@ -266,6 +288,15 @@ public partial class AdminTable<TItem> where TItem : class, new()
[Parameter]
public bool ShowExportButton { get; set; } = false;
/// <inheritdoc cref="Table{TItem}.DisableEditButtonCallback"/>
public Func<List<TItem>, bool> DisableEditButtonCallback { get; set; } = (list) =>
list.Count != 1;
/// <inheritdoc cref="Table{TItem}.DisableDeleteButtonCallback"/>
[Parameter]
public Func<List<TItem>, bool> DisableDeleteButtonCallback { get; set; } = (list) =>
list.Count <= 0;
/// <inheritdoc cref="Table{TItem}.ShowExportCsvButton"/>
[Parameter]
public bool ShowExportCsvButton { get; set; } = false;

View File

@@ -1,7 +1,6 @@
@namespace ThingsGateway.Gateway.Razor
@namespace ThingsGateway.Admin.Razor
@using ThingsGateway.Admin.Application
@using ThingsGateway.Admin.Razor
@using ThingsGateway.Gateway.Application
<div class="h-600px">
<UniverSheet @ref="_sheetExcel" OnReadyAsync="OnReadyAsync"></UniverSheet>

View File

@@ -8,9 +8,10 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Admin.Application;
using ThingsGateway.NewLife.Json.Extension;
namespace ThingsGateway.Gateway.Razor;
namespace ThingsGateway.Admin.Razor;
public partial class USheet
{

View File

@@ -30,7 +30,7 @@ public class BlazorAppContext
/// <summary>
/// 全部菜单
/// </summary>
public IEnumerable<SysResource> AllMenus { get; private set; }
public List<SysResource> AllMenus { get; private set; }
/// <summary>
/// 当前用户
@@ -42,22 +42,22 @@ public class BlazorAppContext
/// <summary>
/// 用户个人菜单
/// </summary>
public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
public List<MenuItem> OwnMenuItems { get; private set; }
/// <summary>
/// 不同模块的菜单
/// </summary>
public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
public List<MenuItem> AllOwnMenuItems { get; private set; }
/// <summary>
/// 用户个人菜单,多个模块
/// </summary>
public IEnumerable<SysResource> OwnMenus { get; private set; }
public List<SysResource> OwnMenus { get; private set; }
/// <summary>
/// 用户个人菜单,非树形
/// </summary>
public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
/// <summary>
/// 个人工作台
@@ -67,9 +67,9 @@ public class BlazorAppContext
/// <summary>
/// 用户个人快捷方式菜单
/// </summary>
public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
public List<SysResource> UserWorkbenchOutputs { get; private set; }
public IEnumerable<SysResource> AllResource { get; private set; }
public List<SysResource> AllResource { get; private set; }
private ISysResourceService ResourceService { get; }
private ISysUserService SysUserService { get; }
@@ -93,7 +93,7 @@ public class BlazorAppContext
AllResource = sysResources;
var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
if (moduleId == null)
{
@@ -123,17 +123,26 @@ public class BlazorAppContext
}
}
var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item =>
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
});
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
var menu = new MenuItem()
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
};
if (menu.Url.IsNullOrEmpty())
{
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
}
return menu;
}).ToList();
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
}
}

View File

@@ -29,7 +29,7 @@ public partial class EditPagePolicy
protected override Task OnParametersSetAsync()
{
ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
return base.OnParametersSetAsync();
}
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
{
await Task.CompletedTask;
ShortcutsSearchText = searchText;
return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
}
}

View File

@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
var all = (await SysResourceService.GetAllAsync());
var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
await base.OnParametersSetAsync();
}

View File

@@ -26,12 +26,12 @@
OnQueryAsync="OnQueryAsync" CustomerSearchModel="@CustomerSearchModel"
OnSaveAsync="Save" OnDeleteAsync="Delete">
<TableToolbarTemplate>
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count<=0||!AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count <= 0 || !AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
<BodyTemplate>
<Select Items="ModuleSelectedItems" @bind-Value=CopyModule ShowLabel="false" />
</BodyTemplate>
</PopConfirmButton>
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count != 1 || !AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
<BodyTemplate>
<div class="overflow-y-auto" style="height:500px">
<TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />

View File

@@ -39,8 +39,8 @@ public partial class SysResourcePage
protected override async Task OnParametersSetAsync()
{
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
await base.OnParametersSetAsync();
}
@@ -49,7 +49,7 @@ public partial class SysResourcePage
private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
{
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
return data;
@@ -136,14 +136,14 @@ public partial class SysResourcePage
private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
{
var sysResources = await SysResourceService.GetAllAsync();
var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
return result;
}
private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
{
await Task.CompletedTask;
var result = ResourceUtil.BuildTableTrees(items, 0);
var result = AdminResourceUtil.BuildTableTrees(items, 0);
return result;
}

View File

@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
{
var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
await base.OnInitializedAsync();
}

View File

@@ -35,7 +35,7 @@ public partial class SysUserEdit
BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
Items = PositionUtil.BuildCascaderItemList(items);
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
await InvokeAsync(StateHasChanged);
await base.OnInitializedAsync();
}

View File

@@ -15,7 +15,7 @@
</Card>
</div>
<div class="col-12 col-sm-10 h-100">
<div class="col-12 col-sm-10 h-100 ps-2">
<AdminTable @ref=table TItem="SysUser"
AutoGenerateColumns="true"
ShowAdvancedSearch=false

View File

@@ -20,5 +20,6 @@ public class Startup : AppStartup
services.AddScoped<IMenuService, MenuService>();
services.AddScoped<IAuthRazorService, AuthRazorService>();
services.AddBootstrapBlazorTableExportService();
services.AddBootstrapBlazorWinBoxService();
}
}

View File

@@ -5,18 +5,22 @@
<ItemGroup>
<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.3" />
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.7" />
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET8Version)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net9.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET9Version)" />
<ItemGroup Condition="'$(TargetFramework)'=='net10.0'">
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(NET10Version)" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
</PropertyGroup>
<ItemGroup>

View File

@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
/// <inheritdoc/>
[ThingsGateway.DependencyInjection.SuppressSniffer]
public static class ResourceUtil
public static class AdminResourceUtil
{
/// <summary>
/// 构造选择项ID/TITLE
@@ -41,15 +41,22 @@ public static class ResourceUtil
return items
.Where(it => it.ParentId == parentId)
.Select((item, index) =>
new MenuItem()
{
var menu = new MenuItem()
{
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix,
Text = item.Title,
Icon = item.Icon,
Url = item.Href,
Target = item.Target.ToString(),
Items = BuildMenuTrees(items, item.Id).ToList()
};
if (menu.Url.IsNullOrEmpty())
{
menu.Match = Microsoft.AspNetCore.Components.Routing.NavLinkMatch.Prefix;
}
return menu;
}
);
}

View File

@@ -29,7 +29,7 @@
<Target Name="AdminPostPublish" AfterTargets="Publish">
<ItemGroup>
<!-- setting up the variable for convenience -->
<AdminFiles Include="bin\$(Configuration)\$(TargetFramework)\SeedData\**" />
<AdminFiles Include="$(OutputPath)\$(TargetFramework)\SeedData\**" />
</ItemGroup>
<PropertyGroup>
</PropertyGroup>

View File

@@ -5,7 +5,8 @@
#推送docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
#aspnetcore9.0环境
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
COPY . /app
WORKDIR /app
#默认web
@@ -13,6 +14,8 @@ EXPOSE 5000
# 添加时区环境变量,亚洲,上海
ENV TimeZone=Asia/Shanghai
# 转发头
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
# 使用软连接,并且将时区配置覆盖/etc/timezone
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone

View File

@@ -5,7 +5,8 @@
#推送docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
#aspnetcore9.0环境
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
COPY . /app
WORKDIR /app
#默认web
@@ -13,6 +14,8 @@ EXPOSE 5000
# 添加时区环境变量,亚洲,上海
ENV TimeZone=Asia/Shanghai
# 转发头
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
# 使用软连接,并且将时区配置覆盖/etc/timezone
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone

View File

@@ -21,11 +21,13 @@
<link rel="apple-touch-icon" href="favicon.png">
<base href="/" />
<title>ThingsGateway</title>
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css?v={this.GetType().Assembly.GetName().Version}") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css") />
<link rel="stylesheet" href=@($"_content/BootstrapBlazor/css/motronic.min.css") />
<link rel="stylesheet" href=@($"ThingsGateway.AdminServer.styles.css") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/site.css") />
<link rel="stylesheet" href=@($"{WebsiteConst.DefaultResourceUrl}css/devui.css") />
@* <script src=@($"{WebsiteConst.DefaultResourceUrl}js/theme.js") type="module"></script><!-- 初始主题 --> *@
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json" />
@@ -38,12 +40,13 @@
<BlazorReconnector @rendermode="new InteractiveServerRenderMode(false)" />
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js?v={this.GetType().Assembly.GetName().Version}")></script>
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js?v={this.GetType().Assembly.GetName().Version}")></script>
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js")></script>
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
<script src="_framework/blazor.web.js"></script>
<!-- PWA Service Worker -->
<script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
<script src="pwa-install.js"></script>
</body>
</html>

View File

@@ -70,7 +70,7 @@
<Button @onclick="ShowAbout" class="layout-header-bar d-none d-lg-flex px-2" Icon="fa fa-info" Color="Color.None" TooltipText="@Localizer[nameof(About)]" />
}
@* 版本号 *@
<div class="px-1 navbar-header-text d-none d-lg-block">@_versionString</div>
<div class="px-1 navbar-header-text text-nowrap d-none d-lg-block">@_versionString</div>
@* 主题切换 *@
@* <ThemeToggle /> *@
@@ -89,12 +89,19 @@
</div>
</Side>
<Main>
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowContextMenuFullScreen="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
{
return Task.FromResult(!(a.Url=="/"||a.Url.IsNullOrEmpty()));
})>
<Tab @ref=_tab ClickTabToNavigation="true" ShowToolbar="true" ShowContextMenu="true" ShowExtendButtons="false" ShowClose="true" AllowDrag=true
ShowFullscreenToolbarButton=false ShowContextMenuFullScreen=false ShowFullScreen=false AdditionalAssemblies="@App.RazorAssemblies" Menus="@MenuService.AllOwnMenuItems"
DefaultUrl=@("/") Body=@(Body!) OnCloseTabItemAsync=@((a)=>
{
return Task.FromResult(!(a.Url == "/" || a.Url.IsNullOrEmpty()));
})
>
<BeforeContextMenuTemplate>
<ContextMenuItem Icon="fa fa-window-restore" Text="@Localizer["WindowRestore"]" OnClick="WinboxRender"></ContextMenuItem>
<ContextMenuDivider></ContextMenuDivider>
</BeforeContextMenuTemplate>
</Tab>
</Main>
<NotAuthorized>

View File

@@ -120,6 +120,38 @@ public partial class MainLayout : IDisposable
#endregion
private async Task WinboxRender(ContextMenuItem item, object? context)
{
if (context is TabItem tabItem)
{
await WinboxRender(tabItem.ChildContent, tabItem.Text);
//await _tab.RemoveTab(tabItem);
}
}
[Inject]
[NotNull]
private WinBoxService? WinBoxService { get; set; }
private async Task WinboxRender(RenderFragment item, string title)
{
if (item != null)
{
var option = new WinBoxOption()
{
Title = title,
ContentTemplate = item,
Max = false,
Width = "80%",
Height = "80%",
Top = "0%",
Left = "10%",
Background = "var(--bb-primary-color)",
Overflow = true
};
await WinBoxService.Show(option);
}
}
private string _versionString = string.Empty;
[Inject]
[NotNull]

View File

@@ -45,7 +45,7 @@
</app>
<script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
<script src="_framework/blazor.server.js"></script>
<!-- PWA Service Worker -->

View File

@@ -15,6 +15,7 @@ using System.Text;
using ThingsGateway.Admin.Application;
using ThingsGateway.DB;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.AdminServer;
@@ -64,7 +65,7 @@ public class Program
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
builder.Host.UseSystemd();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (Runtime.IsLegacyWindows)
builder.Logging.ClearProviders(); //去除默认的事件日志提供者,某些情况下会日志输出异常,导致程序崩溃
}).ConfigureBuilder(builder =>
{

View File

@@ -45,11 +45,11 @@ public class Startup : AppStartup
options.ServicesStopConcurrently = true;
});
//// 事件总线
//services.AddEventBus(options =>
//{
// 事件总线
services.AddEventBus(options =>
{
//});
});
// 任务调度
services.AddSchedule(options => options.AddPersistence<JobPersistence>());
@@ -132,7 +132,11 @@ public class Startup : AppStartup
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
#if NET10_0_OR_GREATER
options.KnownIPNetworks.Clear();
#else
options.KnownNetworks.Clear();
#endif
options.KnownProxies.Clear();
});
@@ -183,25 +187,37 @@ public class Startup : AppStartup
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
if (!NewLife.Runtime.IsLegacyWindows)
{
#if NET9_0_OR_GREATER
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#else
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
}
}
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
{
var app = (WebApplication)applicationBuilder;
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All, KnownNetworks = { }, KnownProxies = { } });
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All,
#if NET10_0_OR_GREATER
KnownIPNetworks = { },
#else
KnownNetworks = { },
#endif
KnownProxies = { }
});
app.UseBootstrapBlazor();
// 启用本地化

View File

@@ -4,7 +4,8 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
@@ -52,9 +53,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET9Version)" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET9Version)" />
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="$(NET10Version)" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(NET10Version)" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,34 @@
let installPromptTriggered = false;
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
function hasShownInstallPrompt() {
return getCookie("tgPWAInstallPromptShown") === "true";
}
function markInstallPromptShown() {
document.cookie = "tgPWAInstallPromptShown=true; max-age=31536000; path=/";
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
if (!hasShownInstallPrompt() && !installPromptTriggered) {
installPromptTriggered = true;
setTimeout(() => {
e.prompt()
.then(() => e.userChoice)
.then(choiceResult => {
markInstallPromptShown();
})
.catch(err => {
// 可选错误处理
});
}, 2000); // 延迟 2 秒提示
} else {
// console.log("已提示过安装,不再弹出");
}
});

View File

@@ -30,9 +30,27 @@ public class ImportPreviewOutputBase
/// <summary>
/// 返回状态
/// </summary>
public ConcurrentList<(int Row, bool Success, string? ErrorMessage)> Results { get; set; } = new();
public ConcurrentList<ImportPreviewResult> Results { get; set; } = new();
}
public class ImportPreviewResult
{
public ImportPreviewResult()
{
}
public ImportPreviewResult(int row, bool success, string error)
{
this.Row = row;
this.Success = success;
this.ErrorMessage = error;
}
public int Row { get; set; }
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
}
/// <summary>
/// 导入预览
/// </summary>

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
namespace ThingsGateway.Common;
public class SmartTriggerScheduler
{

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
namespace ThingsGateway.Common;
public sealed class StringOrdinalIgnoreCaseEqualityComparer : EqualityComparer<string>
{

View File

@@ -31,8 +31,8 @@ public static class GenericExtensions
// 比较oldModel和model的属性找出差异
var differences = properties
.Where(prop => prop.CanRead && prop.CanWrite) // 确保属性可读可写
.Where(prop => !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 找出值不同的属性
.Where(prop => prop.CanRead && prop.CanWrite && !Equals(prop.GetValue(oldModel), prop.GetValue(model))) // 确保属性可读可写
// 找出值不同的属性
.ToDictionary(prop => prop.Name, prop => prop.GetValue(model)); // 将属性名和新值存储到字典中
// 应用差异到channels列表中的每个Channel对象

View File

@@ -16,6 +16,8 @@ using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using ThingsGateway.Extension;
namespace ThingsGateway.Common.Extension;
/// <summary>
/// 对象拓展类
@@ -48,113 +50,7 @@ public static class ObjectExtensions
bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type);
}
/// <summary>
/// 将 DateTimeOffset 转换成本地 DateTime
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
{
if (dateTime.Offset.Equals(TimeSpan.Zero))
return dateTime.UtcDateTime;
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
return dateTime.ToLocalTime().DateTime;
else
return dateTime.DateTime;
}
/// <summary>
/// 将 DateTimeOffset? 转换成本地 DateTime?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
}
/// <summary>
/// 将 DateTime 转换成 DateTimeOffset
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
{
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}
/// <summary>
/// 将 DateTime? 转换成 DateTimeOffset?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
}
/// <summary>
/// 将流保存到本地磁盘
/// </summary>
/// <param name="stream"></param>
/// <param name="path"></param>
/// <returns></returns>
public static void CopyToSave(this Stream stream, string path)
{
// 空检查
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
using var fileStream = File.Create(path);
stream.CopyTo(fileStream);
}
/// <summary>
/// 将字节数组保存到本地磁盘
/// </summary>
/// <param name="bytes"></param>
/// <param name="path"></param>
/// <returns></returns>
public static void CopyToSave(this byte[] bytes, string path)
{
using var stream = new MemoryStream(bytes);
stream.CopyToSave(path);
}
/// <summary>
/// 将流保存到本地磁盘
/// </summary>
/// <param name="stream"></param>
/// <param name="path">需包含文件名完整路径</param>
/// <returns></returns>
public static async Task CopyToSaveAsync(this Stream stream, string path)
{
// 空检查
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(nameof(path));
}
// 文件名判断
if (string.IsNullOrWhiteSpace(Path.GetFileName(path)))
{
throw new ArgumentException("The parameter of <path> parameter must include the complete file name.");
}
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
/// <summary>
/// 将字节数组保存到本地磁盘
/// </summary>
/// <param name="bytes"></param>
/// <param name="path"></param>
/// <returns></returns>
public static async Task CopyToSaveAsync(this byte[] bytes, string path)
{
using var stream = new MemoryStream(bytes);
await stream.CopyToSaveAsync(path).ConfigureAwait(false);
}
/// <summary>
/// 合并两个字典

View File

@@ -24,7 +24,7 @@ public static class ParallelExtensions
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T> body)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body)
{
ParallelOptions options = new();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
@@ -38,7 +38,7 @@ public static class ParallelExtensions
/// <typeparam name="T">集合元素类型</typeparam>
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T, ParallelLoopState, long> body)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T, ParallelLoopState, long> body)
{
ParallelOptions options = new();
options.MaxDegreeOfParallelism = Environment.ProcessorCount;
@@ -53,7 +53,7 @@ public static class ParallelExtensions
/// <param name="source">要操作的集合</param>
/// <param name="body">要执行的操作</param>
/// <param name="parallelCount">最大并行度</param>
public static void ParallelForEach<T>(this IList<T> source, Action<T> body, int parallelCount)
public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> body, int parallelCount)
{
// 创建并行操作的选项对象,设置最大并行度为指定的值
var options = new ParallelOptions();
@@ -109,7 +109,7 @@ public static class ParallelExtensions
/// <param name="parallelCount">最大并行度</param>
/// <param name="cancellationToken">取消操作的标志</param>
/// <returns>表示异步操作的任务</returns>
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, int parallelCount, CancellationToken cancellationToken = default)
{
// 创建并行操作的选项对象,设置最大并行度和取消标志
var options = new ParallelOptions();
@@ -126,7 +126,7 @@ public static class ParallelExtensions
/// <param name="body">异步执行的操作</param>
/// <param name="cancellationToken">取消操作的标志</param>
/// <returns>表示异步操作的任务</returns>
public static Task ParallelForEachAsync<T>(this IList<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, CancellationToken, ValueTask> body, CancellationToken cancellationToken = default)
{
return ParallelForEachAsync(source, body, Environment.ProcessorCount, cancellationToken);
}

View File

@@ -27,11 +27,11 @@ public class WebsiteOptions : IConfigurableOptions
/// </summary>
public bool Demo { get; set; }
public bool WebPageEnable { get; set; } = true;
public int MaxBlazorConnections { get; set; } = 5;
public bool BlazorConnectionLimitEnable { get; set; } = false;
/// <summary>
/// 是否显示关于页面
/// </summary>

View File

@@ -5,15 +5,16 @@
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.7" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
<PackageReference Include="BootstrapBlazor" Version="9.9.0" />
<PackageReference Include="BootstrapBlazor" Version="9.11.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -23,13 +23,19 @@ public abstract class PrimaryIdEntity : IPrimaryIdEntity
[SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
[IgnoreExcel]
[AutoGenerateColumn(Visible = false, IsVisibleWhenEdit = false, IsVisibleWhenAdd = false, Sortable = true, DefaultSort = true, DefaultSortOrder = SortOrder.Asc)]
[System.ComponentModel.DataAnnotations.Key]
public virtual long Id { get; set; }
}
public interface IPrimaryKeyEntity
{
string ExtJson { get; set; }
}
/// <summary>
/// 主键实体基类
/// </summary>
public abstract class PrimaryKeyEntity : PrimaryIdEntity
public abstract class PrimaryKeyEntity : PrimaryIdEntity, IPrimaryKeyEntity
{
/// <summary>
/// 拓展信息

View File

@@ -9,6 +9,7 @@
// ------------------------------------------------------------------------------
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;
namespace ThingsGateway.DB;
@@ -41,4 +42,31 @@ public static class FileExtensions
}
return fileName;
}
/// <summary>
/// 存储本地文件
/// </summary>
/// <param name="pPath">存储的第一层目录</param>
/// <param name="file"></param>
/// <returns>文件全路径</returns>
public static async Task<string> StorageLocal(this IFormFile file, string pPath = "imports")
{
string uploadFileFolder = App.WebHostEnvironment?.WebRootPath ?? "wwwroot"!;//赋值路径
var now = CommonUtils.GetSingleId();
var filePath = Path.Combine(uploadFileFolder, pPath);
if (!Directory.Exists(filePath))//如果不存在就创建文件夹
Directory.CreateDirectory(filePath);
//var fileSuffix = Path.GetExtension(file.Name).ToLower();// 文件后缀
var fileObjectName = $"{now}{file.Name}";//存储后的文件名
var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路径
fileName = fileName.Replace("\\", "/");//格式化一系
//存储文件
using (var stream = File.Create(Path.Combine(filePath, fileObjectName)))
{
await file.CopyToAsync(stream).ConfigureAwait(false);
}
return fileName;
}
}

View File

@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
return datas;
}
public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
{
if (queryFunc != null)
@@ -123,7 +125,36 @@ public static class QueryPageOptionsExtensions
};
var items = datas.GetData(option, out var totalCount, where);
ret.TotalCount = totalCount;
if (totalCount > 0)
{
if (!items.Any() && option.PageIndex != 1)
{
option.PageIndex = 1;
items = datas.GetData(option, out totalCount, where);
}
}
ret.Items = items.ToList();
return ret;
}
/// <summary>
/// 根据查询条件返回QueryData
/// </summary>
public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
{
var ret = new QueryData<SelectedItem>()
{
IsSorted = false,
IsFiltered = false,
IsAdvanceSearch = false,
IsSearch = !option.SearchText.IsNullOrWhiteSpace()
};
var items = datas.Skip((option.StartIndex)).Take(option.Count);
ret.TotalCount = datas.Count();
ret.Items = func(items).ToList();
return ret;
}
}

View File

@@ -209,16 +209,10 @@ public static class SqlSugarExtensions
}
/// <inheritdoc/>
public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new()
public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
{
return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0;
}
/// <inheritdoc/>
public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
{
return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
.ExecuteCommandAsync().ConfigureAwait(false) > 0;
return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
.ExecuteCommandAsync();
}
private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput)

View File

@@ -0,0 +1,13 @@
{
"ThingsGateway.Admin.Application.BaseDataEntity": {
"CreateOrgId": "CreateOrgId"
},
"ThingsGateway.Admin.Application.BaseEntity": {
"CreateTime": "CreateTime",
"CreateUser": "CreateUser",
"SortCode": "SortCode",
"UpdateTime": "UpdateTime",
"UpdateUser": "UpdateUser"
}
}

View File

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

View File

@@ -46,7 +46,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
public async Task<bool> DeleteAsync(IEnumerable<T> models)
{
using var db = GetDB();
return await db.Deleteable<T>().In(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
return await db.Deleteable<T>(models.ToList()).ExecuteCommandHasChangeAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -140,18 +140,22 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
return (await db.UpdateableT(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
}
}
/// <inheritdoc/>
public virtual async Task<bool> SaveAsync(List<T> model, ItemChangedType changedType)
{
return (await SaveReturnCountAsync(model, changedType).ConfigureAwait(false)) > 0;
}
/// <inheritdoc/>
public async Task<int> SaveReturnCountAsync(List<T> model, ItemChangedType changedType)
{
using var db = GetDB();
if (changedType == ItemChangedType.Add)
{
return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
return (await db.Insertable(model).ExecuteCommandAsync().ConfigureAwait(false));
}
else
{
return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false)) > 0;
return (await db.Updateable(model).ExecuteCommandAsync().ConfigureAwait(false));
}
}
/// <summary>

View File

@@ -5,9 +5,10 @@
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
<TargetFrameworks>net8.0;$(OtherTargetFrameworks);</TargetFrameworks>
</PropertyGroup>
@@ -18,6 +19,12 @@
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Locales\en-US.json" />
<EmbeddedResource Include="Locales\zh-CN.json" />
</ItemGroup>
<ItemGroup>
<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->

View File

@@ -1 +1 @@
https://gitee.com/dotnetchina/Furion/commit/8bf85f6908c1630268e45eeec607267a03947d2b
https://gitee.com/dotnetchina/Furion/commit/f1c07d65cccb623aca9d1905bf2e1ac6e4f4b714

View File

@@ -27,18 +27,27 @@ using System.Security.Claims;
using ThingsGateway.ConfigurableOptions;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Collections;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Log;
using ThingsGateway.Reflection;
using ThingsGateway.Templates;
namespace ThingsGateway;
public static class WebEnableVariable
{
public static bool WebEnable => Environment.GetEnvironmentVariable(nameof(WebEnable)).ToBoolean(true);
}
/// <summary>
/// 全局应用类
/// </summary>
[SuppressSniffer]
public static class App
{
/// <summary>
/// 私有设置,避免重复解析
/// </summary>
@@ -157,7 +166,7 @@ public static class App
var httpContextAccessor = RootServices?.GetService<IHttpContextAccessor>();
try
{
return httpContextAccessor.HttpContext;
return httpContextAccessor?.HttpContext;
}
catch
{
@@ -545,10 +554,9 @@ public static class App
{
types = ass.GetTypes();
}
catch
catch (Exception ex)
{
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
Console.WriteLine($"Error load `{ass.FullName}` assembly.");
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
}
return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));

View File

@@ -12,7 +12,7 @@
using Microsoft.AspNetCore.Hosting;
using ThingsGateway;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Reflection;
namespace Microsoft.Extensions.Hosting;
@@ -44,7 +44,7 @@ public static class HostBuilderExtensions
hostBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, combineAssembliesName);
// 实现假的 Starup解决泛型主机启动问题
// 实现假的 Startup解决泛型主机启动问题
hostBuilder.UseStartup<FakeStartup>();
return hostBuilder;
}

View File

@@ -20,7 +20,7 @@ using System.Text.RegularExpressions;
using ThingsGateway.NewLife;
namespace ThingsGateway.Extensions;
namespace ThingsGateway.Extension;
/// <summary>
/// 对象拓展类
@@ -28,70 +28,10 @@ namespace ThingsGateway.Extensions;
[SuppressSniffer]
public static class ObjectExtensions
{
/// <summary>
/// 将 DateTimeOffset 转换成本地 DateTime
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime ConvertToDateTime(this DateTimeOffset dateTime)
{
if (dateTime.Offset.Equals(TimeSpan.Zero))
return dateTime.UtcDateTime;
if (dateTime.Offset.Equals(TimeZoneInfo.Local.GetUtcOffset(dateTime.DateTime)))
return dateTime.ToLocalTime().DateTime;
else
return dateTime.DateTime;
}
/// <summary>
/// 将 DateTimeOffset? 转换成本地 DateTime?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTime? ConvertToDateTime(this DateTimeOffset? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTime() : null;
}
/// <summary>
/// 将 DateTime 转换成 DateTimeOffset
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset ConvertToDateTimeOffset(this DateTime dateTime)
{
return DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}
/// <summary>
/// 将 DateTime? 转换成 DateTimeOffset?
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static DateTimeOffset? ConvertToDateTimeOffset(this DateTime? dateTime)
{
return dateTime.HasValue ? dateTime.Value.ConvertToDateTimeOffset() : null;
}
/// <summary>
/// 将时间戳转换为 DateTime
/// </summary>
/// <param name="timestamp"></param>
/// <returns></returns>
internal static DateTime ConvertToDateTime(this long timestamp)
{
var timeStampDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var digitCount = (int)Math.Floor(Math.Log10(timestamp) + 1);
if (digitCount != 13 && digitCount != 10)
{
throw new ArgumentException("Data is not a valid timestamp format.");
}
return (digitCount == 13
? timeStampDateTime.AddMilliseconds(timestamp) // 13 位时间戳
: timeStampDateTime.AddSeconds(timestamp)).ToLocalTime(); // 10 位时间戳
}
/// <summary>
/// 将 IFormFile 转换成 byte[]

View File

@@ -47,7 +47,7 @@ public class StartupFilter : IStartupFilter
else
{
// 输出当前环境标识
context.Response.Headers["environment"] = envName;
context.Response.Headers["Environment"] = envName;
// 输出框架版本
context.Response.Headers[nameof(ThingsGateway)] = version;

View File

@@ -85,11 +85,14 @@ internal static class InternalApp
// 存储根服务(解决 Web 主机还未启动时在 HostedService 中使用 App.GetService 问题
services.AddHostedService<GenericHostLifetimeEventsHostedService>();
// 注册 Startup 过滤器
services.AddTransient<IStartupFilter, StartupFilter>();
if (WebEnableVariable.WebEnable == true)
{
// 注册 Startup 过滤器
services.AddTransient<IStartupFilter, StartupFilter>();
// 注册 HttpContextAccessor 服务
services.AddHttpContextAccessor();
// 注册 HttpContextAccessor 服务
services.AddHttpContextAccessor();
}
// 初始化应用服务
services.AddApp();
@@ -212,6 +215,10 @@ internal static class InternalApp
// 获取环境变量名,如果没找到,则读取 NETCORE_ENVIRONMENT 环境变量信息识别(用于非 Web 环境)
var envName = hostEnvironment?.EnvironmentName ?? Environment.GetEnvironmentVariable("NETCORE_ENVIRONMENT") ?? "Unknown";
// 获取 JSON 文件扫描配置2025.07.25),修复 docker 中挂载大文件数据卷导致启动缓慢的问题
var jsonFileScanner = configuration.GetSection("AppSettings:JsonFileScanner")
.Get<JsonFileScanner>() ?? new JsonFileScanner();
// 读取忽略的配置文件
var ignoreConfigurationFiles = (configuration.GetSection("IgnoreConfigurationFiles")
.Get<string[]>()
@@ -237,7 +244,7 @@ internal static class InternalApp
// 循环加载
foreach (var jsonFile in files)
{
configurationBuilder.AddJsonFile(jsonFile, optional: true, reloadOnChange: true);
configurationBuilder.AddJsonFile(jsonFile, optional: jsonFileScanner.Optional, reloadOnChange: jsonFileScanner.ReloadOnChange);
}
}
}

View File

@@ -20,6 +20,7 @@ namespace ThingsGateway;
/// </summary>
public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions>
{
/// <summary>
/// 是否启用规范化文档
/// </summary>
@@ -50,7 +51,10 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
/// 【部署】二级虚拟目录
/// </summary>
public string VirtualPath { get; set; }
/// <summary>
/// JSON 文件扫描配置
/// </summary>
public JsonFileScanner JsonFileScanner { get; set; }
/// <summary>
/// 后期配置
/// </summary>
@@ -66,3 +70,20 @@ public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions
options.VirtualPath ??= string.Empty;
}
}
/// <summary>
/// JSON 文件扫描配置
/// </summary>
/// <remarks>修复 docker 中挂载大文件数据卷导致启动缓慢的问题。</remarks>
public class JsonFileScanner
{
/// <summary>
/// 是否可选
/// </summary>
public bool Optional { get; set; } = true;
/// <summary>
/// 是否改变的时候重载
/// </summary>
public bool ReloadOnChange { get; set; } = true;
}

View File

@@ -0,0 +1,341 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ThingsGateway;
namespace System;
/// <summary>
/// <see cref="WebApplication"/> 方式配置选项
/// </summary>
[SuppressSniffer]
public sealed class MiniRunOptions : IRunOptions
{
/// <summary>
/// 内部构造函数
/// </summary>
internal MiniRunOptions()
{
}
/// <summary>
/// 默认配置
/// </summary>
public static MiniRunOptions Default { get; } = new MiniRunOptions();
/// <summary>
/// 默认配置(带启动参数)
/// </summary>
public static MiniRunOptions Main(string[] args)
{
return Default.WithArgs(args);
}
/// <summary>
/// 默认配置(静默启动)
/// </summary>
public static MiniRunOptions DefaultSilence { get; } = new MiniRunOptions().Silence();
/// <summary>
/// 默认配置(静默启动 + 启动参数)
/// </summary>
public static MiniRunOptions MainSilence(string[] args)
{
return DefaultSilence.WithArgs(args);
}
/// <summary>
/// 配置 <see cref="WebApplicationOptions"/>
/// </summary>
/// <param name="options"></param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureOptions(WebApplicationOptions options)
{
Options = options;
return this;
}
/// <summary>
/// 配置 <see cref="IWebHostBuilder"/>
/// </summary>
/// <param name="configureAction"></param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureBuilder(Action<IWebHostBuilder> configureAction)
{
ActionBuilder = configureAction;
return this;
}
/// <summary>
/// 配置 <see cref="IHostBuilder"/>
/// </summary>
/// <param name="configureAction"></param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureFirstActionBuilder(Action<IHostBuilder> configureAction)
{
FirstActionBuilder = configureAction;
return this;
}
/// <summary>
/// 配置 <see cref="IServiceCollection"/>
/// </summary>
/// <param name="configureAction"></param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureServices(Action<IServiceCollection> configureAction)
{
ActionServices = configureAction;
return this;
}
/// <summary>
/// 配置 <see cref="InjectOptions"/>
/// </summary>
/// <param name="configureAction"></param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureInject(Action<IWebHostBuilder, InjectOptions> configureAction)
{
ActionInject = configureAction;
return this;
}
/// <summary>
/// 配置 <see cref="WebApplication"/>
/// </summary>
/// <param name="configureAction">配置委托</param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions Configure(Action<IHost> configureAction)
{
ActionConfigure = configureAction;
return this;
}
/// <summary>
/// 配置 <see cref="ConfigurationManager"/>
/// </summary>
/// <param name="configureAction">配置委托</param>
/// <returns><see cref="MiniRunOptions"/></returns>
public MiniRunOptions ConfigureConfiguration(Action<IHostEnvironment, IConfiguration> configureAction)
{
ActionConfigurationManager = configureAction;
return this;
}
/// <summary>
/// 添加应用服务组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <returns></returns>
public MiniRunOptions AddComponent<TComponent>()
where TComponent : class, IServiceComponent, new()
{
ServiceComponents.Add(typeof(TComponent), null);
return this;
}
/// <summary>
/// 添加应用服务组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <typeparam name="TComponentOptions"></typeparam>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions AddComponent<TComponent, TComponentOptions>(TComponentOptions options)
where TComponent : class, IServiceComponent, new()
{
ServiceComponents.Add(typeof(TComponent), options);
return this;
}
/// <summary>
/// 添加应用服务组件
/// </summary>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions AddComponent(Type componentType, object options)
{
ServiceComponents.Add(componentType, options);
return this;
}
/// <summary>
/// 添加应用中间件组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <returns></returns>
public MiniRunOptions UseComponent<TComponent>()
where TComponent : class, IApplicationComponent, new()
{
ApplicationComponents.Add(typeof(TComponent), null);
return this;
}
/// <summary>
/// 添加应用中间件组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <typeparam name="TComponentOptions"></typeparam>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions UseComponent<TComponent, TComponentOptions>(TComponentOptions options)
where TComponent : class, IApplicationComponent, new()
{
ApplicationComponents.Add(typeof(TComponent), options);
return this;
}
/// <summary>
/// 添加应用中间件组件
/// </summary>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions UseComponent(Type componentType, object options)
{
ApplicationComponents.Add(componentType, options);
return this;
}
/// <summary>
/// 添加 IWebHostBuilder 组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <returns></returns>
public MiniRunOptions AddWebComponent<TComponent>()
where TComponent : class, IWebComponent, new()
{
WebComponents.Add(typeof(TComponent), null);
return this;
}
/// <summary>
/// 添加 IWebHostBuilder 组件
/// </summary>
/// <typeparam name="TComponent">组件类型</typeparam>
/// <typeparam name="TComponentOptions"></typeparam>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions AddWebComponent<TComponent, TComponentOptions>(TComponentOptions options)
where TComponent : class, IWebComponent, new()
{
WebComponents.Add(typeof(TComponent), options);
return this;
}
/// <summary>
/// 添加 IWebHostBuilder 组件
/// </summary>
/// <param name="componentType">组件类型</param>
/// <param name="options">组件参数</param>
/// <returns></returns>
public MiniRunOptions AddWebComponent(Type componentType, object options)
{
WebComponents.Add(componentType, options);
return this;
}
/// <summary>
/// 标识主机静默启动
/// </summary>
/// <remarks>不阻塞程序运行</remarks>
/// <param name="silence">静默启动</param>
/// <param name="logging">静默启动日志状态,默认 false</param>
/// <returns></returns>
public MiniRunOptions Silence(bool silence = true, bool logging = false)
{
IsSilence = silence;
SilenceLogging = logging;
return this;
}
/// <summary>
/// 设置进程启动参数
/// </summary>
/// <param name="args">启动参数</param>
/// <returns></returns>
public MiniRunOptions WithArgs(string[] args)
{
Args = args;
return this;
}
/// <summary>
/// <see cref="WebApplicationOptions"/>
/// </summary>
internal WebApplicationOptions Options { get; set; }
/// <summary>
/// 自定义 <see cref="IServiceCollection"/> 委托
/// </summary>
internal Action<IServiceCollection> ActionServices { get; set; }
/// <summary>
/// 自定义 <see cref="IWebHostBuilder"/> 委托
/// </summary>
internal Action<IHostBuilder> FirstActionBuilder { get; set; }
/// <summary>
/// 自定义 <see cref="IWebHostBuilder"/> 委托
/// </summary>
internal Action<IWebHostBuilder> ActionBuilder { get; set; }
/// <summary>
/// 自定义 <see cref="InjectOptions"/> 委托
/// </summary>
internal Action<IWebHostBuilder, InjectOptions> ActionInject { get; set; }
/// <summary>
/// 自定义 <see cref="IHost"/> 委托
/// </summary>
internal Action<IHost> ActionConfigure { get; set; }
/// <summary>
/// 自定义 <see cref="IConfiguration"/> 委托
/// </summary>
internal Action<IHostEnvironment, IConfiguration> ActionConfigurationManager { get; set; }
/// <summary>
/// 应用服务组件
/// </summary>
internal Dictionary<Type, object> ServiceComponents { get; set; } = new();
/// <summary>
/// IWebHostBuilder 组件
/// </summary>
internal Dictionary<Type, object> WebComponents { get; set; } = new();
/// <summary>
/// 应用中间件组件
/// </summary>
internal Dictionary<Type, object> ApplicationComponents { get; set; } = new();
/// <summary>
/// 静默启动
/// </summary>
/// <remarks>不阻塞程序运行</remarks>
internal bool IsSilence { get; private set; }
/// <summary>
/// 静默启动日志状态
/// </summary>
internal bool SilenceLogging { get; set; }
/// <summary>
/// 命令行参数
/// </summary>
internal string[] Args { get; set; }
}

View File

@@ -602,6 +602,33 @@ public static class Serve
return app;
}
/// <summary>
/// 启动 WebApplication 主机
/// </summary>
/// <remarks>未包含 Web 基础功能,需手动注册服务/中间件</remarks>
/// <param name="options">配置选项</param>
/// <param name="urls">默认 5000/5001 端口</param>
/// <param name="cancellationToken"></param>
/// <returns><see cref="IHost"/></returns>
public static async Task<IHost> RunAsync(MiniRunOptions options, string urls = default, CancellationToken cancellationToken = default)
{
// 构建 WebApplication 对象
BuildMiniApplication(options, urls, out var app);
// 是否静默启动
if (!options.IsSilence)
{
// 配置启动地址和端口
await app.RunAsync(cancellationToken).ConfigureAwait(false);
}
else
{
await app.StartAsync(cancellationToken).ConfigureAwait(false);
}
return app;
}
/// <summary>
/// 构建 WebApplication 对象
/// </summary>
@@ -616,8 +643,8 @@ public static class Serve
// 初始化 WebApplicationBuilder
var builder = (options.Options == null
? WebApplication.CreateBuilder(args)
: WebApplication.CreateBuilder(options.Options));
? WebApplication.CreateBuilder(args)
: WebApplication.CreateBuilder(options.Options));
// 调用自定义配置服务
options?.FirstActionBuilder?.Invoke(builder);
@@ -674,7 +701,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app);
UseStartups(app.Services);
@@ -793,12 +820,138 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();
}
/// <summary>
/// 构建 IHost 对象
/// </summary>
/// <param name="options">配置选项</param>
/// <param name="urls">默认 5000/5001 端口</param>
/// <param name="app"><see cref="IHost"/></param>
public static void BuildMiniApplication(MiniRunOptions options, string urls, out IHost app)
{
// 获取命令行参数
var args = options.Args ?? Environment.GetCommandLineArgs().Skip(1).ToArray();
var builder = Host.CreateDefaultBuilder(args);
// 静默启动排除指定日志类名
if (options.IsSilence && !options.SilenceLogging)
{
builder = builder.ConfigureLogging(logging =>
{
logging.AddFilter((provider, category, logLevel) => !SilenceExcludesOfLogCategoryName.Any(u => category.StartsWith(u)));
});
}
// 配置 Web 主机
builder = builder.ConfigureWebHost(webHostBuilder =>
{
// 调用自定义配置服务
options?.FirstActionBuilder?.Invoke(builder);
// 注册 WebApplicationBuilder 组件
if (options.WebComponents.Count > 0)
{
foreach (var (componentType, opt) in options.WebComponents)
{
webHostBuilder.AddWebComponent(componentType, opt);
}
}
webHostBuilder.Configure((WebHostBuilderContext app, IApplicationBuilder applicationBuilder) =>
{
// 添加自定义配置
options.ActionConfigurationManager?.Invoke(app.HostingEnvironment, app.Configuration);
});
// 初始化框架
webHostBuilder.Inject(options.ActionInject);
// 配置服务
if (options.ServiceComponents.Count > 0)
{
webHostBuilder = webHostBuilder.ConfigureServices(services =>
{
// 注册应用服务组件
foreach (var (componentType, opt) in options.ServiceComponents)
{
services.AddComponent(componentType, opt);
}
});
}
// 配置启动地址和端口
var startUrls = !string.IsNullOrWhiteSpace(urls) ? urls : webHostBuilder.GetSetting(nameof(urls));
// 自定义启动端口
if (!string.IsNullOrWhiteSpace(startUrls))
{
webHostBuilder = webHostBuilder.UseUrls(startUrls);
}
// 调用自定义配置
options?.ActionBuilder?.Invoke(webHostBuilder);
// 配置中间件
if (options.ApplicationComponents.Count > 0)
{
webHostBuilder = webHostBuilder.Configure((context, app) =>
{
// 注册应用中间件组件
foreach (var (componentType, opt) in options.ApplicationComponents)
{
app.UseComponent(context.HostingEnvironment, componentType, opt);
}
});
}
});
builder = builder.ConfigureServices(services =>
{
// 调用自定义配置服务
options?.ActionServices?.Invoke(services);
});
// 构建主机
app = builder.Build();
InternalApp.RootServices ??= app.Services;
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();
// 调用自定义配置
options?.ActionConfigure?.Invoke(app);
}
/// <summary>
/// 构建 IHost 对象
/// </summary>
@@ -852,7 +1005,7 @@ public static class Serve
var applicationPartManager = app.Services.GetService<ApplicationPartManager>();
applicationPartManager?.ApplicationParts?.RemoveWhere(p => App.BakImageNames.Any(b => b == p.Name));
// 配置所有 Starup Configure
// 配置所有 Startup Configure
UseStartups(app.Services);
// 释放内存
App.AppStartups.Clear();

View File

@@ -11,7 +11,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -11,7 +11,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -12,7 +12,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.AspNetCore;

View File

@@ -18,7 +18,7 @@ using System.Reflection;
using ThingsGateway;
using ThingsGateway.ConfigurableOptions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace Microsoft.Extensions.DependencyInjection;

View File

@@ -34,9 +34,12 @@ public static class PBKDF2Encryption
using var rng = RandomNumberGenerator.Create();
var salt = new byte[saltSize];
rng.GetBytes(salt);
#if NET10_0_OR_GREATER
var hash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), salt, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
#else
using var pbkdf2 = new Rfc2898DeriveBytes(text, salt, iterationCount, HashAlgorithmName.SHA256);
var hash = pbkdf2.GetBytes(derivedKeyLength);
#endif
// 分别编码盐和哈希,并用分隔符拼接
return Convert.ToBase64String(salt) + SaltHashSeparator + Convert.ToBase64String(hash);
@@ -65,8 +68,12 @@ public static class PBKDF2Encryption
if (saltBytes.Length != saltSize || storedHashBytes.Length != derivedKeyLength)
return false;
#if NET10_0_OR_GREATER
var computedHash = Rfc2898DeriveBytes.Pbkdf2(System.Text.Encoding.UTF8.GetBytes(text), saltBytes, iterationCount, HashAlgorithmName.SHA256, derivedKeyLength);
#else
using var pbkdf2 = new Rfc2898DeriveBytes(text, saltBytes, iterationCount, HashAlgorithmName.SHA256);
var computedHash = pbkdf2.GetBytes(derivedKeyLength);
#endif
return computedHash.SequenceEqual(storedHashBytes);
}

View File

@@ -16,7 +16,7 @@ using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Templates.Extensions;
namespace ThingsGateway.DataValidation;

View File

@@ -50,9 +50,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
#pragma warning disable CA1851
if (services
.OfType<AspectDispatchProxy>()
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
{
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
}
#pragma warning restore CA1851
@@ -85,9 +85,9 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
#pragma warning disable CA1851
if (services
.OfType<AspectDispatchProxy>()
.FirstOrDefault(u => ResovleServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
.FirstOrDefault(u => ResolveServiceName(((dynamic)u).Target.GetType()) == serviceName) is not TService service)
{
service = services.FirstOrDefault(u => ResovleServiceName(u.GetType()) == serviceName);
service = services.FirstOrDefault(u => ResolveServiceName(u.GetType()) == serviceName);
}
#pragma warning restore CA1851
@@ -116,7 +116,7 @@ internal sealed class NamedServiceProvider<TService> : INamedServiceProvider<TSe
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static string ResovleServiceName(Type type)
private static string ResolveServiceName(Type type)
{
if (type.IsDefined(typeof(InjectionAttribute)))
{

View File

@@ -21,7 +21,7 @@ using System.Collections.Concurrent;
using System.Reflection;
using System.Text.RegularExpressions;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.UnifyResult;
namespace ThingsGateway.DynamicApiController;

View File

@@ -17,7 +17,7 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Templates.Extensions;
namespace ThingsGateway.FriendlyException;

View File

@@ -27,13 +27,15 @@ public sealed class Retry
/// <param name="exceptionTypes">异常类型,可多个</param>
/// <param name="fallbackPolicy">重试失败回调</param>
/// <param name="retryAction">重试时调用方法</param>
/// <param name="shouldExit">退出条件</param>
public static void Invoke(Action action
, int numRetries
, int retryTimeout = 1000
, bool finalThrow = true
, Type[] exceptionTypes = default
, Action<Exception> fallbackPolicy = default
, Action<int, int> retryAction = default)
, Action<int, int> retryAction = default
, Func<bool> shouldExit = default)
{
if (action == null) throw new ArgumentNullException(nameof(action));
@@ -46,7 +48,7 @@ public sealed class Retry
{
fallbackPolicy?.Invoke(ex);
return Task.CompletedTask;
}, retryAction).GetAwaiter().GetResult();
}, retryAction, shouldExit).GetAwaiter().GetResult();
}
/// <summary>
@@ -59,6 +61,7 @@ public sealed class Retry
/// <param name="exceptionTypes">异常类型,可多个</param>
/// <param name="fallbackPolicy">重试失败回调</param>
/// <param name="retryAction">重试时调用方法</param>
/// <param name="shouldExit">退出条件</param>
/// <returns><see cref="Task"/></returns>
public static async Task InvokeAsync(Func<Task> action
, int numRetries
@@ -66,7 +69,8 @@ public sealed class Retry
, bool finalThrow = true
, Type[] exceptionTypes = default
, Func<Exception, Task> fallbackPolicy = default
, Action<int, int> retryAction = default)
, Action<int, int> retryAction = default
, Func<bool> shouldExit = default)
{
if (action == null) throw new ArgumentNullException(nameof(action));
@@ -117,6 +121,12 @@ public sealed class Retry
// 如果可重试异常数大于 0则间隔指定时间后继续执行
if (retryTimeout > 0) await Task.Delay(retryTimeout).ConfigureAwait(false);
// 处理退出机制
if (shouldExit != null && shouldExit())
{
return;
}
}
}
}

View File

@@ -16,7 +16,7 @@ using Microsoft.AspNetCore.SignalR;
using System.Reflection;
using ThingsGateway;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.InstantMessaging;
namespace Microsoft.AspNetCore.Builder;

View File

@@ -9,7 +9,7 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
namespace ThingsGateway.Logging;

View File

@@ -127,7 +127,8 @@ public sealed class DatabaseLogger : ILogger, IDisposable
// 设置日志消息模板
logMsg.Message = _options.MessageFormat != null
? _options.MessageFormat(logMsg)
: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
: string.Empty;
//: Penetrates.OutputStandardMessage(logMsg, _options.DateFormat, withTraceId: _options.WithTraceId, withStackFrame: _options.WithStackFrame, provider: _options.FormatProvider);
// 空检查
if (logMsg.Message is null)

View File

@@ -36,8 +36,9 @@ using System.Text.Json;
using ThingsGateway;
using ThingsGateway.DataValidation;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.FriendlyException;
using ThingsGateway.JsonSerialization;
using ThingsGateway.Logging;
using ThingsGateway.Templates;
using ThingsGateway.UnifyResult;
@@ -150,7 +151,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return;
}
await MonitorAsync(actionMethod, context.ActionArguments, context, next).ConfigureAwait(false);
await MonitorAsync(actionMethod, context.ActionArguments, context, () => next()).ConfigureAwait(false);
}
/// <summary>
@@ -182,7 +183,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return;
}
await MonitorAsync(actionMethod, context.HandlerArguments, context, next).ConfigureAwait(false);
await MonitorAsync(actionMethod, context.HandlerArguments, context, () => next()).ConfigureAwait(false);
}
/// <summary>
@@ -639,6 +640,11 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 解决 JsonElement 序列化问题
jsonSerializerSettings.Converters.Add(new JsonElementConverter());
// 解决 JsonObject 和 JsonArray 序列化问题
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonObjectJsonConverter());
jsonSerializerSettings.Converters.Add(new NewtonsoftJsonJsonArrayJsonConverter());
// 解决 DateTimeOffset 序列化/反序列化问题
if (obj is DateTimeOffset)
{
@@ -783,12 +789,12 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
return typeName;
}
private async Task MonitorAsync(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, dynamic next)
private async Task MonitorAsync<T>(MethodInfo actionMethod, IDictionary<string, object> parameterValues, FilterContext context, Func<Task<T>> next)
{
// 排除 WebSocket 请求处理
if (context.HttpContext.IsWebSocketRequest())
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -799,14 +805,14 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
if (actionMethod.IsDefined(typeof(SuppressMonitorAttribute), true)
|| actionMethod.DeclaringType.IsDefined(typeof(SuppressMonitorAttribute), true))
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
// 判断是否自定义了日志筛选器,如果是则检查是否符合条件
if (LoggingMonitorSettings.InternalWriteFilter?.Invoke(context) == false)
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -819,7 +825,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 解决局部和全局触发器同时配置触发两次问题
if (isDefinedScopedAttribute && Settings.FromGlobalFilter == true)
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -833,7 +839,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
&& !Settings.IncludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
{
// 查找是否包含匹配,忽略大小写
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
@@ -841,7 +847,7 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
if (Settings.GlobalEnabled
&& Settings.ExcludeOfMethods.Contains(methodFullName, StringComparer.OrdinalIgnoreCase))
{
_ = await next();
_ = await next().ConfigureAwait(false);
return;
}
}
@@ -952,7 +958,8 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
// 计算接口执行时间
var timeOperation = Stopwatch.StartNew();
var resultContext = await next();
var resultContext = await next().ConfigureAwait(false);
timeOperation.Stop();
writer.WriteNumber("timeOperationElapsedMilliseconds", timeOperation.ElapsedMilliseconds);
@@ -1008,8 +1015,13 @@ public sealed class LoggingMonitorAttribute : Attribute, IAsyncActionFilter, IAs
var environment = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().EnvironmentName;
writer.WriteString(nameof(environment), environment);
Exception exception = null;
// 获取异常对象情况
Exception exception = resultContext.Exception;
if (resultContext is PageHandlerExecutedContext pageHandlerExecutedContext)
exception = pageHandlerExecutedContext.Exception;
else if (resultContext is ActionExecutedContext actionExecutedContext)
exception = actionExecutedContext.Exception;
if (exception == null)
{
// 解析存储的验证信息

View File

@@ -16,7 +16,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Linq.Expressions;
using System.Reflection;
using ThingsGateway.Extensions;
using ThingsGateway.Extension;
using ThingsGateway.Options;
namespace Microsoft.Extensions.Options;

View File

@@ -20,9 +20,10 @@ public sealed class DailyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public DailyAtAttribute(params object[] fields)
: base("@daily", fields)
public DailyAtAttribute(object field, params object[] fields)
: base("@daily", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class HourlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public HourlyAtAttribute(params object[] fields)
: base("@hourly", fields)
public HourlyAtAttribute(object field, params object[] fields)
: base("@hourly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class MinutelyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public MinutelyAtAttribute(params object[] fields)
: base("@minutely", fields)
public MinutelyAtAttribute(object field, params object[] fields)
: base("@minutely", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class MonthlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public MonthlyAtAttribute(params object[] fields)
: base("@monthly", fields)
public MonthlyAtAttribute(object field, params object[] fields)
: base("@monthly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -14,15 +14,16 @@ namespace ThingsGateway.Schedule;
/// <summary>
/// 特定秒开始作业触发器特性
/// </summary>
[SecondlyAtAttribute, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[SuppressSniffer, AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SecondlyAtAttribute : CronAttribute
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public SecondlyAtAttribute(params object[] fields)
: base("@secondly", fields)
public SecondlyAtAttribute(object field, params object[] fields)
: base("@secondly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class WeeklyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public WeeklyAtAttribute(params object[] fields)
: base("@weekly", fields)
public WeeklyAtAttribute(object field, params object[] fields)
: base("@weekly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -20,9 +20,10 @@ public sealed class YearlyAtAttribute : CronAttribute
/// <summary>
/// 构造函数
/// </summary>
/// <param name="field">字段值</param>
/// <param name="fields">字段值</param>
public YearlyAtAttribute(params object[] fields)
: base("@yearly", fields)
public YearlyAtAttribute(object field, params object[] fields)
: base("@yearly", new[] { field }.Concat(fields).ToArray())
{
}
}

View File

@@ -93,12 +93,14 @@ internal sealed class JobCancellationToken : IJobCancellationToken
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex) when (!(ex is OperationCanceledException ||
ex is ObjectDisposedException ||
(ex is AggregateException aggEx && aggEx.InnerExceptions.All(e => e is OperationCanceledException || e is ObjectDisposedException))))
{ }
catch { }
catch (Exception ex)
{
// 输出非任务取消异常日志
if (!(ex is OperationCanceledException || (ex is AggregateException aggEx && aggEx.InnerExceptions.Count == 1 && aggEx.InnerExceptions[0] is TaskCanceledException)))
{
// 待输出
}
}
}
}

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.Schedule;
/// <summary>
@@ -152,7 +154,31 @@ public abstract class JobExecutionContext
writer.WriteEndObject();
});
}
/// <summary>
/// 检查作业任务是否处于正常状态
/// </summary>
/// <param name="schedulerFactory"><see cref="ISchedulerFactory"/></param>
/// <returns><see cref="bool"/></returns>
public bool IsNormalStatus(ISchedulerFactory schedulerFactory = null)
{
// 解析作业计划工厂服务
schedulerFactory ??= ServiceProvider.GetRequiredService<ISchedulerFactory>();
// 情况 1检查作业是否存在
if (schedulerFactory.TryGetJob(JobId, out var scheduler) != ScheduleResult.Succeed)
{
return false;
}
// 情况 2检查作业触发器是否存在
if (scheduler.TryGetTrigger(TriggerId, out var trigger) != ScheduleResult.Succeed)
{
return false;
}
// 情况 3检查作业触发器是否正常运行
return trigger.IsNormalStatus();
}
/// <summary>
/// 作业执行上下文转字符串输出输出
/// </summary>

View File

@@ -209,7 +209,7 @@ public sealed class ScheduleUIMiddleware
case "remove":
_schedulerFactory.RemoveJob(jobId);
break;
// 立即执行
// 手动执行
case "run":
_schedulerFactory.RunJob(jobId);
break;
@@ -264,7 +264,7 @@ public sealed class ScheduleUIMiddleware
case "remove":
scheduler1?.RemoveTrigger(triggerId);
break;
// 立即执行
// 手动执行
case "run":
scheduler1?.Run(triggerId);
break;

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/__schedule__/static/css/main.fbe5db1c.css",
"main.js": "/__schedule__/static/js/main.851eb0b3.js",
"main.css": "/__schedule__/static/css/main.765127e9.css",
"main.js": "/__schedule__/static/js/main.326c761f.js",
"index.html": "/__schedule__/index.html",
"main.fbe5db1c.css.map": "/__schedule__/static/css/main.fbe5db1c.css.map",
"main.851eb0b3.js.map": "/__schedule__/static/js/main.851eb0b3.js.map"
"main.765127e9.css.map": "/__schedule__/static/css/main.765127e9.css.map",
"main.326c761f.js.map": "/__schedule__/static/js/main.326c761f.js.map"
},
"entrypoints": [
"static/css/main.fbe5db1c.css",
"static/js/main.851eb0b3.js"
"static/css/main.765127e9.css",
"static/js/main.326c761f.js"
]
}

View File

@@ -1 +1,19 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/__schedule__/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Schedule Dashboard"/><link rel="apple-touch-icon" href="/__schedule__/logo192.png"/><script src="/__schedule__/apiconfig.js"></script><title>Schedule Dashboard</title><script defer="defer" src="/__schedule__/static/js/main.851eb0b3.js"></script><link href="/__schedule__/static/css/main.fbe5db1c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>document.title=window.apiconfig.title</script></body></html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/__schedule__/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Schedule Dashboard" />
<link rel="apple-touch-icon" href="/__schedule__/logo192.png" />
<script src="/__schedule__/apiconfig.js"></script>
<title>Schedule Dashboard</title>
<script defer="defer" src="/__schedule__/static/js/main.326c761f.js"></script>
<link href="/__schedule__/static/css/main.765127e9.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div>
<script>document.title = window.apiconfig.title</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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