Compare commits
	
		
			28 Commits
		
	
	
		
			10.9.92.0
			...
			10.10.12.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d7df6fc605 | ||
| 
						 | 
					eb4bb2fd48 | ||
| 
						 | 
					faa9858974 | ||
| 
						 | 
					1b3d2dda49 | ||
| 
						 | 
					a8a9453611 | ||
| 
						 | 
					e84f42ce14 | ||
| 
						 | 
					6f814cf6b8 | ||
| 
						 | 
					e36432e4e9 | ||
| 
						 | 
					ebd71e807b | ||
| 
						 | 
					34000d8d7d | ||
| 
						 | 
					e785f6660c | ||
| 
						 | 
					831c611797 | ||
| 
						 | 
					453817ef86 | ||
| 
						 | 
					8ce0b981c1 | ||
| 
						 | 
					4e5c51b54c | ||
| 
						 | 
					3cc9d31f28 | ||
| 
						 | 
					10391f869b | ||
| 
						 | 
					fba0723a6d | ||
| 
						 | 
					2db3f78f0c | ||
| 
						 | 
					badf61fe01 | ||
| 
						 | 
					d74e0952dc | ||
| 
						 | 
					fb1699ce80 | ||
| 
						 | 
					44adddbcd4 | ||
| 
						 | 
					0eab889452 | ||
| 
						 | 
					e14d39a459 | ||
| 
						 | 
					7575264ede | ||
| 
						 | 
					3e1a077b96 | ||
| 
						 | 
					a921cb8400 | 
@@ -38,6 +38,7 @@ 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>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Admin.Application;
 | 
			
		||||
 | 
			
		||||
public class USheetDatas
 | 
			
		||||
{
 | 
			
		||||
@@ -18,6 +18,7 @@ public class SessionOutput : PrimaryIdEntity
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 主键Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public override long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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
 | 
			
		||||
{
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
 | 
			
		||||
 
 | 
			
		||||
@@ -45,11 +45,11 @@ public class Startup : AppStartup
 | 
			
		||||
            options.ServicesStopConcurrently = true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //// 事件总线
 | 
			
		||||
        //services.AddEventBus(options =>
 | 
			
		||||
        //{
 | 
			
		||||
        // 事件总线
 | 
			
		||||
        services.AddEventBus(options =>
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
        //});
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 任务调度
 | 
			
		||||
        services.AddSchedule(options => options.AddPersistence<JobPersistence>());
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Common;
 | 
			
		||||
 | 
			
		||||
public class SmartTriggerScheduler
 | 
			
		||||
{
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
namespace ThingsGateway.Common;
 | 
			
		||||
 | 
			
		||||
public sealed class StringOrdinalIgnoreCaseEqualityComparer : EqualityComparer<string>
 | 
			
		||||
{
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
 | 
			
		||||
		<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.8.2" />
 | 
			
		||||
		<PackageReference Include="BootstrapBlazor" Version="9.9.1" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ 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; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -123,6 +123,15 @@ 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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -213,12 +213,18 @@ public static class AppServiceCollectionExtensions
 | 
			
		||||
        // 缓存
 | 
			
		||||
        if (cacheOptions.CacheType == CacheType.Memory)
 | 
			
		||||
        {
 | 
			
		||||
            services.AddSingleton<ICache, MemoryCache>(a => new()
 | 
			
		||||
            services.AddSingleton<ICache>(a =>
 | 
			
		||||
            {
 | 
			
		||||
                Capacity = cacheOptions.MemoryCacheOptions.Capacity,
 | 
			
		||||
                Expire = cacheOptions.MemoryCacheOptions.Expire,
 | 
			
		||||
                Period = cacheOptions.MemoryCacheOptions.Period
 | 
			
		||||
            });
 | 
			
		||||
                Cache.Default = new MemoryCache()
 | 
			
		||||
                {
 | 
			
		||||
                    Capacity = cacheOptions.MemoryCacheOptions.Capacity,
 | 
			
		||||
                    Expire = cacheOptions.MemoryCacheOptions.Expire,
 | 
			
		||||
                    Period = cacheOptions.MemoryCacheOptions.Period
 | 
			
		||||
                };
 | 
			
		||||
                return Cache.Default;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        else if (cacheOptions.CacheType == CacheType.Redis)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ namespace ThingsGateway;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class AppSettingsOptions : IConfigurableOptions<AppSettingsOptions>
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否启用规范化文档
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										341
									
								
								src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								src/Admin/ThingsGateway.Furion/App/Options/MiniRunOptions.cs
									
									
									
									
									
										Normal 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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
@@ -799,6 +826,132 @@ public static class Serve
 | 
			
		||||
        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));
 | 
			
		||||
        // 配置所有 Starup Configure
 | 
			
		||||
        UseStartups(app.Services);
 | 
			
		||||
        // 释放内存
 | 
			
		||||
        App.AppStartups.Clear();
 | 
			
		||||
        // 调用自定义配置
 | 
			
		||||
        options?.ActionConfigure?.Invoke(app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 构建 IHost 对象
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -683,13 +683,20 @@ public class MachineInfo : IExtend
 | 
			
		||||
            if (dic.TryGetValue("MemTotal", out var str) && !str.IsNullOrEmpty())
 | 
			
		||||
                Memory = (UInt64)str.TrimEnd(" kB").ToLong();
 | 
			
		||||
 | 
			
		||||
            ulong ma = 0;
 | 
			
		||||
            if (dic.TryGetValue("MemAvailable", out str) && !str.IsNullOrEmpty())
 | 
			
		||||
                AvailableMemory = (UInt64)str.TrimEnd(" kB").ToLong();
 | 
			
		||||
            else if (dic.TryGetValue("MemFree", out str) && !str.IsNullOrEmpty())
 | 
			
		||||
                AvailableMemory =
 | 
			
		||||
                    (UInt64)(str.TrimEnd(" kB").ToLong() +
 | 
			
		||||
                    dic["Buffers"]?.TrimEnd(" kB").ToLong() ?? 0 +
 | 
			
		||||
                    dic["Cached"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            {
 | 
			
		||||
                ma = (UInt64)(str.TrimEnd(" kB").ToLong());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //低于3.14内核的版本用 free+cache
 | 
			
		||||
            var mf = (UInt64)(dic["MemFree"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            var mc = (UInt64)(dic["Cached"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
            var bf = (UInt64)(dic["Buffers"]?.TrimEnd(" kB").ToLong() ?? 0);
 | 
			
		||||
 | 
			
		||||
            var free = mf + mc + bf;
 | 
			
		||||
 | 
			
		||||
            AvailableMemory = ma > free ? ma : free;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // A2/A4温度获取,Buildroot,CPU温度和主板温度
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
@namespace ThingsGateway.Razor
 | 
			
		||||
 | 
			
		||||
@if (show)
 | 
			
		||||
{
 | 
			
		||||
    <Spinner class="ms-auto"></Spinner>
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Razor;
 | 
			
		||||
namespace ThingsGateway.Razor;
 | 
			
		||||
 | 
			
		||||
public partial class SpinnerComponent
 | 
			
		||||
{
 | 
			
		||||
@@ -27,16 +27,17 @@
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertDatas">要插入的数据列表</param>
 | 
			
		||||
        /// <param name="tableName">表名称</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>插入的记录数</returns>
 | 
			
		||||
        public int BulkCopy<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        public int BulkCopy<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            int result = 0;
 | 
			
		||||
            // 使用分页方式处理大数据量插入
 | 
			
		||||
            db.Utilities.PageEach(insertDatas, pageSize, pageItems =>
 | 
			
		||||
            {
 | 
			
		||||
                // 同步调用批量插入API并累加结果
 | 
			
		||||
                result += questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
                result += questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
            });
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
@@ -46,16 +47,17 @@
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertDatas">要插入的数据列表</param>
 | 
			
		||||
        /// <param name="tableName">表名称</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>插入的记录数</returns>
 | 
			
		||||
        public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            int result = 0;
 | 
			
		||||
            // 异步分页处理大数据量插入
 | 
			
		||||
            await db.Utilities.PageEachAsync(insertDatas, pageSize, async pageItems =>
 | 
			
		||||
            {
 | 
			
		||||
                // 异步调用批量插入API并累加结果
 | 
			
		||||
                result += await questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).ConfigureAwait(false);
 | 
			
		||||
                result += await questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).ConfigureAwait(false);
 | 
			
		||||
            }).ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,12 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 绑定RestAPI需要的信息
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string httpPort, ref string username, ref string password)
 | 
			
		||||
        public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string username, ref string password)
 | 
			
		||||
        {
 | 
			
		||||
            if (builder.TryGetValue("Host", out object hostValue))
 | 
			
		||||
            {
 | 
			
		||||
                host = Convert.ToString(hostValue);
 | 
			
		||||
            }
 | 
			
		||||
            if (builder.TryGetValue("HttpPort", out object httpPortValue))
 | 
			
		||||
            {
 | 
			
		||||
                httpPort = Convert.ToString(httpPortValue);
 | 
			
		||||
            }
 | 
			
		||||
            if (builder.TryGetValue("Username", out object usernameValue))
 | 
			
		||||
            {
 | 
			
		||||
                username = Convert.ToString(usernameValue);
 | 
			
		||||
 
 | 
			
		||||
@@ -28,16 +28,16 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// 初始化 QuestDbRestAPI 实例
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="db">SqlSugar 数据库客户端</param>
 | 
			
		||||
        public QuestDbRestAPI(ISqlSugarClient db)
 | 
			
		||||
        /// <param name="httpPort">restApi端口</param>
 | 
			
		||||
        public QuestDbRestAPI(ISqlSugarClient db, int httpPort = 9000)
 | 
			
		||||
        {
 | 
			
		||||
            var builder = new DbConnectionStringBuilder();
 | 
			
		||||
            builder.ConnectionString = db.CurrentConnectionConfig.ConnectionString;
 | 
			
		||||
            this.db = db;
 | 
			
		||||
            string httpPort = String.Empty;
 | 
			
		||||
            string host = String.Empty;
 | 
			
		||||
            string username = String.Empty;
 | 
			
		||||
            string password = String.Empty;
 | 
			
		||||
            QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref httpPort, ref username, ref password);
 | 
			
		||||
            QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref username, ref password);
 | 
			
		||||
            BindHost(host, httpPort, username, password);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            // HTTP GET 请求执行SQL
 | 
			
		||||
            var result = string.Empty;
 | 
			
		||||
            var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}";
 | 
			
		||||
 | 
			
		||||
            var request = new HttpRequestMessage(HttpMethod.Get, url);
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(authorization))
 | 
			
		||||
                client.DefaultRequestHeaders.Add("Authorization", authorization);
 | 
			
		||||
            var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false);
 | 
			
		||||
            {
 | 
			
		||||
                request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false);
 | 
			
		||||
            result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
@@ -68,34 +73,34 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return ExecuteCommandAsync(sql).GetAwaiter().GetResult();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 异步批量插入单条数据
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertData">要插入的数据</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>影响的行数</returns>
 | 
			
		||||
        public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            if (db.CurrentConnectionConfig.MoreSettings == null)
 | 
			
		||||
                db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
 | 
			
		||||
            db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
 | 
			
		||||
            var sql = db.InsertableT(insertData).ToSqlString();
 | 
			
		||||
            var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
 | 
			
		||||
            return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
 | 
			
		||||
        }
 | 
			
		||||
        ///// <summary>
 | 
			
		||||
        ///// 异步批量插入单条数据
 | 
			
		||||
        ///// </summary>
 | 
			
		||||
        ///// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        ///// <param name="insertData">要插入的数据</param>
 | 
			
		||||
        ///// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        ///// <returns>影响的行数</returns>
 | 
			
		||||
        //public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        //{
 | 
			
		||||
        //    if (db.CurrentConnectionConfig.MoreSettings == null)
 | 
			
		||||
        //        db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
 | 
			
		||||
        //    db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
 | 
			
		||||
        //    var sql = db.InsertableT(insertData).ToSqlString();
 | 
			
		||||
        //    var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
 | 
			
		||||
        //    return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 同步批量插入单条数据
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertData">要插入的数据</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>影响的行数</returns>
 | 
			
		||||
        public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
        }
 | 
			
		||||
        ///// <summary>
 | 
			
		||||
        ///// 同步批量插入单条数据
 | 
			
		||||
        ///// </summary>
 | 
			
		||||
        ///// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        ///// <param name="insertData">要插入的数据</param>
 | 
			
		||||
        ///// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        ///// <returns>影响的行数</returns>
 | 
			
		||||
        //public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        //{
 | 
			
		||||
        //    return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 创建分页批量插入器
 | 
			
		||||
@@ -115,9 +120,10 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertList">要插入的数据列表</param>
 | 
			
		||||
        /// <param name="tableName">表名称</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>插入的记录数</returns>
 | 
			
		||||
        public async Task<int> BulkCopyAsync<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        public async Task<int> BulkCopyAsync<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            var result = 0;
 | 
			
		||||
            var fileName = $"{Guid.NewGuid()}.csv";
 | 
			
		||||
@@ -127,12 +133,12 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                // 准备多部分表单数据
 | 
			
		||||
                var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
 | 
			
		||||
                var list = new List<Hashtable>();
 | 
			
		||||
                var name = db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
 | 
			
		||||
                tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
 | 
			
		||||
 | 
			
		||||
                // 获取或创建列信息缓存
 | 
			
		||||
                var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode();
 | 
			
		||||
                var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () =>
 | 
			
		||||
                 db.CopyNew().DbMaintenance.GetColumnInfosByTableName(name));
 | 
			
		||||
                 db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName));
 | 
			
		||||
 | 
			
		||||
                // 构建schema信息
 | 
			
		||||
                columns.ForEach(d =>
 | 
			
		||||
@@ -170,8 +176,8 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                // 准备HTTP请求内容
 | 
			
		||||
                using var httpContent = new MultipartFormDataContent(boundary);
 | 
			
		||||
                using var fileStream = File.OpenRead(filePath);
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(this.authorization))
 | 
			
		||||
                    client.DefaultRequestHeaders.Add("Authorization", this.authorization);
 | 
			
		||||
                //if (!string.IsNullOrWhiteSpace(this.authorization))
 | 
			
		||||
                //    client.DefaultRequestHeaders.Add("Authorization", this.authorization);
 | 
			
		||||
                httpContent.Add(new StringContent(schema), "schema");
 | 
			
		||||
                var streamContent = new StreamContent(fileStream);
 | 
			
		||||
                streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
 | 
			
		||||
@@ -183,8 +189,8 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
                    "multipart/form-data; boundary=" + boundary);
 | 
			
		||||
 | 
			
		||||
                // 发送请求并处理响应
 | 
			
		||||
                var httpResponseMessage =
 | 
			
		||||
                    await Post(client, name, httpContent).ConfigureAwait(false);
 | 
			
		||||
                using var httpResponseMessage =
 | 
			
		||||
                      await Post(client, tableName, httpContent).ConfigureAwait(false);
 | 
			
		||||
                var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
			
		||||
                var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);
 | 
			
		||||
 | 
			
		||||
@@ -266,11 +272,12 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">数据类型</typeparam>
 | 
			
		||||
        /// <param name="insertList">要插入的数据列表</param>
 | 
			
		||||
        /// <param name="tableName">表名称</param>
 | 
			
		||||
        /// <param name="dateFormat">日期格式字符串</param>
 | 
			
		||||
        /// <returns>插入的记录数</returns>
 | 
			
		||||
        public int BulkCopy<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        public int BulkCopy<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
 | 
			
		||||
        {
 | 
			
		||||
            return BulkCopyAsync(insertList, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
            return BulkCopyAsync(insertList, tableName, dateFormat).GetAwaiter().GetResult();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@@ -280,7 +287,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <param name="httpPort">HTTP端口</param>
 | 
			
		||||
        /// <param name="username">用户名</param>
 | 
			
		||||
        /// <param name="password">密码</param>
 | 
			
		||||
        private void BindHost(string host, string httpPort, string username, string password)
 | 
			
		||||
        private void BindHost(string host, int httpPort, string username, string password)
 | 
			
		||||
        {
 | 
			
		||||
            url = host;
 | 
			
		||||
            if (url.EndsWith('/'))
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
{
 | 
			
		||||
    public static class QuestDbSqlSugarClientExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static QuestDbRestAPI RestApi(this ISqlSugarClient db)
 | 
			
		||||
        public static QuestDbRestAPI RestApi(this ISqlSugarClient db, int httpPort = 9000)
 | 
			
		||||
        {
 | 
			
		||||
            return new QuestDbRestAPI(db);
 | 
			
		||||
            return new QuestDbRestAPI(db, httpPort);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <param name="context">SqlSugar提供者</param>
 | 
			
		||||
        /// <param name="dataRecord">数据记录器</param>
 | 
			
		||||
        /// <param name="fieldNames">字段名列表</param>
 | 
			
		||||
        public IDataReaderEntityBuilder(SqlSugarProvider context, IDataRecord dataRecord, List<string> fieldNames)
 | 
			
		||||
        public IDataReaderEntityBuilder(SqlSugarProvider context, IDataRecord dataRecord, IEnumerable<string> fieldNames)
 | 
			
		||||
        {
 | 
			
		||||
            this.Context = context;
 | 
			
		||||
            this.DataRecord = dataRecord;
 | 
			
		||||
 
 | 
			
		||||
@@ -679,7 +679,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
            {
 | 
			
		||||
                var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
 | 
			
		||||
                    columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
 | 
			
		||||
                    columns.Select(it => it.Item1)).CreateBuilder(typeof(T));
 | 
			
		||||
                return cacheResult;
 | 
			
		||||
            });
 | 
			
		||||
            using (dr)
 | 
			
		||||
@@ -706,7 +706,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
            {
 | 
			
		||||
                var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
 | 
			
		||||
                    columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
 | 
			
		||||
                    columns.Select(it => it.Item1)).CreateBuilder(typeof(T));
 | 
			
		||||
                return cacheResult;
 | 
			
		||||
            });
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested) yield break;
 | 
			
		||||
@@ -743,7 +743,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
            {
 | 
			
		||||
                var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
 | 
			
		||||
                    columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
 | 
			
		||||
                    columns.Select(it => it.Item1)).CreateBuilder(typeof(T));
 | 
			
		||||
                return cacheResult;
 | 
			
		||||
            });
 | 
			
		||||
            using (dr)
 | 
			
		||||
@@ -775,7 +775,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            IDataReaderEntityBuilder<T> entytyList = this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
            {
 | 
			
		||||
                var cacheResult = new IDataReaderEntityBuilder<T>(this.Context, dr,
 | 
			
		||||
                    columns.Select(it => it.Item1).ToList()).CreateBuilder(typeof(T));
 | 
			
		||||
                    columns.Select(it => it.Item1)).CreateBuilder(typeof(T));
 | 
			
		||||
                return cacheResult;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,6 @@
 | 
			
		||||
        V Get<V>(string key);
 | 
			
		||||
        IEnumerable<string> GetAllKey<V>();
 | 
			
		||||
        void Remove<V>(string key);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue);
 | 
			
		||||
        V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetAllKey();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue)
 | 
			
		||||
        public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return ReflectionInoCore<V>.GetInstance().GetOrCreate(cacheKey, create);
 | 
			
		||||
        }
 | 
			
		||||
@@ -43,10 +43,13 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
    }
 | 
			
		||||
    public class ReflectionInoCore<V>
 | 
			
		||||
    {
 | 
			
		||||
        private MemoryCache InstanceCache => MemoryCache.Instance;
 | 
			
		||||
        private MemoryCache InstanceCache = new MemoryCache() { Expire = 180 };
 | 
			
		||||
        private static ReflectionInoCore<V> _instance = null;
 | 
			
		||||
        private static readonly object _instanceLock = new object();
 | 
			
		||||
        private ReflectionInoCore() { }
 | 
			
		||||
        private ReflectionInoCore()
 | 
			
		||||
        {
 | 
			
		||||
        
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V this[string key]
 | 
			
		||||
        {
 | 
			
		||||
@@ -86,7 +89,7 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
 | 
			
		||||
        public void Add(string key, V value, int cacheDurationInSeconds)
 | 
			
		||||
        {
 | 
			
		||||
            Check.ThrowNotSupportedException("ReflectionInoCache.Add(string key, V value, int cacheDurationInSeconds)");
 | 
			
		||||
            this.InstanceCache.Add<V>(key, value, cacheDurationInSeconds);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Remove(string key)
 | 
			
		||||
@@ -107,9 +110,10 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
            return this.InstanceCache.Keys;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create)
 | 
			
		||||
        public V GetOrCreate(string cacheKey, Func<V> create, int expire = 3600)
 | 
			
		||||
        {
 | 
			
		||||
            return InstanceCache.GetOrAdd<V>(cacheKey, (a) => create());
 | 
			
		||||
            return InstanceCache.GetOrAdd<V>(cacheKey, (a) =>
 | 
			
		||||
            create(), expire);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    public static class ReflectionInoHelper
 | 
			
		||||
 
 | 
			
		||||
@@ -447,6 +447,28 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "QuestDB.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                {
 | 
			
		||||
                    return GetColInfo(tableName);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = String.Format(GetColumnInfosByTableNameSql, tableName);
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 
 | 
			
		||||
@@ -406,22 +406,24 @@ AND sql LIKE '%" + tableName + "%'");
 | 
			
		||||
        public override bool CreateDatabase(string databaseName, string databaseDirectory = null)
 | 
			
		||||
        {
 | 
			
		||||
            var connString = this.Context.CurrentConnectionConfig.ConnectionString;
 | 
			
		||||
            var path = Regex.Match(connString, @"[a-z,A-Z]\:\\.+\\").Value;
 | 
			
		||||
            if (path.IsNullOrEmpty())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // 提取 Data Source=xxx(不管是绝对还是相对路径)
 | 
			
		||||
            var match = Regex.Match(connString, @"(?i)Data\s+Source\s*=\s*(.+?)(;|$)");
 | 
			
		||||
            if (match.Success)
 | 
			
		||||
            {
 | 
			
		||||
                path = Regex.Match(connString, @"\/.+\/").Value;
 | 
			
		||||
            }
 | 
			
		||||
            if (path.IsNullOrEmpty())
 | 
			
		||||
            {
 | 
			
		||||
                path = Regex.Match(connString, @"[a-z,A-Z]\:\\").Value;
 | 
			
		||||
            }
 | 
			
		||||
            if (!path.IsNullOrEmpty())
 | 
			
		||||
            {
 | 
			
		||||
                if (!FileHelper.IsExistDirectory(path))
 | 
			
		||||
                var filePath = match.Groups[1].Value.Trim(); // => ./DB/data.sqlite
 | 
			
		||||
                var folderPath = Path.GetDirectoryName(filePath); // => ./DB
 | 
			
		||||
 | 
			
		||||
                if (!folderPath.IsNullOrEmpty())
 | 
			
		||||
                {
 | 
			
		||||
                    FileHelper.CreateDirectory(path);
 | 
			
		||||
                    if (!FileHelper.IsExistDirectory(folderPath))
 | 
			
		||||
                    {
 | 
			
		||||
                        FileHelper.CreateDirectory(folderPath);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.Context.Ado.Connection.Open();
 | 
			
		||||
            this.Context.Ado.Connection.Close();
 | 
			
		||||
            return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -717,8 +717,32 @@ namespace ThingsGateway.SqlSugar
 | 
			
		||||
        /// <returns>列信息列表</returns>
 | 
			
		||||
        public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
 | 
			
		||||
        {
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
 | 
			
		||||
            string cacheKey = "TDengine.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
 | 
			
		||||
            cacheKey = GetCacheKey(cacheKey);
 | 
			
		||||
 | 
			
		||||
            if (isCache)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
 | 
			
		||||
                     {
 | 
			
		||||
                         return GetColInfo(tableName);
 | 
			
		||||
                     });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return GetColInfo(tableName);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<DbColumnInfo> GetColInfo(string tableName)
 | 
			
		||||
        {
 | 
			
		||||
            List<DbColumnInfo> result = new List<DbColumnInfo>();
 | 
			
		||||
 | 
			
		||||
            var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
 | 
			
		||||
            DataTable dt = null;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,11 +31,12 @@
 | 
			
		||||
		<PackageReference Include="CsvHelper" Version="33.1.0" />
 | 
			
		||||
		<PackageReference Include="TDengine.Connector" Version="3.1.7" />
 | 
			
		||||
		<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.20" />
 | 
			
		||||
		<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.21" />
 | 
			
		||||
		<PackageReference Include="System.Data.Common" Version="4.3.0" />
 | 
			
		||||
		<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.0" />
 | 
			
		||||
		<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
 | 
			
		||||
		<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
 | 
			
		||||
		<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
<Project>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<PluginVersion>10.9.91</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.9.91</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.9.92</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>2.9.29</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.9.29</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.18</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.7</NET9Version>
 | 
			
		||||
		<PluginVersion>10.10.9</PluginVersion>
 | 
			
		||||
		<ProPluginVersion>10.10.9</ProPluginVersion>
 | 
			
		||||
		<DefaultVersion>10.10.12</DefaultVersion>
 | 
			
		||||
		<AuthenticationVersion>10.10.1</AuthenticationVersion>
 | 
			
		||||
		<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
 | 
			
		||||
		<NET8Version>8.0.19</NET8Version>
 | 
			
		||||
		<NET9Version>9.0.8</NET9Version>
 | 
			
		||||
		<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>
 | 
			
		||||
		<IsTrimmable>false</IsTrimmable>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<Project>
 | 
			
		||||
	<PropertyGroup>
 | 
			
		||||
		<TargetFrameworks>net462;netstandard2.0;net6.0;</TargetFrameworks>
 | 
			
		||||
		<TargetFrameworks>net462;netstandard2.0;net6.0;net8.0</TargetFrameworks>
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	</PropertyGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.10.0" />
 | 
			
		||||
		<PackageReference Include="CS-Script" Version="4.10.1" />
 | 
			
		||||
	</ItemGroup>
 | 
			
		||||
 | 
			
		||||
	<ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,8 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private ToastService ToastService { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    ITextFileReadService TextFileReadService { get; set; }
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        Disposed = true;
 | 
			
		||||
@@ -94,7 +95,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
 | 
			
		||||
            if (LogPath != null)
 | 
			
		||||
            {
 | 
			
		||||
                var files = TextFileReader.GetFiles(LogPath);
 | 
			
		||||
                var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
                if (!files.IsSuccess)
 | 
			
		||||
                {
 | 
			
		||||
                    Messages = new List<LogMessage>();
 | 
			
		||||
@@ -105,7 +106,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
                    await Task.Run(async () =>
 | 
			
		||||
                    {
 | 
			
		||||
                        Stopwatch sw = Stopwatch.StartNew();
 | 
			
		||||
                        var result = TextFileReader.LastLog(files.Content.FirstOrDefault());
 | 
			
		||||
                        var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault());
 | 
			
		||||
                        if (result.IsSuccess)
 | 
			
		||||
                        {
 | 
			
		||||
                            Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList();
 | 
			
		||||
@@ -143,7 +144,7 @@ public partial class LogConsole : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        if (LogPath != null)
 | 
			
		||||
        {
 | 
			
		||||
            var files = TextFileReader.GetFiles(LogPath);
 | 
			
		||||
            var files = await TextFileReadService.GetLogFiles(LogPath);
 | 
			
		||||
            if (files.IsSuccess)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var item in files.Content)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ public class PlatformService : IPlatformService
 | 
			
		||||
 | 
			
		||||
    public async Task OnLogExport(string logPath)
 | 
			
		||||
    {
 | 
			
		||||
        var files = TextFileReader.GetFiles(logPath);
 | 
			
		||||
        var files = TextFileReader.GetLogFiles(logPath);
 | 
			
		||||
        if (!files.IsSuccess)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,5 +33,6 @@ public class Startup : AppStartup
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        services.AddScoped<IPlatformService, PlatformService>();
 | 
			
		||||
        services.AddSingleton<ITextFileReadService, TextFileReadService>();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    public OtherChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        ChannelOptions = channelOptions;
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
@@ -39,7 +40,7 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; set; } = new();
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannelOptions ChannelOptions { get; }
 | 
			
		||||
@@ -51,16 +52,16 @@ public class OtherChannel : SetupConfigObject, IClientChannel
 | 
			
		||||
    public ConcurrentList<IDevice> Collects { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Starting { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Starting { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoped { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoped { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoping { get; } = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    public SerialPortChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        ChannelOptions = channelOptions;
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
@@ -36,7 +37,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; set; } = new();
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannelOptions ChannelOptions { get; }
 | 
			
		||||
@@ -51,16 +52,16 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => ProtectedDataHandlingAdapter;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Starting { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Starting { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoped { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoped { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoping { get; } = new();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
 | 
			
		||||
    public TcpClientChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        ChannelOptions = channelOptions;
 | 
			
		||||
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
 
 | 
			
		||||
@@ -134,6 +134,7 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
 | 
			
		||||
 | 
			
		||||
    protected override void SafetyDispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        m_transport?.SafeCancel();
 | 
			
		||||
        m_transport?.SafeDispose();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
@@ -179,7 +180,7 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; set; } = new();
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannelOptions ChannelOptions { get; }
 | 
			
		||||
@@ -191,15 +192,15 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
    public bool Online => ServerState == ServerState.Running;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Starting { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Starting { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoped { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoped { get; } = new();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoping { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public Task<Result> CloseAsync(string msg, CancellationToken token)
 | 
			
		||||
@@ -227,8 +228,8 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
 | 
			
		||||
        data.ResetSign(MinSign, MaxSign);
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
    public int MaxSign { get; set; }
 | 
			
		||||
    public int MinSign { get; set; }
 | 
			
		||||
    public int MaxSign { get; private set; } = 0;
 | 
			
		||||
    public int MinSign { get; private set; } = ushort.MaxValue;
 | 
			
		||||
    public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
 | 
			
		||||
    {
 | 
			
		||||
        MinSign = minSign;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
        pool?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; set; } = new();
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannelOptions ChannelOptions { get; internal set; }
 | 
			
		||||
@@ -47,15 +47,15 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => DataHandlingAdapter;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Starting { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Starting { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoped { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoped { get; } = new();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoping { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    public UdpSessionChannel(IChannelOptions channelOptions)
 | 
			
		||||
    {
 | 
			
		||||
        ChannelOptions = channelOptions;
 | 
			
		||||
        ResetSign();
 | 
			
		||||
    }
 | 
			
		||||
    public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
 | 
			
		||||
 | 
			
		||||
@@ -37,7 +38,7 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; set; } = new();
 | 
			
		||||
    public ChannelReceivedEventHandler ChannelReceived { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannelOptions ChannelOptions { get; }
 | 
			
		||||
@@ -55,15 +56,15 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    public DataHandlingAdapter ReadOnlyDataHandlingAdapter => DataHandlingAdapter;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Started { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Started { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Starting { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Starting { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoped { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoped { get; } = new();
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public ChannelEventHandler Stoping { get; set; } = new();
 | 
			
		||||
    public ChannelEventHandler Stoping { get; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 等待池
 | 
			
		||||
@@ -204,6 +205,7 @@ public class UdpSessionChannel : UdpSession, IClientChannel
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void SafetyDispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        m_transport?.SafeCancel();
 | 
			
		||||
        m_transport?.SafeDispose();
 | 
			
		||||
        WaitHandlePool.SafeDispose();
 | 
			
		||||
        base.SafetyDispose(disposing);
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
    protected override FilterResult Filter<TByteBlock>(ref TByteBlock byteBlock, bool beCached, ref TRequest request, ref int tempCapacity)
 | 
			
		||||
    {
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString() : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString(' ') : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -172,7 +172,7 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
    {
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString() : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString(' ') : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
 | 
			
		||||
        //发送
 | 
			
		||||
        await GoSendAsync(memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
@@ -191,7 +191,7 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
 | 
			
		||||
        {
 | 
			
		||||
            sendMessage.Build(ref byteBlock);
 | 
			
		||||
            if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? byteBlock.Span.ToHexString() : (byteBlock.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
                Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? byteBlock.Span.ToHexString(' ') : (byteBlock.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
            //非并发主从协议
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
            byteBlock.Position = 0;
 | 
			
		||||
 | 
			
		||||
            if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                Logger?.Trace($"{remoteEndPoint}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString() : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
                Logger?.Trace($"{remoteEndPoint}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString(' ') : byteBlock.ToString(byteBlock.Position))}");
 | 
			
		||||
 | 
			
		||||
            TRequest request = null;
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
@@ -151,7 +151,7 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
        cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
 | 
			
		||||
        if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString() : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
            Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString(' ') : (memory.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
        //发送
 | 
			
		||||
        await GoSendAsync(endPoint, memory, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
@@ -169,7 +169,7 @@ public class DeviceUdpDataHandleAdapter<TRequest> : UdpDataHandlingAdapter where
 | 
			
		||||
        {
 | 
			
		||||
            sendMessage.Build(ref byteBlock);
 | 
			
		||||
            if (Logger?.LogLevel <= LogLevel.Trace)
 | 
			
		||||
                Logger?.Trace($"{endPoint}- Send:{(IsHexLog ? byteBlock.Span.ToHexString() : (byteBlock.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
                Logger?.Trace($"{endPoint}- Send:{(IsHexLog ? byteBlock.Span.ToHexString(' ') : (byteBlock.Span.ToString(Encoding.UTF8)))}");
 | 
			
		||||
 | 
			
		||||
            if (IsSingleThread)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 协议基类
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
public abstract class DeviceBase : AsyncAndSyncDisposableObject, IDevice
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IChannel Channel { get; private set; }
 | 
			
		||||
@@ -394,6 +394,8 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
                await connectWaitLock.WaitAsync(token).ConfigureAwait(false);
 | 
			
		||||
                if (AutoConnect && Channel != null && Channel?.Online != true)
 | 
			
		||||
                {
 | 
			
		||||
                    if (Channel.PluginManager == null)
 | 
			
		||||
                        await Channel.SetupAsync(Channel.Config.Clone()).ConfigureAwait(false);
 | 
			
		||||
                    await Channel.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    await Task.Delay(500, token).ConfigureAwait(false);
 | 
			
		||||
                    await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, token).ConfigureAwait(false);
 | 
			
		||||
@@ -444,7 +446,13 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
    public virtual OperResult<IClientChannel> GetChannel(string socketId)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(socketId))
 | 
			
		||||
            return new OperResult<IClientChannel>() { Content = (IClientChannel)Channel };
 | 
			
		||||
        {
 | 
			
		||||
            if (Channel is IClientChannel clientChannel)
 | 
			
		||||
                return new OperResult<IClientChannel>() { Content = clientChannel };
 | 
			
		||||
            else
 | 
			
		||||
                return new OperResult<IClientChannel>("The communication link cannot be obtained, DtuId must be set!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (Channel is ITcpServiceChannel serviceChannel)
 | 
			
		||||
        {
 | 
			
		||||
@@ -462,7 +470,12 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            return new OperResult<IClientChannel>() { Content = (IClientChannel)Channel };
 | 
			
		||||
        {
 | 
			
		||||
            if (Channel is IClientChannel clientChannel)
 | 
			
		||||
                return new OperResult<IClientChannel>() { Content = clientChannel };
 | 
			
		||||
            else
 | 
			
		||||
                return new OperResult<IClientChannel>("The communication link cannot be obtained!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
@@ -540,11 +553,11 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
        WaitLock? waitLock = null;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await BefortSendAsync(clientChannel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var dtuId = this is IDtu dtu1 ? dtu1.DtuId : null;
 | 
			
		||||
            waitLock = GetWaitLock(clientChannel, dtuId);
 | 
			
		||||
 | 
			
		||||
            await BefortSendAsync(clientChannel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            EndPoint? endPoint = GetUdpEndpoint(dtuId);
 | 
			
		||||
@@ -562,12 +575,18 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
 | 
			
		||||
            var sendOperResult = await SendAsync(command, clientChannel, endPoint, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            if (!sendOperResult.IsSuccess)
 | 
			
		||||
                throw sendOperResult.Exception ?? new(sendOperResult.ErrorMessage ?? "unknown error");
 | 
			
		||||
                return new MessageBase(sendOperResult);
 | 
			
		||||
 | 
			
		||||
            waitData.SetCancellationToken(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            await waitData.WaitAsync(timeout).ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                waitData.SetCancellationToken(Channel.ClosedToken);
 | 
			
		||||
 | 
			
		||||
                await waitData.WaitAsync(timeout).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                return new MessageBase(ex);
 | 
			
		||||
            }
 | 
			
		||||
            var result = waitData.Check();
 | 
			
		||||
            if (result.IsSuccess)
 | 
			
		||||
            {
 | 
			
		||||
@@ -575,25 +594,11 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!this.DisposedValue)
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(timeout, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return new MessageBase(result);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            if (cancellationToken.IsCancellationRequested)
 | 
			
		||||
            {
 | 
			
		||||
                if (!this.DisposedValue)
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(timeout, CancellationToken.None).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return new MessageBase(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
@@ -1029,6 +1034,56 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override async Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        if (Channel != null)
 | 
			
		||||
        {
 | 
			
		||||
            Channel.Starting.Remove(ChannelStarting);
 | 
			
		||||
            Channel.Stoped.Remove(ChannelStoped);
 | 
			
		||||
            Channel.Started.Remove(ChannelStarted);
 | 
			
		||||
            Channel.Stoping.Remove(ChannelStoping);
 | 
			
		||||
            Channel.ChannelReceived.Remove(ChannelReceived);
 | 
			
		||||
 | 
			
		||||
            if (Channel.Collects.Count == 1)
 | 
			
		||||
            {
 | 
			
		||||
                if (Channel is ITcpServiceChannel tcpServiceChannel)
 | 
			
		||||
                {
 | 
			
		||||
                    tcpServiceChannel.Clients.ForEach(a => a.WaitHandlePool.SafeDispose());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    //只关闭,不释放
 | 
			
		||||
                    await Channel.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    if (Channel is IClientChannel client)
 | 
			
		||||
                    {
 | 
			
		||||
                        client.WaitHandlePool.SafeDispose();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Logger?.LogWarning(ex);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (Channel is ITcpServiceChannel tcpServiceChannel && this is IDtu dtu)
 | 
			
		||||
                {
 | 
			
		||||
                    if (tcpServiceChannel.TryGetClient($"ID={dtu.DtuId}", out var client))
 | 
			
		||||
                    {
 | 
			
		||||
                        client.WaitHandlePool?.SafeDispose();
 | 
			
		||||
                        await client.CloseAsync().ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Channel.Collects.Remove(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _deviceLogger?.TryDispose();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
    }
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public virtual Action<IPluginManager> ConfigurePlugins(TouchSocketConfig config)
 | 
			
		||||
    {
 | 
			
		||||
@@ -1046,4 +1101,5 @@ public abstract class DeviceBase : DisposableObject, IDevice
 | 
			
		||||
        return a => { };
 | 
			
		||||
    }
 | 
			
		||||
    public abstract ValueTask<OperResult<ReadOnlyMemory<byte>>> ReadAsync(object state, CancellationToken cancellationToken = default);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 协议设备接口
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IDevice : IDisposable, IDisposableObject
 | 
			
		||||
public interface IDevice : IDisposable, IDisposableObject, IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    #region 属性
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Buffers;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
@@ -235,5 +236,28 @@ public static class ByteBlockExtension
 | 
			
		||||
    }
 | 
			
		||||
    #endregion AsSegment
 | 
			
		||||
 | 
			
		||||
    #region ToString
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static string ToString<TByteBlock>(this TByteBlock byteBlock, long offset, long length) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return byteBlock.TotalSequence.Slice(offset, length).ToString(Encoding.UTF8);
 | 
			
		||||
    }
 | 
			
		||||
    public static string ToString(this ReadOnlySequence<byte> byteBlock, Encoding encoding)
 | 
			
		||||
    {
 | 
			
		||||
# if NET6_0_OR_GREATER
 | 
			
		||||
        return encoding.GetString(byteBlock);
 | 
			
		||||
#else
 | 
			
		||||
        using ContiguousMemoryBuffer contiguousMemoryBuffer = new(byteBlock);
 | 
			
		||||
        return contiguousMemoryBuffer.Memory.Span.ToString(encoding);
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public static string ToString<TByteBlock>(this TByteBlock byteBlock, long offset) where TByteBlock : IBytesReader
 | 
			
		||||
    {
 | 
			
		||||
        return ToString(byteBlock, offset, byteBlock.BytesRead + byteBlock.BytesRemaining - offset);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    #endregion ToString
 | 
			
		||||
}
 | 
			
		||||
@@ -252,7 +252,7 @@ public static class ByteExtensions
 | 
			
		||||
    /// 字节数组默认转16进制字符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToHexString(this ArraySegment<byte> buffer, char splite = ' ', int newLineCount = 0)
 | 
			
		||||
    public static string ToHexString(this ArraySegment<byte> buffer, char splite = default, int newLineCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return DataTransUtil.ByteToHexString(buffer, splite, newLineCount);
 | 
			
		||||
    }
 | 
			
		||||
@@ -261,7 +261,7 @@ public static class ByteExtensions
 | 
			
		||||
    /// 字节数组默认转16进制字符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToHexString(this byte[] buffer, char splite = ' ', int newLineCount = 0)
 | 
			
		||||
    public static string ToHexString(this byte[] buffer, char splite = default, int newLineCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return DataTransUtil.ByteToHexString(buffer, splite, newLineCount);
 | 
			
		||||
    }
 | 
			
		||||
@@ -269,7 +269,7 @@ public static class ByteExtensions
 | 
			
		||||
    /// 字节数组默认转16进制字符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToHexString(this Span<byte> buffer, char splite = ' ', int newLineCount = 0)
 | 
			
		||||
    public static string ToHexString(this Span<byte> buffer, char splite = default, int newLineCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return DataTransUtil.ByteToHexString(buffer, splite, newLineCount);
 | 
			
		||||
    }
 | 
			
		||||
@@ -277,7 +277,7 @@ public static class ByteExtensions
 | 
			
		||||
    /// 字节数组默认转16进制字符
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public static string ToHexString(this ReadOnlySpan<byte> buffer, char splite = ' ', int newLineCount = 0)
 | 
			
		||||
    public static string ToHexString(this ReadOnlySpan<byte> buffer, char splite = default, int newLineCount = 0)
 | 
			
		||||
    {
 | 
			
		||||
        return DataTransUtil.ByteToHexString(buffer, splite, newLineCount);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,73 @@
 | 
			
		||||
// ------------------------------------------------------------------------------
 | 
			
		||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
// 使用文档:https://thingsgateway.cn/
 | 
			
		||||
// QQ群:605534569
 | 
			
		||||
// ------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public static class DisposableExtensions
 | 
			
		||||
{
 | 
			
		||||
    #region IDisposable
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 安全性释放(不用判断对象是否为空)。不会抛出任何异常。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="dis"></param>
 | 
			
		||||
    /// <returns>释放状态,当对象为<see langword="null"/>,或者已被释放时,均会返回<see cref="Result.Success"/>,只有实际在释放时遇到异常时,才显示其他状态。</returns>
 | 
			
		||||
    public static async Task<Result> SafeDisposeAsync(this IAsyncDisposable dis)
 | 
			
		||||
    {
 | 
			
		||||
        if (dis == default)
 | 
			
		||||
        {
 | 
			
		||||
            return Result.Success;
 | 
			
		||||
        }
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await dis.DisposeAsync().ConfigureAwait(false);
 | 
			
		||||
            return Result.Success;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return Result.FromException(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion IDisposable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#if NET8_0_OR_GREATER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 安全地取消 <see cref="CancellationTokenSource"/>,并返回操作结果。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="tokenSource">要取消的 <see cref="CancellationTokenSource"/>。</param>
 | 
			
		||||
    /// <returns>一个 <see cref="Result"/> 对象,表示操作的结果。</returns>
 | 
			
		||||
    public static async Task<Result> SafeCancelAsync(this CancellationTokenSource tokenSource)
 | 
			
		||||
    {
 | 
			
		||||
        if (tokenSource is null)
 | 
			
		||||
        {
 | 
			
		||||
            return Result.Success;
 | 
			
		||||
        }
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await tokenSource.CancelAsync().ConfigureAwait(false);
 | 
			
		||||
            return Result.Success;
 | 
			
		||||
        }
 | 
			
		||||
        catch (ObjectDisposedException)
 | 
			
		||||
        {
 | 
			
		||||
            return Result.Disposed;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return Result.FromException(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
#pragma warning disable CA1851
 | 
			
		||||
 | 
			
		||||
public static class PackHelpers
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ public static class TextFileReader
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="directoryPath">目录路径</param>
 | 
			
		||||
    /// <returns>包含文件信息的列表</returns>
 | 
			
		||||
    public static OperResult<List<string>> GetFiles(string directoryPath)
 | 
			
		||||
    public static OperResult<List<string>> GetLogFiles(string directoryPath)
 | 
			
		||||
    {
 | 
			
		||||
        OperResult<List<string>> result = new(); // 初始化结果对象
 | 
			
		||||
        // 检查目录是否存在
 | 
			
		||||
@@ -91,7 +91,7 @@ public static class TextFileReader
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static OperResult<List<LogData>> LastLog(string file, int lineCount = 200)
 | 
			
		||||
    public static OperResult<List<LogData>> LastLogData(string file, int lineCount = 200)
 | 
			
		||||
    {
 | 
			
		||||
        if (!File.Exists(file))
 | 
			
		||||
            return new OperResult<List<LogData>>("The file path is invalid");
 | 
			
		||||
@@ -104,7 +104,7 @@ public static class TextFileReader
 | 
			
		||||
            {
 | 
			
		||||
                var fileInfo = new FileInfo(file);
 | 
			
		||||
                var length = fileInfo.Length;
 | 
			
		||||
                var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLog)}_{file})";
 | 
			
		||||
                var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLogData)}_{file})";
 | 
			
		||||
                if (_cache.TryGetValue<LogDataCache>(cacheKey, out var cachedData))
 | 
			
		||||
                {
 | 
			
		||||
                    if (cachedData != null && cachedData.Length == length)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,27 @@ namespace ThingsGateway.Foundation;
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class OperResultExtension
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public static OperResult<object> GetOperResult(this IOperResult data)
 | 
			
		||||
    {
 | 
			
		||||
        OperResult<object> result = new(data);
 | 
			
		||||
        var operResultType = typeof(IOperResult<>);
 | 
			
		||||
        var interfaceType = data.GetType().GetInterfaces()
 | 
			
		||||
            .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == operResultType);
 | 
			
		||||
 | 
			
		||||
        if (interfaceType != null)
 | 
			
		||||
        {
 | 
			
		||||
            var contentProperty = interfaceType.GetProperty("Content");
 | 
			
		||||
            if (contentProperty != null)
 | 
			
		||||
            {
 | 
			
		||||
                result.Content = contentProperty.GetValue(data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 转换对应类型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
public interface ITextFileReadService
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取指定目录下所有文件信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="directoryPath">目录路径</param>
 | 
			
		||||
    /// <returns>包含文件信息的列表</returns>
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFiles(string directoryPath);
 | 
			
		||||
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200);
 | 
			
		||||
}
 | 
			
		||||
@@ -8,13 +8,13 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Riok.Mapperly.Abstractions;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Dmtp;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Upgrade;
 | 
			
		||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
 | 
			
		||||
public static partial class UpgradeMapper
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
public class TextFileReadService : ITextFileReadService
 | 
			
		||||
{
 | 
			
		||||
    public static partial List<TcpSessionClientDto> AdaptListTcpSessionClientDto(this List<TcpDmtpSessionClient> src);
 | 
			
		||||
    public Task<OperResult<List<string>>> GetLogFiles(string directoryPath) => Task.FromResult(TextFileReader.GetLogFiles(directoryPath));
 | 
			
		||||
 | 
			
		||||
    public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200) => Task.FromResult(TextFileReader.LastLogData(file, lineCount));
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
 | 
			
		||||
//  CSDN博客:https://blog.csdn.net/qq_40374647
 | 
			
		||||
//  哔哩哔哩视频:https://space.bilibili.com/94253567
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/RRQM_Home
 | 
			
		||||
//  Github源代码仓库:https://github.com/RRQM
 | 
			
		||||
//  API首页:https://touchsocket.net/
 | 
			
		||||
//  交流QQ群:234762506
 | 
			
		||||
//  感谢您的下载和使用
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 具有释放的对象。内部实现了<see cref="GC.SuppressFinalize(object)"/>,但不包括析构函数相关。
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract partial class AsyncAndSyncDisposableObject :
 | 
			
		||||
   IDisposableObject,
 | 
			
		||||
   IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断当前对象是否已经被释放。
 | 
			
		||||
    /// 如果已经被释放,则抛出<see cref="ObjectDisposedException"/>异常。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <exception cref="ObjectDisposedException">当对象已经被释放时抛出此异常</exception>
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    protected void ThrowIfDisposed()
 | 
			
		||||
    {
 | 
			
		||||
        // 检查对象是否已经被释放
 | 
			
		||||
        if (this.m_disposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果对象已被释放,抛出ObjectDisposedException异常
 | 
			
		||||
            throw new ObjectDisposedException($"The object instance with type {this.GetType().FullName} has been released");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int m_count = 0;
 | 
			
		||||
    private int m_asyncCount = 0;
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断是否已释放。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private volatile bool m_disposedValue;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public bool DisposedValue => this.m_disposedValue;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 处置资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="disposing">一个值,表示是否释放托管资源</param>
 | 
			
		||||
    protected virtual void Dispose(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        // 标记当前对象为已处置状态
 | 
			
		||||
        this.m_disposedValue = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源。内部已经处理了<see cref="GC.SuppressFinalize(object)"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        if (this.DisposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Increment(ref this.m_count) == 1)
 | 
			
		||||
        {
 | 
			
		||||
            this.Dispose(disposing: true);
 | 
			
		||||
        }
 | 
			
		||||
        GC.SuppressFinalize(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 处置资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="disposing">一个值,表示是否释放托管资源</param>
 | 
			
		||||
    protected virtual Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        // 标记当前对象为已处置状态
 | 
			
		||||
        this.m_disposedValue = true;
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源。内部已经处理了<see cref="GC.SuppressFinalize(object)"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async ValueTask DisposeAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (this.DisposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //if (Interlocked.Increment(ref this.m_count) == 1)
 | 
			
		||||
        //{
 | 
			
		||||
        //    this.Dispose(disposing: true);
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Increment(ref this.m_asyncCount) == 1)
 | 
			
		||||
        {
 | 
			
		||||
            await this.DisposeAsync(disposing: true).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        GC.SuppressFinalize(this);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,108 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权(除特别声明或在XREF结尾的命名空间的代码)归作者本人若汝棋茗所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议,若本仓库没有设置,则按MIT开源协议授权
 | 
			
		||||
//  CSDN博客:https://blog.csdn.net/qq_40374647
 | 
			
		||||
//  哔哩哔哩视频:https://space.bilibili.com/94253567
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/RRQM_Home
 | 
			
		||||
//  Github源代码仓库:https://github.com/RRQM
 | 
			
		||||
//  API首页:https://touchsocket.net/
 | 
			
		||||
//  交流QQ群:234762506
 | 
			
		||||
//  感谢您的下载和使用
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Foundation;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 具有释放的对象。内部实现了<see cref="GC.SuppressFinalize(object)"/>,但不包括析构函数相关。
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract partial class AsyncDisposableObject :
 | 
			
		||||
   //IDisposableObject,
 | 
			
		||||
   IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断当前对象是否已经被释放。
 | 
			
		||||
    /// 如果已经被释放,则抛出<see cref="ObjectDisposedException"/>异常。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <exception cref="ObjectDisposedException">当对象已经被释放时抛出此异常</exception>
 | 
			
		||||
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | 
			
		||||
    protected void ThrowIfDisposed()
 | 
			
		||||
    {
 | 
			
		||||
        // 检查对象是否已经被释放
 | 
			
		||||
        if (this.m_disposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果对象已被释放,抛出ObjectDisposedException异常
 | 
			
		||||
            throw new ObjectDisposedException($"The object instance with type {this.GetType().FullName} has been released");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int m_asyncCount = 0;
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 判断是否已释放。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private volatile bool m_disposedValue;
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public bool DisposedValue => this.m_disposedValue;
 | 
			
		||||
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// 处置资源
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    ///// <param name="disposing">一个值,表示是否释放托管资源</param>
 | 
			
		||||
    //protected virtual void Dispose(bool disposing)
 | 
			
		||||
    //{
 | 
			
		||||
    //    // 标记当前对象为已处置状态
 | 
			
		||||
    //    this.m_disposedValue = true;
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// 释放资源。内部已经处理了<see cref="GC.SuppressFinalize(object)"/>
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    //public void Dispose()
 | 
			
		||||
    //{
 | 
			
		||||
    //    if (this.DisposedValue)
 | 
			
		||||
    //    {
 | 
			
		||||
    //        return;
 | 
			
		||||
    //    }
 | 
			
		||||
 | 
			
		||||
    //    if (Interlocked.Increment(ref this.m_count) == 1)
 | 
			
		||||
    //    {
 | 
			
		||||
    //        this.Dispose(disposing: true);
 | 
			
		||||
    //    }
 | 
			
		||||
    //    GC.SuppressFinalize(this);
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 处置资源
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="disposing">一个值,表示是否释放托管资源</param>
 | 
			
		||||
    protected virtual Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        // 标记当前对象为已处置状态
 | 
			
		||||
        this.m_disposedValue = true;
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源。内部已经处理了<see cref="GC.SuppressFinalize(object)"/>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async ValueTask DisposeAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (this.DisposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //if (Interlocked.Increment(ref this.m_count) == 1)
 | 
			
		||||
        //{
 | 
			
		||||
        //    this.Dispose(disposing: true);
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Increment(ref this.m_asyncCount) == 1)
 | 
			
		||||
        {
 | 
			
		||||
            await this.DisposeAsync(disposing: true).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        GC.SuppressFinalize(this);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,48 +14,88 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public class AsyncReadWriteLock
 | 
			
		||||
{
 | 
			
		||||
    private readonly int _writeReadRatio = 3; // 写3次会允许1次读,但写入也不会被阻止,具体协议取决于插件协议实现
 | 
			
		||||
    public AsyncReadWriteLock(int writeReadRatio)
 | 
			
		||||
    {
 | 
			
		||||
        _writeReadRatio = writeReadRatio;
 | 
			
		||||
    }
 | 
			
		||||
    private AsyncAutoResetEvent _readerLock = new AsyncAutoResetEvent(false); // 控制读计数
 | 
			
		||||
    private long _writerCount = 0; // 当前活跃的写线程数
 | 
			
		||||
    private long _readerCount = 0; // 当前被阻塞的读线程数
 | 
			
		||||
    private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取读锁,支持多个线程并发读取,但写入时会阻止所有读取。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task<CancellationToken> ReaderLockAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Read(ref _writerCount) > 0)
 | 
			
		||||
        {
 | 
			
		||||
            Interlocked.Increment(ref _readerCount);
 | 
			
		||||
 | 
			
		||||
            // 第一个读者需要获取写入锁,防止写操作
 | 
			
		||||
            await _readerLock.WaitOneAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            Interlocked.Decrement(ref _readerCount);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        return _cancellationTokenSource.Token;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public bool WriteWaited => _writerCount > 0;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取写锁,阻止所有读取。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IDisposable WriterLock()
 | 
			
		||||
    public async Task<IDisposable> WriterLockAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (Interlocked.Increment(ref _writerCount) == 1)
 | 
			
		||||
        {
 | 
			
		||||
            var cancellationTokenSource = _cancellationTokenSource;
 | 
			
		||||
            _cancellationTokenSource = new();
 | 
			
		||||
            cancellationTokenSource.Cancel();//取消读取
 | 
			
		||||
            await cancellationTokenSource.SafeCancelAsync().ConfigureAwait(false); // 取消读取
 | 
			
		||||
            cancellationTokenSource.SafeDispose();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Writer(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private object lockObject = new();
 | 
			
		||||
    private void ReleaseWriter()
 | 
			
		||||
    {
 | 
			
		||||
        if (Interlocked.Decrement(ref _writerCount) == 0)
 | 
			
		||||
        var writerCount = Interlocked.Decrement(ref _writerCount);
 | 
			
		||||
        if (writerCount == 0)
 | 
			
		||||
        {
 | 
			
		||||
            var resetEvent = _readerLock;
 | 
			
		||||
            _readerLock = new(false);
 | 
			
		||||
            Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
 | 
			
		||||
            resetEvent.SetAll();
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            lock (lockObject)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                // 读写占空比, 用于控制写操作与读操作的比率。该比率 n 次写入操作会执行一次读取操作。即使在应用程序执行大量的连续写入操作时,也必须确保足够的读取数据处理时间。相对于更加均衡的读写数据流而言,该特点使得外部写入可连续无顾忌操作
 | 
			
		||||
                if (_writeReadRatio > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var count = Interlocked.Increment(ref _writeSinceLastReadCount);
 | 
			
		||||
                    if (count >= _writeReadRatio)
 | 
			
		||||
                    {
 | 
			
		||||
                        Interlocked.Exchange(ref _writeSinceLastReadCount, 0);
 | 
			
		||||
                        _readerLock.Set();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    _readerLock.Set();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int _writeSinceLastReadCount = 0;
 | 
			
		||||
    private struct Writer : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        private readonly AsyncReadWriteLock _lock;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,20 +16,20 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public static class ThingsGatewayCacheConst
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 通道
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Cache_Channel = $"{Cache_Prefix}Cache_Channel:List";
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// 通道
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    //public const string Cache_Channel = $"{Cache_Prefix}Cache_Channel:List";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// device
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Cache_Device = $"{Cache_Prefix}Cache_Device:List";
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// device
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    //public const string Cache_Device = $"{Cache_Prefix}Cache_Device:List";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// variable
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public const string Cache_Variable = $"{Cache_Prefix}Cache_Variable:IdNameList";
 | 
			
		||||
    ///// <summary>
 | 
			
		||||
    ///// variable
 | 
			
		||||
    ///// </summary>
 | 
			
		||||
    //public const string Cache_Variable = $"{Cache_Prefix}Cache_Variable:IdNameList";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 前缀
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
[ApiDescriptionSettings("ThingsGateway.OpenApi", Order = 200)]
 | 
			
		||||
[Route("openApi/autoUpdate")]
 | 
			
		||||
[RolePermission]
 | 
			
		||||
[RequestAudit]
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class AutoUpdateController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private IUpdateZipFileHostedService _updateZipFileService;
 | 
			
		||||
    public AutoUpdateController(IUpdateZipFileHostedService updateZipFileService)
 | 
			
		||||
    {
 | 
			
		||||
        _updateZipFileService = updateZipFileService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 检查更新
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("update")]
 | 
			
		||||
    public async Task Update()
 | 
			
		||||
    {
 | 
			
		||||
        var data = await _updateZipFileService.GetList().ConfigureAwait(false);
 | 
			
		||||
        if (data.Count != 0)
 | 
			
		||||
            await _updateZipFileService.Update(data.OrderByDescending(a => a.Version).FirstOrDefault()).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ using System.IO.Ports;
 | 
			
		||||
using ThingsGateway.FriendlyException;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
@@ -34,7 +35,9 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[RequestAudit]
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class ControlController : ControllerBase
 | 
			
		||||
[TouchSocket.WebApi.Router("/miniapi/[api]/[action]")]
 | 
			
		||||
[TouchSocket.WebApi.EnableCors("cors")]
 | 
			
		||||
public class ControlController : ControllerBase, IRpcServer
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -43,6 +46,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("removeAllCache")]
 | 
			
		||||
    [DisplayName("清空全部缓存")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public void RemoveAllCache()
 | 
			
		||||
    {
 | 
			
		||||
        App.CacheService.Clear();
 | 
			
		||||
@@ -54,6 +58,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("removeCache")]
 | 
			
		||||
    [DisplayName("删除通道/设备缓存")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public void RemoveCache()
 | 
			
		||||
    {
 | 
			
		||||
        App.GetService<IDeviceService>().DeleteDeviceFromCache();
 | 
			
		||||
@@ -66,6 +71,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("pauseBusinessThread")]
 | 
			
		||||
    [DisplayName("控制设备线程启停")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task PauseDeviceThreadAsync(long id, bool pause)
 | 
			
		||||
    {
 | 
			
		||||
        if (GlobalData.IdDevices.TryGetValue(id, out var device))
 | 
			
		||||
@@ -85,6 +91,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("restartScopeThread")]
 | 
			
		||||
    [DisplayName("重启当前机构线程")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task RestartScopeThread()
 | 
			
		||||
    {
 | 
			
		||||
        var data = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
 | 
			
		||||
@@ -97,6 +104,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("restartAllThread")]
 | 
			
		||||
    [DisplayName("重启全部线程")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task RestartAllThread()
 | 
			
		||||
    {
 | 
			
		||||
        await GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.IdChannels.Values).ConfigureAwait(false);
 | 
			
		||||
@@ -108,6 +116,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpPost("restartThread")]
 | 
			
		||||
    [DisplayName("重启设备线程")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task RestartDeviceThreadAsync(long deviceId)
 | 
			
		||||
    {
 | 
			
		||||
        if (GlobalData.IdDevices.TryGetValue(deviceId, out var deviceRuntime))
 | 
			
		||||
@@ -126,7 +135,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("writeVariables")]
 | 
			
		||||
    [DisplayName("写入变量")]
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, OperResult>>> WriteVariablesAsync([FromBody] Dictionary<string, Dictionary<string, string>> deviceDatas)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<Dictionary<string, Dictionary<string, OperResult>>> WriteVariablesAsync([FromBody][TouchSocket.WebApi.FromBody] Dictionary<string, Dictionary<string, string>> deviceDatas)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var deviceData in deviceDatas)
 | 
			
		||||
        {
 | 
			
		||||
@@ -145,7 +155,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("batchSaveChannel")]
 | 
			
		||||
    [DisplayName("保存通道")]
 | 
			
		||||
    public Task<bool> BatchSaveChannelAsync([FromBody] List<ChannelInput> channels, ItemChangedType type, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<ChannelInput> channels, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.BatchSaveChannelAsync(channels.AdaptListChannel(), type, restart);
 | 
			
		||||
    }
 | 
			
		||||
@@ -155,7 +166,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("batchSaveDevice")]
 | 
			
		||||
    [DisplayName("保存设备")]
 | 
			
		||||
    public Task<bool> BatchSaveDeviceAsync([FromBody] List<DeviceInput> devices, ItemChangedType type, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<DeviceInput> devices, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.BatchSaveDeviceAsync(devices.AdaptListDevice(), type, restart);
 | 
			
		||||
    }
 | 
			
		||||
@@ -165,7 +177,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("batchSaveVariable")]
 | 
			
		||||
    [DisplayName("保存变量")]
 | 
			
		||||
    public Task<bool> BatchSaveVariableAsync([FromBody] List<VariableInput> variables, ItemChangedType type, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> BatchSaveVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<VariableInput> variables, ItemChangedType type, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableRuntimeService.BatchSaveVariableAsync(variables.AdaptListVariable(), type, restart, default);
 | 
			
		||||
    }
 | 
			
		||||
@@ -175,7 +188,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("deleteChannel")]
 | 
			
		||||
    [DisplayName("删除通道")]
 | 
			
		||||
    public Task<bool> DeleteChannelAsync([FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> DeleteChannelAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdChannels.Keys.ToList();
 | 
			
		||||
        return GlobalData.ChannelRuntimeService.DeleteChannelAsync(ids, restart, default);
 | 
			
		||||
@@ -186,7 +200,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("deleteDevice")]
 | 
			
		||||
    [DisplayName("删除设备")]
 | 
			
		||||
    public Task<bool> DeleteDeviceAsync([FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> DeleteDeviceAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdDevices.Keys.ToList();
 | 
			
		||||
        return GlobalData.DeviceRuntimeService.DeleteDeviceAsync(ids, restart, default);
 | 
			
		||||
@@ -197,7 +212,8 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("deleteVariable")]
 | 
			
		||||
    [DisplayName("删除变量")]
 | 
			
		||||
    public Task<bool> DeleteVariableAsync([FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task<bool> DeleteVariableAsync([FromBody][TouchSocket.WebApi.FromBody] List<long> ids, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        if (ids == null || ids.Count == 0) ids = GlobalData.IdVariables.Keys.ToList();
 | 
			
		||||
        return GlobalData.VariableRuntimeService.DeleteVariableAsync(ids, restart, default);
 | 
			
		||||
@@ -208,6 +224,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpPost("insertTestData")]
 | 
			
		||||
    [DisplayName("增加测试数据")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public Task InsertTestDataAsync(int testVariableCount, int testDeviceCount, string slaveUrl, bool businessEnable, bool restart = true)
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.VariableRuntimeService.InsertTestDataAsync(testVariableCount, testDeviceCount, slaveUrl, businessEnable, restart, default);
 | 
			
		||||
@@ -220,6 +237,7 @@ public class ControlController : ControllerBase
 | 
			
		||||
    [HttpPost("checkRealAlarm")]
 | 
			
		||||
    [RequestAudit]
 | 
			
		||||
    [DisplayName("确认实时报警")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task CheckRealAlarm(long variableId)
 | 
			
		||||
    {
 | 
			
		||||
        if (GlobalData.ReadOnlyRealAlarmIdVariables.TryGetValue(variableId, out var variable))
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,8 @@ using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Rpc;
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -25,7 +27,9 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[ApiController]
 | 
			
		||||
[RolePermission]
 | 
			
		||||
[Authorize(AuthenticationSchemes = "Bearer")]
 | 
			
		||||
public class RuntimeInfoController : ControllerBase
 | 
			
		||||
[TouchSocket.WebApi.Router("/miniapi/[api]/[action]")]
 | 
			
		||||
[TouchSocket.WebApi.EnableCors("cors")]
 | 
			
		||||
public class RuntimeInfoController : ControllerBase, IRpcServer
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取冗余状态
 | 
			
		||||
@@ -33,6 +37,7 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("redundancyStatus")]
 | 
			
		||||
    [DisplayName("获取冗余状态")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
 | 
			
		||||
    public bool GetRedundancyStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return GlobalData.StartCollectChannelEnable;
 | 
			
		||||
@@ -44,7 +49,8 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("channelList")]
 | 
			
		||||
    [DisplayName("获取通道信息")]
 | 
			
		||||
    public async Task<SqlSugarPagedList<ChannelRuntime>> GetChannelListAsync([FromQuery] ChannelPageInput input)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<SqlSugarPagedList<ChannelRuntime>> GetChannelListAsync([FromQuery][TouchSocket.WebApi.FromBody] ChannelPageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var channelRuntimes = await GlobalData.GetCurrentUserChannels().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +68,8 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("deviceList")]
 | 
			
		||||
    [DisplayName("获取设备信息")]
 | 
			
		||||
    public async Task<SqlSugarPagedList<DeviceRuntime>> GetDeviceListAsync([FromQuery] DevicePageInput input)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<SqlSugarPagedList<DeviceRuntime>> GetDeviceListAsync([FromQuery][TouchSocket.WebApi.FromBody] DevicePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var deviceRuntimes = await GlobalData.GetCurrentUserDevices().ConfigureAwait(false);
 | 
			
		||||
        var data = deviceRuntimes
 | 
			
		||||
@@ -80,7 +87,8 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("realAlarmList")]
 | 
			
		||||
    [DisplayName("获取实时报警变量信息")]
 | 
			
		||||
    public async Task<SqlSugarPagedList<AlarmVariable>> GetRealAlarmList([FromQuery] AlarmVariablePageInput input)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<SqlSugarPagedList<AlarmVariable>> GetRealAlarmList([FromQuery][TouchSocket.WebApi.FromBody] AlarmVariablePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var realAlarmVariables = await GlobalData.GetCurrentUserRealAlarmVariables().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +106,8 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    [HttpGet("variableList")]
 | 
			
		||||
    [DisplayName("获取变量信息")]
 | 
			
		||||
    public async Task<SqlSugarPagedList<VariableRuntime>> GetVariableList([FromQuery] VariablePageInput input)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public async Task<SqlSugarPagedList<VariableRuntime>> GetVariableList([FromQuery][TouchSocket.WebApi.FromBody] VariablePageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        var variables = await GlobalData.GetCurrentUserIdVariables().ConfigureAwait(false);
 | 
			
		||||
        var data = variables
 | 
			
		||||
@@ -117,6 +126,7 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpGet("getPluginPropertys")]
 | 
			
		||||
    [DisplayName("获取默认插件属性")]
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Get)]
 | 
			
		||||
    public Dictionary<string, string> GetPluginPropertys(string pluginName)
 | 
			
		||||
    {
 | 
			
		||||
        var data = GlobalData.PluginService.GetDriverPropertyTypes(pluginName);
 | 
			
		||||
@@ -131,10 +141,11 @@ public class RuntimeInfoController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [HttpGet("getPluginInfos")]
 | 
			
		||||
    [DisplayName("获取插件")]
 | 
			
		||||
    public SqlSugarPagedList<PluginInfo> GetPluginInfos([FromQuery] PluginInfoPageInput input)
 | 
			
		||||
    [TouchSocket.WebApi.WebApi(Method = TouchSocket.WebApi.HttpMethodType.Post)]
 | 
			
		||||
    public SqlSugarPagedList<PluginInfo> GetPluginInfos([FromQuery][TouchSocket.WebApi.FromBody] PluginInfoPageInput input)
 | 
			
		||||
    {
 | 
			
		||||
        //指定关键词搜索为插件FullName
 | 
			
		||||
        return GlobalData.PluginService.GetList().WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
 | 
			
		||||
        return (GlobalData.PluginService.GetPluginListSync()).WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
 | 
			
		||||
                .ToPagedList(input);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,10 +59,10 @@ public abstract class BusinessBaseWithCacheAlarm : BusinessBaseWithCache
 | 
			
		||||
 | 
			
		||||
        await base.InitChannelAsync(channel, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    protected override Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
        return base.DisposeAsync(disposing);
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当报警值发生变化时触发此事件处理方法。该方法内部会检查是否需要进行报警上传,如果需要,则调用 <see cref="AlarmChange(AlarmVariable)"/> 方法。
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
            IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
            {
 | 
			
		||||
                if (((!_businessPropertyWithCacheInterval.OnlineFilter) || a.Value.IsOnline) && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                    VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
                    VariableValueInit(a.Value);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -130,7 +130,7 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 释放资源方法
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    protected override Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        // 解绑事件
 | 
			
		||||
        GlobalData.AlarmChangedEvent -= AlarmValueChange;
 | 
			
		||||
@@ -142,7 +142,7 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
        _memoryDevModelQueue.Clear();
 | 
			
		||||
        _memoryVarModelQueue.Clear();
 | 
			
		||||
        _memoryVarModelsQueue.Clear();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
        return base.DisposeAsync(disposing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -269,7 +269,7 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
                    IdVariableRuntimes.ForEach(a =>
 | 
			
		||||
                {
 | 
			
		||||
                    if (((!_businessPropertyWithCacheInterval.OnlineFilter) || a.Value.IsOnline) && _businessPropertyWithCacheInterval.BusinessUpdateEnum != BusinessUpdateEnum.Interval)
 | 
			
		||||
                        VariableValueChange(a.Value, a.Value.AdaptVariableBasicData());
 | 
			
		||||
                        VariableValueInit(a.Value);
 | 
			
		||||
                });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -316,4 +316,29 @@ public abstract class BusinessBaseWithCacheInterval : BusinessBaseWithCache
 | 
			
		||||
                VariableChange(variableRuntime, variable);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当初始化时触发此事件处理方法。该方法内部会检查是否需要进行变量上传,如果需要,则调用 <see cref="VariableChange(VariableRuntime, VariableBasicData)"/> 方法。
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="variableRuntime">变量运行时信息</param>
 | 
			
		||||
    protected void VariableValueInit(VariableRuntime variableRuntime)
 | 
			
		||||
    {
 | 
			
		||||
        if (CurrentDevice?.Pause != false)
 | 
			
		||||
            return;
 | 
			
		||||
        if (!VarModelEnable) return;
 | 
			
		||||
        if (TaskSchedulerLoop?.Stoped == true) return;
 | 
			
		||||
 | 
			
		||||
        // 如果业务属性的缓存为间隔上传,则不执行后续操作
 | 
			
		||||
        //if (_businessPropertyWithCacheInterval?.IsInterval != true)
 | 
			
		||||
        {
 | 
			
		||||
            // 检查当前设备的变量是否包含此变量,如果包含,则触发变量的变化处理方法
 | 
			
		||||
            if (IdVariableRuntimes.ContainsKey(variableRuntime.Id))
 | 
			
		||||
            {
 | 
			
		||||
                var data = variableRuntime.AdaptVariableBasicData();
 | 
			
		||||
                data.ValueInited = false;
 | 
			
		||||
                VariableChange(variableRuntime, data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ public class CacheDBItem<T> : IPrimaryIdEntity
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(IsPrimaryKey = true)]
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    [SugarColumn(IsJson = true, ColumnDataType = "TEXT")]
 | 
			
		||||
 
 | 
			
		||||
@@ -194,6 +194,8 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
 | 
			
		||||
        // 从插件服务中获取当前设备关联的驱动方法信息列表
 | 
			
		||||
        DriverMethodInfos = GlobalData.PluginService.GetDriverMethodInfos(device.PluginName, this);
 | 
			
		||||
 | 
			
		||||
        ReadWriteLock = new(CollectProperties.DutyCycle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public virtual string GetAddressDescription()
 | 
			
		||||
@@ -351,15 +353,13 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
 | 
			
		||||
    async Task ReadVariableSource(object? state, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (state is not VariableSourceRead variableSourceRead) return;
 | 
			
		||||
 | 
			
		||||
        if (Pause) return;
 | 
			
		||||
        if (cancellationToken.IsCancellationRequested) return;
 | 
			
		||||
 | 
			
		||||
        var readErrorCount = 0;
 | 
			
		||||
 | 
			
		||||
        var readToken = await ReadWriteLock.ReaderLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        if (readToken.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            await ReadVariableSource(state, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
@@ -373,6 +373,8 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
        //    LogMessage?.Trace(string.Format("{0} - Collecting [{1} - {2}]", DeviceName, variableSourceRead?.RegisterAddress, variableSourceRead?.Length));
 | 
			
		||||
        var readResult = await ReadSourceAsync(variableSourceRead, allToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
        var readErrorCount = 0;
 | 
			
		||||
 | 
			
		||||
        // 读取失败时重试一定次数
 | 
			
		||||
        while (!readResult.IsSuccess && readErrorCount < CollectProperties.RetryCount)
 | 
			
		||||
        {
 | 
			
		||||
@@ -474,7 +476,7 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected abstract Task<List<VariableSourceRead>> ProtectedLoadSourceReadAsync(List<VariableRuntime> deviceVariables);
 | 
			
		||||
 | 
			
		||||
    protected AsyncReadWriteLock ReadWriteLock = new();
 | 
			
		||||
    protected AsyncReadWriteLock ReadWriteLock;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 采集驱动读取,读取成功后直接赋值变量
 | 
			
		||||
@@ -565,7 +567,8 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
 | 
			
		||||
        ConcurrentDictionary<string, OperResult<object>> operResults = new();
 | 
			
		||||
 | 
			
		||||
        using var writeLock = ReadWriteLock.WriterLock();
 | 
			
		||||
 | 
			
		||||
        using var writeLock = await ReadWriteLock.WriterLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        var list = writeInfoLists
 | 
			
		||||
        .Where(a => !results.Any(b => b.Key == a.Key.Name))
 | 
			
		||||
        .ToDictionary(item => item.Key, item => item.Value).ToArray();
 | 
			
		||||
@@ -685,19 +688,8 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
            {
 | 
			
		||||
                // 调用方法并获取结果
 | 
			
		||||
                var data = await variableMethod.InvokeMethodAsync(this, value, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                result = new(data);
 | 
			
		||||
                var operResultType = typeof(IOperResult<>);
 | 
			
		||||
                var interfaceType = data.GetType().GetInterfaces()
 | 
			
		||||
                    .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == operResultType);
 | 
			
		||||
 | 
			
		||||
                if (interfaceType != null)
 | 
			
		||||
                {
 | 
			
		||||
                    var contentProperty = interfaceType.GetProperty("Content");
 | 
			
		||||
                    if (contentProperty != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Content = contentProperty.GetValue(data);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                result = data.GetOperResult();
 | 
			
		||||
 | 
			
		||||
                // 如果方法有返回值,并且是读取操作
 | 
			
		||||
                if (method.HasReturn && isRead)
 | 
			
		||||
@@ -728,5 +720,6 @@ public abstract class CollectBase : DriverBase, IRpcDriver
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    #endregion 写入方法
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,10 +47,11 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    protected override async Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        FoundationDevice?.Dispose();
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
        if (FoundationDevice != null)
 | 
			
		||||
            await FoundationDevice.SafeDisposeAsync().ConfigureAwait(false);
 | 
			
		||||
        await base.DisposeAsync(disposing).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 开始通讯执行的方法
 | 
			
		||||
@@ -175,7 +176,7 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    protected override async ValueTask<Dictionary<string, OperResult>> WriteValuesAsync(Dictionary<VariableRuntime, JToken> writeInfoLists, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        using var writeLock = ReadWriteLock.WriterLock();
 | 
			
		||||
        using var writeLock = await ReadWriteLock.WriterLockAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        // 检查协议是否为空,如果为空则抛出异常
 | 
			
		||||
        if (FoundationDevice == null)
 | 
			
		||||
            throw new NotSupportedException();
 | 
			
		||||
@@ -188,8 +189,6 @@ public abstract class CollectFoundationBase : CollectBase
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (LogMessage?.LogLevel <= TouchSocket.Core.LogLevel.Debug)
 | 
			
		||||
                    LogMessage?.Debug(string.Format("{0} - Writing [{1} - {2} - {3}]", DeviceName, writeInfo.Key.RegisterAddress, writeInfo.Value, writeInfo.Key.DataType));
 | 
			
		||||
 | 
			
		||||
                // 调用协议的写入方法,将写入信息中的数据写入到对应的寄存器地址,并获取操作结果
 | 
			
		||||
                var result = await FoundationDevice.WriteJTokenAsync(writeInfo.Key.RegisterAddress, writeInfo.Value, writeInfo.Key.DataType, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,12 @@ public abstract class CollectPropertyBase : DriverPropertyBase
 | 
			
		||||
    /// 失败重试次数,默认3
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual int RetryCount { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 读写占空比
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public virtual int DutyCycle { get; set; } = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -45,4 +51,9 @@ public abstract class CollectPropertyRetryBase : CollectPropertyBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [DynamicProperty]
 | 
			
		||||
    public override int RetryCount { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
    [DynamicProperty(Remark = "n 次写入操作会执行一次读取")]
 | 
			
		||||
    [MinValue(1)]
 | 
			
		||||
    public override int DutyCycle { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -26,11 +26,12 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 插件基类
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class DriverBase : DisposableObject, IDriver
 | 
			
		||||
public abstract class DriverBase : AsyncDisposableObject, IDriver
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc cref="DriverBase"/>
 | 
			
		||||
    public DriverBase()
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        Localizer = App.CreateLocalizerByType(typeof(DriverBase))!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -39,8 +40,7 @@ public abstract class DriverBase : DisposableObject, IDriver
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DeviceRuntime? CurrentDevice => WeakReferenceCurrentDevice?.TryGetTarget(out var target) == true ? target : null;
 | 
			
		||||
    private WeakReference<DeviceRuntime> WeakReferenceCurrentDevice { get; set; }
 | 
			
		||||
    public DeviceRuntime? CurrentDevice { get; private set; }
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前设备Id
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -208,7 +208,7 @@ public abstract class DriverBase : DisposableObject, IDriver
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal void InitDevice(DeviceRuntime device)
 | 
			
		||||
    {
 | 
			
		||||
        WeakReferenceCurrentDevice = new WeakReference<DeviceRuntime>(device);
 | 
			
		||||
        CurrentDevice = device;
 | 
			
		||||
 | 
			
		||||
        _logger = App.RootServices.GetService<Microsoft.Extensions.Logging.ILoggerFactory>().CreateLogger($"Driver[{CurrentDevice.Name}]");
 | 
			
		||||
 | 
			
		||||
@@ -313,38 +313,45 @@ public abstract class DriverBase : DisposableObject, IDriver
 | 
			
		||||
 | 
			
		||||
    protected abstract List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    protected object stopLock = new();
 | 
			
		||||
    protected WaitLock stopLock = new(nameof(DriverBase));
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 已停止任务,释放插件
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    internal virtual void Stop()
 | 
			
		||||
    internal virtual async Task StopAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (!DisposedValue)
 | 
			
		||||
        {
 | 
			
		||||
            lock (stopLock)
 | 
			
		||||
            await stopLock.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if (!DisposedValue)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        // 执行资源释放操作
 | 
			
		||||
                        Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 记录 Dispose 方法执行失败的错误信息
 | 
			
		||||
                        LogMessage?.LogError(ex, "Dispose");
 | 
			
		||||
                    }
 | 
			
		||||
                    // 记录设备线程已停止的信息
 | 
			
		||||
                    LogMessage?.LogInformation(string.Format(AppResource.DeviceTaskStop, DeviceName));
 | 
			
		||||
 | 
			
		||||
                    // 执行资源释放操作
 | 
			
		||||
                    await this.SafeDisposeAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // 记录 Dispose 方法执行失败的错误信息
 | 
			
		||||
                LogMessage?.LogError(ex, "Dispose");
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                stopLock.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override void Dispose(bool disposing)
 | 
			
		||||
    protected override async Task DisposeAsync(bool disposing)
 | 
			
		||||
    {
 | 
			
		||||
        base.Dispose(disposing);
 | 
			
		||||
        await base.DisposeAsync(disposing).ConfigureAwait(false);
 | 
			
		||||
        if (TaskSchedulerLoop != null)
 | 
			
		||||
        {
 | 
			
		||||
            lock (TaskSchedulerLoop)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application
 | 
			
		||||
{
 | 
			
		||||
    public interface IDriver : IDisposable
 | 
			
		||||
    public interface IDriver : IAsyncDisposable
 | 
			
		||||
    {
 | 
			
		||||
        bool DisposedValue { get; }
 | 
			
		||||
        ChannelRuntime CurrentChannel { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,10 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 后台日志表
 | 
			
		||||
///</summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("backend_log", TableDescription = "后台日志表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Log)]
 | 
			
		||||
#endif
 | 
			
		||||
public class BackendLog : PrimaryIdEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,16 @@ using TouchSocket.Core;
 | 
			
		||||
using TouchSocket.Sockets;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 通道表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("channel", TableDescription = "通道表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("unique_channel_name", nameof(Channel.Name), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IBaseEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -32,6 +35,7 @@ public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IB
 | 
			
		||||
    [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; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,16 @@ using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 设备表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("device", TableDescription = "设备表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("unique_device_name", nameof(Device.Name), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Device : BaseDataEntity, IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,10 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Rpc写入日志
 | 
			
		||||
///</summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("rpc_log", TableDescription = "RPC操作日志")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Log)]
 | 
			
		||||
#endif
 | 
			
		||||
public class RpcLog : PrimaryIdEntity
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,14 +16,17 @@ using System.Collections.Concurrent;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
#pragma warning disable CS0649
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 设备变量表
 | 
			
		||||
/// </summary>
 | 
			
		||||
#if !Management
 | 
			
		||||
[SugarTable("variable", TableDescription = "设备变量表")]
 | 
			
		||||
[Tenant(SqlSugarConst.DB_Custom)]
 | 
			
		||||
[SugarIndex("index_device", nameof(Variable.DeviceId), OrderByType.Asc)]
 | 
			
		||||
[SugarIndex("unique_deviceid_variable_name", nameof(Variable.Name), OrderByType.Asc, nameof(Variable.DeviceId), OrderByType.Asc, true)]
 | 
			
		||||
#endif
 | 
			
		||||
public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -31,6 +34,7 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "Id", IsPrimaryKey = true)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, IsVisibleWhenEdit = false, IsVisibleWhenAdd = false, Sortable = true, DefaultSort = true, DefaultSortOrder = SortOrder.Asc)]
 | 
			
		||||
    [System.ComponentModel.DataAnnotations.Key]
 | 
			
		||||
    public override long Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -46,6 +50,7 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    private long deviceId;
 | 
			
		||||
    private int? arrayLength;
 | 
			
		||||
    private int alarmDelay;
 | 
			
		||||
    private int alarmLevel;
 | 
			
		||||
    private ProtectTypeEnum protectType = ProtectTypeEnum.ReadWrite;
 | 
			
		||||
    private DataTypeEnum dataType = DataTypeEnum.Int16;
 | 
			
		||||
 | 
			
		||||
@@ -272,6 +277,15 @@ public class Variable : BaseDataEntity, IValidatableObject
 | 
			
		||||
    public Dictionary<long, Dictionary<string, string>>? VariablePropertys { get => variablePropertys; set => variablePropertys = value; }
 | 
			
		||||
 | 
			
		||||
    #region 报警
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警等级
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警等级")]
 | 
			
		||||
    [AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
 | 
			
		||||
    public int AlarmLevel { get => alarmLevel; set => alarmLevel = value; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警延时
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -39,4 +39,9 @@ public enum EventTypeEnum
 | 
			
		||||
    /// 准备恢复
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    PrepareFinish,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 报警确认并恢复
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ConfirmAndFinish,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@
 | 
			
		||||
// QQ群:605534569
 | 
			
		||||
// ------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection.Extensions;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
 | 
			
		||||
namespace Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
@@ -19,30 +18,19 @@ namespace Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
 | 
			
		||||
public static class ServiceCollectionHostedServiceExtensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Add an <see cref="IHostedService"/> registration for the given type.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="THostedService">An <see cref="IHostedService"/> to register.</typeparam>
 | 
			
		||||
    /// <param name="services">The <see cref="IServiceCollection"/> to register with.</param>
 | 
			
		||||
    /// <returns>The original <see cref="IServiceCollection"/>.</returns>
 | 
			
		||||
    public static IServiceCollection AddGatewayHostedService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
 | 
			
		||||
        where THostedService : class, IHostedService
 | 
			
		||||
    {
 | 
			
		||||
        services.AddSingleton<THostedService>();
 | 
			
		||||
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>(seriveProvider => seriveProvider.GetService<THostedService>()));
 | 
			
		||||
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Add an <see cref="IHostedService"/> registration for the given type.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static IServiceCollection AddGatewayHostedService<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
 | 
			
		||||
    where TService : class, IHostedService
 | 
			
		||||
    where TService : class
 | 
			
		||||
    where THostedService : class, IHostedService, TService
 | 
			
		||||
    {
 | 
			
		||||
        services.AddSingleton(typeof(TService), typeof(THostedService));
 | 
			
		||||
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, TService>(seriveProvider => seriveProvider.GetService<TService>()));
 | 
			
		||||
 | 
			
		||||
        services.AddSingleton<THostedService>();
 | 
			
		||||
        services.AddHostedService<THostedService>(a => a.GetService<THostedService>());
 | 
			
		||||
        services.AddSingleton<TService>(a => a.GetService<THostedService>());
 | 
			
		||||
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application
 | 
			
		||||
{
 | 
			
		||||
    internal 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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -27,7 +27,7 @@ public class LogJob : IJob
 | 
			
		||||
        await DeleteRpcLog(rpcLogDaysdaysAgo, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        await DeleteBackendLog(backendLogdaysAgo, stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        await DeleteTextLog(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        await DeleteLocalDB(stoppingToken).ConfigureAwait(false);
 | 
			
		||||
        DeleteLocalDB(stoppingToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task DeleteRpcLog(int daysAgo, CancellationToken stoppingToken)
 | 
			
		||||
@@ -49,8 +49,8 @@ public class LogJob : IJob
 | 
			
		||||
        //网关通道日志以通道id命名
 | 
			
		||||
        var channelService = App.RootServices.GetService<IChannelService>();
 | 
			
		||||
        var deviceService = App.RootServices.GetService<IDeviceService>();
 | 
			
		||||
        var channelNames = (await channelService.GetAllAsync().ConfigureAwait(false)).Select(a => a.Name.ToString()).ToHashSet();
 | 
			
		||||
        var deviceNames = (await deviceService.GetAllAsync().ConfigureAwait(false)).Select(a => a.Name.ToString()).ToHashSet();
 | 
			
		||||
        var channelNames = (GlobalData.Channels.Keys).ToHashSet();
 | 
			
		||||
        var deviceNames = (GlobalData.Devices.Keys).ToHashSet();
 | 
			
		||||
        var channelBaseDir = LoggerExtensions.GetChannelLogBasePath();
 | 
			
		||||
        Directory.CreateDirectory(channelBaseDir);
 | 
			
		||||
        var deviceBaseDir = LoggerExtensions.GetDeviceLogBasePath();
 | 
			
		||||
@@ -112,10 +112,9 @@ public class LogJob : IJob
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteLocalDB(CancellationToken stoppingToken)
 | 
			
		||||
    public void DeleteLocalDB(CancellationToken stoppingToken)
 | 
			
		||||
    {
 | 
			
		||||
        var deviceService = App.RootServices.GetService<IDeviceService>();
 | 
			
		||||
        var data = (await deviceService.GetAllAsync().ConfigureAwait(false)).Select(a => a.Name).ToHashSet();
 | 
			
		||||
        var data = (GlobalData.Devices.Keys).ToHashSet();
 | 
			
		||||
        var dir = CacheDBUtil.GetCacheFileBasePath();
 | 
			
		||||
        string[] dirs = Directory.GetDirectories(dir);
 | 
			
		||||
        foreach (var item in dirs)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Management.Application._Imports": {
 | 
			
		||||
    "Restart": "Restart",
 | 
			
		||||
    "Upgrade": "Upgrade"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.SaveUpdateZipFile": {
 | 
			
		||||
    "DownTemplate": "Download Template",
 | 
			
		||||
    "SaveUpdateZipFile": "Upload Version Package"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "AppName",
 | 
			
		||||
    "Architecture": "Architecture",
 | 
			
		||||
    "DotNetVersion": "DotNetVersion",
 | 
			
		||||
    "FilePath": "FilePath",
 | 
			
		||||
    "FileSize": "FileSize",
 | 
			
		||||
    "MinimumCompatibleVersion": "MinimumCompatibleVersion",
 | 
			
		||||
    "OSPlatform": "OSPlatform",
 | 
			
		||||
    "Version": "Version"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.DefaultDiagram": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.INode": {
 | 
			
		||||
 | 
			
		||||
    "Actuator": "Actuator",
 | 
			
		||||
    "AlarmChangedTriggerNode": "AlarmStateTrigger",
 | 
			
		||||
@@ -53,11 +71,7 @@
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Management.AutoUpdateController": {
 | 
			
		||||
    "AutoUpdateController": "AutoUpdate",
 | 
			
		||||
    "Update": "Update"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyHostedService": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyHostedService": {
 | 
			
		||||
    "ErrorSynchronizingData": "Synchronize data to standby site error",
 | 
			
		||||
    "RedundancyDisable": "Redundant gateway site not enabled",
 | 
			
		||||
    "RedundancyDup": "Redundant station settings duplicated",
 | 
			
		||||
@@ -67,10 +81,10 @@
 | 
			
		||||
    "SwitchNormalState": "Local machine (primary site) will switch to normal state",
 | 
			
		||||
    "SwitchSlaveState": "Master site has recovered, local machine (standby) will switch to standby state"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyOptions": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyOptions": {
 | 
			
		||||
    "Confirm": "Confirm switching to redundant state",
 | 
			
		||||
    "Enable": "Enable Dual-Machine Redundancy",
 | 
			
		||||
    "ForcedSync": "Forced Synchronous",
 | 
			
		||||
    "RedundancyForcedSync": "Forced Synchronous",
 | 
			
		||||
    "ForcedSyncWarning": "Forcing synchronization will generate database configuration information.Are you sure you want to continue?",
 | 
			
		||||
    "HeartbeatInterval": "Heartbeat Interval",
 | 
			
		||||
    "IsMaster": "IsMaster",
 | 
			
		||||
@@ -86,13 +100,13 @@
 | 
			
		||||
    "SyncInterval": "Data Synchronization Interval",
 | 
			
		||||
    "VerifyToken": "Verification Token"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyService": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyService": {
 | 
			
		||||
    "EditRedundancyOption": "EditRedundancyOption"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.UpdateZipFileHostedService": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.UpdateZipFileService": {
 | 
			
		||||
    "Update": "New version detected"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Upgrade.UpdateZipFile": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "AppName",
 | 
			
		||||
    "Architecture": "Architecture",
 | 
			
		||||
    "DotNetVersion": "DotNetVersion",
 | 
			
		||||
@@ -106,6 +120,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmVariable": {
 | 
			
		||||
    "AlarmCode": "AlarmCode",
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
    "AlarmLevel": "AlarmLevel",
 | 
			
		||||
    "AlarmEnable": "AlarmEnable",
 | 
			
		||||
    "AlarmLimit": "AlarmLimit",
 | 
			
		||||
    "AlarmText": "AlarmText",
 | 
			
		||||
@@ -295,7 +310,8 @@
 | 
			
		||||
    "RetryCount": "RetryCount"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "RetryCount": "RetryCount"
 | 
			
		||||
    "RetryCount": "RetryCount",
 | 
			
		||||
    "DutyCycle": "DutyCycle"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ControlController": {
 | 
			
		||||
    "BatchSaveChannelAsync": "BatchSaveChannel",
 | 
			
		||||
@@ -436,6 +452,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Variable": {
 | 
			
		||||
    "AddressOrOtherMethodNotNull": "Variable address or special method cannot be empty at the same time",
 | 
			
		||||
    "AlarmDelay": "AlarmDelay",
 | 
			
		||||
    "AlarmLevel": "AlarmLevel",
 | 
			
		||||
    "ArrayLength": "ArrayLength",
 | 
			
		||||
    "BoolCloseAlarmEnable": "BoolCloseAlarmEnable",
 | 
			
		||||
    "BoolCloseAlarmText": "BoolCloseAlarmText",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
  "ThingsGateway.Management.Application._Imports": {
 | 
			
		||||
    "Restart": "重启",
 | 
			
		||||
    "Upgrade": "更新"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.SaveUpdateZipFile": {
 | 
			
		||||
    "DownTemplate": "下载模板",
 | 
			
		||||
    "SaveUpdateZipFile": "上传版本包"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "名称",
 | 
			
		||||
    "Architecture": "架构",
 | 
			
		||||
    "DotNetVersion": ".net版本",
 | 
			
		||||
    "FilePath": "文件路径",
 | 
			
		||||
    "FileSize": "文件大小",
 | 
			
		||||
    "MinimumCompatibleVersion": "最小兼容版本",
 | 
			
		||||
    "OSPlatform": "系统版本",
 | 
			
		||||
    "Version": "版本"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.DefaultDiagram": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.INode": {
 | 
			
		||||
 | 
			
		||||
    "Actuator": "执行",
 | 
			
		||||
    "AlarmChangedTriggerNode": "报警状态触发器",
 | 
			
		||||
@@ -51,11 +69,8 @@
 | 
			
		||||
    "RulesId": "名称"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Management.AutoUpdateController": {
 | 
			
		||||
    "AutoUpdateController": "程序更新",
 | 
			
		||||
    "Update": "更新"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyHostedService": {
 | 
			
		||||
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyHostedService": {
 | 
			
		||||
    "ErrorSynchronizingData": "同步数据到从站错误",
 | 
			
		||||
    "RedundancyDisable": "不启用网关冗余站点",
 | 
			
		||||
    "RedundancyDup": "主备站设置重复",
 | 
			
		||||
@@ -65,10 +80,10 @@
 | 
			
		||||
    "SwitchNormalState": "本机(主站)将切换到正常状态",
 | 
			
		||||
    "SwitchSlaveState": "主站已恢复,本机(从站)将切换到备用状态"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyOptions": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyOptions": {
 | 
			
		||||
    "Confirm": "确认切换冗余状态",
 | 
			
		||||
    "Enable": "启用双机冗余",
 | 
			
		||||
    "ForcedSync": "强制同步",
 | 
			
		||||
    "RedundancyForcedSync": "强制同步",
 | 
			
		||||
    "ForcedSyncWarning": "强制同步会生成数据库配置信息,是否继续?",
 | 
			
		||||
    "HeartbeatInterval": "心跳间隔",
 | 
			
		||||
    "IsMaster": "是否为主站",
 | 
			
		||||
@@ -84,13 +99,13 @@
 | 
			
		||||
    "SyncInterval": "数据同步间隔",
 | 
			
		||||
    "VerifyToken": "Token"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.RedundancyService": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.RedundancyService": {
 | 
			
		||||
    "EditRedundancyOption": "修改网关冗余配置"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Management.UpdateZipFileHostedService": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.UpdateZipFileService": {
 | 
			
		||||
    "Update": "检测到新版本"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Upgrade.UpdateZipFile": {
 | 
			
		||||
  "ThingsGateway.Gateway.Application.UpdateZipFile": {
 | 
			
		||||
    "AppName": "名称",
 | 
			
		||||
    "Architecture": "架构",
 | 
			
		||||
    "DotNetVersion": ".net版本",
 | 
			
		||||
@@ -105,6 +120,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.AlarmVariable": {
 | 
			
		||||
    "AlarmCode": "报警值",
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "AlarmLevel": "报警等级",
 | 
			
		||||
    "AlarmEnable": "报警使能",
 | 
			
		||||
    "AlarmLimit": "报警限值",
 | 
			
		||||
    "AlarmText": "报警文本",
 | 
			
		||||
@@ -294,7 +310,8 @@
 | 
			
		||||
    "RetryCount": "失败重试次数"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.CollectPropertyRetryBase": {
 | 
			
		||||
    "RetryCount": "失败重试次数"
 | 
			
		||||
    "RetryCount": "失败重试次数",
 | 
			
		||||
    "DutyCycle": "占空比"
 | 
			
		||||
  },
 | 
			
		||||
  "ThingsGateway.Gateway.Application.ControlController": {
 | 
			
		||||
    "BatchSaveChannelAsync": "保存通道",
 | 
			
		||||
@@ -437,6 +454,7 @@
 | 
			
		||||
  "ThingsGateway.Gateway.Application.Variable": {
 | 
			
		||||
    "AddressOrOtherMethodNotNull": " 变量地址或特殊方法不能同时为空 ",
 | 
			
		||||
    "AlarmDelay": "报警延时",
 | 
			
		||||
    "AlarmLevel": "报警等级",
 | 
			
		||||
    "ArrayLength": "数组长度",
 | 
			
		||||
    "BoolCloseAlarmEnable": "布尔关报警使能",
 | 
			
		||||
    "BoolCloseAlarmText": "布尔关报警文本",
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@
 | 
			
		||||
 | 
			
		||||
using Riok.Mapperly.Abstractions;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Management;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
 | 
			
		||||
public static partial class GatewayMapper
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,11 @@ public class AlarmVariable : PrimaryIdEntity, IDBHistoryAlarm
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public DataTypeEnum DataType { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc  cref="Variable.AlarmLevel"/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警等级", IsNullable = false)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
    public int AlarmLevel { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc  cref="VariableRuntime.AlarmCode"/>
 | 
			
		||||
    [SugarColumn(ColumnDescription = "报警值", IsNullable = false)]
 | 
			
		||||
    [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
 | 
			
		||||
    public void Init()
 | 
			
		||||
    {
 | 
			
		||||
        // 通过插件名称获取插件信息
 | 
			
		||||
        PluginInfo = GlobalData.PluginService.GetList().FirstOrDefault(A => A.FullName == PluginName);
 | 
			
		||||
        PluginInfo = GlobalData.PluginService.GetPluginListSync().FirstOrDefault(A => A.FullName == PluginName);
 | 
			
		||||
 | 
			
		||||
        GlobalData.IdChannels.TryRemove(Id, out _);
 | 
			
		||||
        GlobalData.Channels.TryRemove(Name, out _);
 | 
			
		||||
 
 | 
			
		||||
@@ -188,4 +188,7 @@ public class VariableBasicData
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)]
 | 
			
		||||
    public long CreateOrgId { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <inheritdoc cref="VariableRuntime.ValueInited"/>
 | 
			
		||||
    public bool ValueInited { get; set; }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ public class PluginInfo
 | 
			
		||||
    /// 插件文件名称.插件类型名称
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public string FullName => PluginServiceUtil.GetFullName(FileName, Name);
 | 
			
		||||
    public string FullName => PluginInfoUtil.GetFullName(FileName, Name);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 插件文件名称
 | 
			
		||||
@@ -70,8 +70,5 @@ public class PluginInfo
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [IgnoreExcel]
 | 
			
		||||
    [SugarColumn(IsIgnore = true)]
 | 
			
		||||
    [System.Text.Json.Serialization.JsonIgnore]
 | 
			
		||||
    [Newtonsoft.Json.JsonIgnore]
 | 
			
		||||
    [AutoGenerateColumn(Ignore = true)]
 | 
			
		||||
    public string Directory { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ public partial class VariableRuntime : Variable, IVariable, IDisposable
 | 
			
		||||
 | 
			
		||||
    private bool _isOnline;
 | 
			
		||||
    private bool _isOnlineChanged;
 | 
			
		||||
    private bool _valueInited;
 | 
			
		||||
 | 
			
		||||
    private string alarmLimit;
 | 
			
		||||
    private string alarmText;
 | 
			
		||||
@@ -45,6 +46,8 @@ public partial class VariableRuntime : Variable, IVariable, IDisposable
 | 
			
		||||
    private object _value;
 | 
			
		||||
    private object lastSetValue;
 | 
			
		||||
    private object rawValue;
 | 
			
		||||
    internal object AlarmLockObject = new();
 | 
			
		||||
    internal bool AlarmConfirm;
 | 
			
		||||
    private DeviceRuntime? deviceRuntime;
 | 
			
		||||
    private IVariableSource? variableSource;
 | 
			
		||||
    private VariableMethod? variableMethod;
 | 
			
		||||
@@ -165,6 +168,8 @@ public partial class VariableRuntime : Variable, IVariable, IDisposable
 | 
			
		||||
 | 
			
		||||
            LastSetValue = _value;
 | 
			
		||||
 | 
			
		||||
            ValueInited = true;
 | 
			
		||||
 | 
			
		||||
            if (_isOnline == true)
 | 
			
		||||
            {
 | 
			
		||||
                _value = data;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,8 @@ namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
public partial class VariableRuntime : Variable, IVariable, IDisposable
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    [AutoGenerateColumn(Visible = false)]
 | 
			
		||||
    public bool ValueInited { get => _valueInited; set => _valueInited = value; }
 | 
			
		||||
 | 
			
		||||
    #region 属性
 | 
			
		||||
    /// <summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Management;
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 系统配置种子数据
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,10 @@ using Microsoft.Extensions.Logging;
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.Common.Extension;
 | 
			
		||||
using ThingsGateway.Gateway.Application.Extensions;
 | 
			
		||||
using ThingsGateway.NewLife;
 | 
			
		||||
using ThingsGateway.NewLife.Extension;
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Core;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
@@ -44,7 +45,7 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        StopTask();
 | 
			
		||||
        scheduledTask?.TryDispose();
 | 
			
		||||
        scheduledTask?.SafeDispose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #region 核心实现
 | 
			
		||||
@@ -217,7 +218,7 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
        if (alarmEnum == null)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果仍未获取到报警类型,则触发需恢复报警事件(如果存在)
 | 
			
		||||
            AlarmChange(item, null, text, EventTypeEnum.Finish, alarmEnum, delay);
 | 
			
		||||
            AlarmChange(item, null, text, true, alarmEnum, delay);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
@@ -232,14 +233,14 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
                    if (result)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 如果表达式结果为true,则触发报警事件
 | 
			
		||||
                        AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
                        AlarmChange(item, limit, text, false, alarmEnum, delay);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果不存在报警约束表达式,则直接触发报警事件
 | 
			
		||||
                AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
 | 
			
		||||
                AlarmChange(item, limit, text, false, alarmEnum, delay);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -250,193 +251,227 @@ internal sealed class AlarmTask : IDisposable
 | 
			
		||||
    /// <param name="item">要处理的变量</param>
 | 
			
		||||
    /// <param name="limit">报警限制值</param>
 | 
			
		||||
    /// <param name="text">报警文本</param>
 | 
			
		||||
    /// <param name="eventEnum">报警事件类型枚举</param>
 | 
			
		||||
    /// <param name="finish">是否恢复</param>
 | 
			
		||||
    /// <param name="alarmEnum">报警类型枚举</param>
 | 
			
		||||
    /// <param name="delay">报警延时</param>
 | 
			
		||||
    private static void AlarmChange(VariableRuntime item, object limit, string text, EventTypeEnum eventEnum, AlarmTypeEnum? alarmEnum, int delay)
 | 
			
		||||
    private static void AlarmChange(VariableRuntime item, object limit, string text, bool finish, AlarmTypeEnum? alarmEnum, int delay)
 | 
			
		||||
    {
 | 
			
		||||
        bool changed = false;
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        lock (item.AlarmLockObject)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是需恢复报警事件
 | 
			
		||||
            // 如果实时报警列表中不存在该变量,则直接返回
 | 
			
		||||
            if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
 | 
			
		||||
            bool changed = false;
 | 
			
		||||
            if (finish)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            // 如果是触发报警事件
 | 
			
		||||
            // 在实时报警列表中查找该变量
 | 
			
		||||
            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
 | 
			
		||||
            {
 | 
			
		||||
                // 如果变量已经处于相同的报警类型,则直接返回
 | 
			
		||||
                if (item.AlarmType == alarmEnum)
 | 
			
		||||
                // 如果是需恢复报警事件
 | 
			
		||||
                // 如果实时报警列表中不存在该变量,则直接返回
 | 
			
		||||
                if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
 | 
			
		||||
                {
 | 
			
		||||
                    return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 更新变量的报警信息和事件时间
 | 
			
		||||
        if (eventEnum == EventTypeEnum.Alarm)
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.Now;
 | 
			
		||||
            //添加报警延时策略
 | 
			
		||||
            if (delay > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.PrepareAlarm)
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.PrepareAlarm;//准备报警
 | 
			
		||||
                    item.PrepareAlarmEventTime = now;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (item.EventType == EventTypeEnum.PrepareAlarm)
 | 
			
		||||
                    {
 | 
			
		||||
                        if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareAlarmEventTime = null;
 | 
			
		||||
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
 | 
			
		||||
                    {
 | 
			
		||||
                        //报警类型改变,重新计时
 | 
			
		||||
                        if (item.PrepareAlarmEventTime == null)
 | 
			
		||||
                            item.PrepareAlarmEventTime = now;
 | 
			
		||||
                        if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                        {
 | 
			
		||||
                            //超过延时时间,触发报警
 | 
			
		||||
                            item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                            item.AlarmTime = now;
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                            item.AlarmType = alarmEnum;
 | 
			
		||||
                            item.AlarmLimit = limit.ToString();
 | 
			
		||||
                            item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                            item.RecoveryCode = string.Empty;
 | 
			
		||||
                            item.AlarmText = text;
 | 
			
		||||
                            item.PrepareAlarmEventTime = null;
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (item.EventType != EventTypeEnum.Confirm)
 | 
			
		||||
                    item.AlarmConfirm = false;
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                item.EventType = eventEnum;
 | 
			
		||||
                item.AlarmTime = now;
 | 
			
		||||
                item.EventTime = now;
 | 
			
		||||
                item.AlarmType = alarmEnum;
 | 
			
		||||
                item.AlarmLimit = limit.ToString();
 | 
			
		||||
                item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                item.RecoveryCode = string.Empty;
 | 
			
		||||
                item.AlarmText = text;
 | 
			
		||||
                item.PrepareAlarmEventTime = null;
 | 
			
		||||
                changed = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (eventEnum == EventTypeEnum.Finish)
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.Now;
 | 
			
		||||
            //添加报警延时策略
 | 
			
		||||
            if (delay > 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (item.EventType != EventTypeEnum.Finish && item.EventType != EventTypeEnum.PrepareFinish)
 | 
			
		||||
                // 在实时报警列表中查找该变量
 | 
			
		||||
                if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.PrepareFinish;//准备报警
 | 
			
		||||
                    item.PrepareFinishEventTime = now;
 | 
			
		||||
                    // 如果变量已经处于相同的报警类型,则直接返回
 | 
			
		||||
                    if (item.AlarmType == alarmEnum)
 | 
			
		||||
                        return;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 更新变量的报警信息和事件时间
 | 
			
		||||
            if (!finish)
 | 
			
		||||
            {
 | 
			
		||||
                var now = DateTime.Now;
 | 
			
		||||
                //添加报警延时策略
 | 
			
		||||
                if (delay > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    if (item.EventType == EventTypeEnum.PrepareFinish)
 | 
			
		||||
                    if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.PrepareAlarm)
 | 
			
		||||
                    {
 | 
			
		||||
                        if ((now - item.PrepareFinishEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                        item.EventType = EventTypeEnum.PrepareAlarm;//准备报警
 | 
			
		||||
                        item.PrepareAlarmEventTime = now;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        if (item.EventType == EventTypeEnum.PrepareAlarm)
 | 
			
		||||
                        {
 | 
			
		||||
                            if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
                            if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                            {
 | 
			
		||||
                                item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                                item.EventType = eventEnum;
 | 
			
		||||
                                item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                                item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                                item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                                item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                                item.EventTime = DateTime.Now;
 | 
			
		||||
                                item.PrepareFinishEventTime = null;
 | 
			
		||||
                                //超过延时时间,触发报警
 | 
			
		||||
                                item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                                item.AlarmTime = now;
 | 
			
		||||
                                item.EventTime = now;
 | 
			
		||||
                                item.AlarmType = alarmEnum;
 | 
			
		||||
                                item.AlarmLimit = limit.ToString();
 | 
			
		||||
                                item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                                item.RecoveryCode = string.Empty;
 | 
			
		||||
                                item.AlarmText = text;
 | 
			
		||||
                                item.PrepareAlarmEventTime = null;
 | 
			
		||||
 | 
			
		||||
                                changed = true;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
 | 
			
		||||
                        {
 | 
			
		||||
                            //报警类型改变,重新计时
 | 
			
		||||
                            if (item.PrepareAlarmEventTime == null)
 | 
			
		||||
                                item.PrepareAlarmEventTime = now;
 | 
			
		||||
                            if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                            {
 | 
			
		||||
                                //超过延时时间,触发报警
 | 
			
		||||
                                item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                                item.AlarmTime = now;
 | 
			
		||||
                                item.EventTime = now;
 | 
			
		||||
                                item.AlarmType = alarmEnum;
 | 
			
		||||
                                item.AlarmLimit = limit.ToString();
 | 
			
		||||
                                item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                                item.RecoveryCode = string.Empty;
 | 
			
		||||
                                item.AlarmText = text;
 | 
			
		||||
                                item.PrepareAlarmEventTime = null;
 | 
			
		||||
                                changed = true;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果是触发报警事件
 | 
			
		||||
                    item.EventType = EventTypeEnum.Alarm;
 | 
			
		||||
                    item.AlarmTime = now;
 | 
			
		||||
                    item.EventTime = now;
 | 
			
		||||
                    item.AlarmType = alarmEnum;
 | 
			
		||||
                    item.AlarmLimit = limit.ToString();
 | 
			
		||||
                    item.AlarmCode = item.Value.ToString();
 | 
			
		||||
                    item.RecoveryCode = string.Empty;
 | 
			
		||||
                    item.AlarmText = text;
 | 
			
		||||
                    item.PrepareAlarmEventTime = null;
 | 
			
		||||
                    changed = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是需恢复报警事件
 | 
			
		||||
                // 获取旧的报警信息
 | 
			
		||||
                if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
                var now = DateTime.Now;
 | 
			
		||||
                //添加报警延时策略
 | 
			
		||||
                if (delay > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                    item.EventType = eventEnum;
 | 
			
		||||
                    item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                    item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                    item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                    item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                    item.EventTime = DateTime.Now;
 | 
			
		||||
                    item.PrepareFinishEventTime = null;
 | 
			
		||||
                    changed = true;
 | 
			
		||||
                    if (item.EventType != EventTypeEnum.Finish && item.EventType != EventTypeEnum.PrepareFinish)
 | 
			
		||||
                    {
 | 
			
		||||
                        item.EventType = EventTypeEnum.PrepareFinish;
 | 
			
		||||
                        item.PrepareFinishEventTime = now;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        if (item.EventType == EventTypeEnum.PrepareFinish)
 | 
			
		||||
                        {
 | 
			
		||||
                            if ((now - item.PrepareFinishEventTime!.Value).TotalMilliseconds > delay)
 | 
			
		||||
                            {
 | 
			
		||||
                                if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
                                {
 | 
			
		||||
                                    item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                                    item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                                    item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                                    item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                                    item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                                    if (item.EventType != EventTypeEnum.Finish)
 | 
			
		||||
                                    {
 | 
			
		||||
                                        item.EventTime = now;
 | 
			
		||||
                                    }
 | 
			
		||||
                                    item.EventType = EventTypeEnum.Finish;
 | 
			
		||||
                                    item.PrepareFinishEventTime = null;
 | 
			
		||||
                                    changed = true;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    // 如果是需恢复报警事件
 | 
			
		||||
                    // 获取旧的报警信息
 | 
			
		||||
                    if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
 | 
			
		||||
                    {
 | 
			
		||||
                        item.AlarmType = oldAlarm.AlarmType;
 | 
			
		||||
                        item.AlarmLimit = oldAlarm.AlarmLimit;
 | 
			
		||||
                        item.AlarmCode = oldAlarm.AlarmCode;
 | 
			
		||||
                        item.RecoveryCode = item.Value.ToString();
 | 
			
		||||
                        item.AlarmText = oldAlarm.AlarmText;
 | 
			
		||||
                        if (item.EventType != EventTypeEnum.Finish)
 | 
			
		||||
                        {
 | 
			
		||||
                            item.EventTime = now;
 | 
			
		||||
                        }
 | 
			
		||||
                        item.EventType = EventTypeEnum.Finish;
 | 
			
		||||
                        item.PrepareFinishEventTime = null;
 | 
			
		||||
                        changed = true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 触发报警变化事件
 | 
			
		||||
        if (changed)
 | 
			
		||||
        {
 | 
			
		||||
            if (item.EventType == EventTypeEnum.Alarm)
 | 
			
		||||
            // 触发报警变化事件
 | 
			
		||||
            if (changed)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是触发报警事件
 | 
			
		||||
                //lock (GlobalData. RealAlarmVariables)
 | 
			
		||||
                if (item.EventType == EventTypeEnum.Alarm)
 | 
			
		||||
                {
 | 
			
		||||
                    // 从实时报警列表中移除旧的报警信息,并添加新的报警信息
 | 
			
		||||
                    GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                    // 如果是触发报警事件
 | 
			
		||||
                    //lock (GlobalData. RealAlarmVariables)
 | 
			
		||||
                    {
 | 
			
		||||
                        // 从实时报警列表中移除旧的报警信息,并添加新的报警信息
 | 
			
		||||
                        GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (item.EventType == EventTypeEnum.Finish)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    // 如果是需恢复报警事件,则从实时报警列表中移除该变量
 | 
			
		||||
                    if (item.AlarmConfirm)
 | 
			
		||||
                    {
 | 
			
		||||
                        GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
 | 
			
		||||
                        item.EventType = EventTypeEnum.ConfirmAndFinish;
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                GlobalData.AlarmChange(item.AdaptAlarmVariable());
 | 
			
		||||
            }
 | 
			
		||||
            else if (item.EventType == EventTypeEnum.Finish)
 | 
			
		||||
            {
 | 
			
		||||
                // 如果是需恢复报警事件,则从实时报警列表中移除该变量
 | 
			
		||||
                GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
 | 
			
		||||
                //GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
            }
 | 
			
		||||
            GlobalData.AlarmChange(item.AdaptAlarmVariable());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void ConfirmAlarm(long variableId)
 | 
			
		||||
    {
 | 
			
		||||
        // 如果是确认报警事件
 | 
			
		||||
        if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var variableRuntime))
 | 
			
		||||
        if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var item))
 | 
			
		||||
        {
 | 
			
		||||
            variableRuntime.EventType = EventTypeEnum.Confirm;
 | 
			
		||||
            variableRuntime.EventTime = DateTime.Now;
 | 
			
		||||
            GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => variableRuntime.AdaptAlarmVariable(), (a, b) => variableRuntime.AdaptAlarmVariable());
 | 
			
		||||
            GlobalData.AlarmChange(variableRuntime.AdaptAlarmVariable());
 | 
			
		||||
            lock (item.AlarmLockObject)
 | 
			
		||||
            {
 | 
			
		||||
                item.AlarmConfirm = true;
 | 
			
		||||
                item.EventTime = DateTime.Now;
 | 
			
		||||
 | 
			
		||||
                if (item.EventType == EventTypeEnum.Finish)
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.ConfirmAndFinish;
 | 
			
		||||
                    GlobalData.RealAlarmIdVariables.TryRemove(variableId, out _);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    item.EventType = EventTypeEnum.Confirm;
 | 
			
		||||
                    GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                GlobalData.AlarmChange(item.AdaptAlarmVariable());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,9 @@
 | 
			
		||||
// QQ群:605534569
 | 
			
		||||
// ------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
public interface IAlarmHostedService : IHostedService
 | 
			
		||||
public interface IAlarmHostedService
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 确认报警
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
//  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
 | 
			
		||||
//  此代码版权(除特别声明外的代码)归作者本人Diego所有
 | 
			
		||||
//  源代码使用协议遵循本仓库的开源协议及附加协议
 | 
			
		||||
//  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
 | 
			
		||||
//  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
 | 
			
		||||
//  使用文档:https://thingsgateway.cn/
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
using TouchSocket.Dmtp.Rpc;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Gateway.Application
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    public interface IRealAlarmService
 | 
			
		||||
    {
 | 
			
		||||
        [DmtpRpc]
 | 
			
		||||
        Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,20 +8,16 @@
 | 
			
		||||
//  QQ群:605534569
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
using ThingsGateway.DB;
 | 
			
		||||
 | 
			
		||||
namespace ThingsGateway.Upgrade;
 | 
			
		||||
namespace ThingsGateway.Gateway.Application;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 关系表种子数据
 | 
			
		||||
/// 设备采集报警后台服务
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class SysRelationSeedData : ISqlSugarEntitySeedData<SysRelation>
 | 
			
		||||
internal sealed class RealAlarmService : IRealAlarmService
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc/>
 | 
			
		||||
    public IEnumerable<SysRelation> SeedData()
 | 
			
		||||
    public Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables()
 | 
			
		||||
    {
 | 
			
		||||
        var data = SeedDataUtil.GetSeedData<SysRelation>(PathExtensions.CombinePathWithOs("SeedData", "Upgrade", "seed_upgrade_relation.json"));
 | 
			
		||||
        var assembly = GetType().Assembly;
 | 
			
		||||
        return SeedDataUtil.GetSeedDataByJson<SysRelation>(SeedDataUtil.GetManifestResourceStream(assembly, "SeedData.Upgrade.seed_upgrade_relation.json")).Concat(data);
 | 
			
		||||
        return GlobalData.GetCurrentUserRealAlarmVariables();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user