NewLife/NewLife.Remoting

完成第二个例程,节点管理
大石头 authored at 2024-06-30 00:30:55
3c7f43b
Tree
1 Parent(s) 917dfa7
Summary: 24 changed files with 284 additions and 415 deletions.
Modified +7 -2
Modified +4 -0
Modified +9 -0
Modified +2 -2
Modified +8 -16
Modified +4 -4
Modified +4 -4
Modified +1 -1
Modified +1 -1
Modified +3 -3
Renamed +36 -40
Samples/ZeroServer/Clients/HttpDevice.cs → Samples/ZeroServer/Clients/NodeClient.cs
Modified +37 -4
Modified +21 -21
Modified +5 -5
Modified +4 -15
Modified +24 -23
Modified +33 -47
Modified +1 -1
Modified +1 -1
Modified +2 -4
Modified +1 -1
Modified +76 -150
Deleted +0 -69
Samples/ZeroServer/Services/QueueService.cs
Modified +0 -1
Modified +7 -2
diff --git a/NewLife.Remoting.Extensions/Controllers/BaseDeviceController.cs b/NewLife.Remoting.Extensions/Controllers/BaseDeviceController.cs
index 98ed9d2..2a14817 100644
--- a/NewLife.Remoting.Extensions/Controllers/BaseDeviceController.cs
+++ b/NewLife.Remoting.Extensions/Controllers/BaseDeviceController.cs
@@ -182,13 +182,18 @@ public class BaseDeviceController : BaseController
         {
             using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
 
-            await Handle(socket, Token);
+            await HandleNotify(socket, Token);
         }
         else
             HttpContext.Response.StatusCode = 400;
     }
 
-    private async Task Handle(WebSocket socket, String token)
+    /// <summary>处理长连接</summary>
+    /// <param name="socket"></param>
+    /// <param name="token"></param>
+    /// <returns></returns>
+    /// <exception cref="InvalidOperationException"></exception>
+    protected virtual async Task HandleNotify(WebSocket socket, String token)
     {
         var device = _device ?? throw new InvalidOperationException("未登录!");
 
Modified +4 -0
diff --git a/NewLife.Remoting/Clients/ClientBase.cs b/NewLife.Remoting/Clients/ClientBase.cs
index faa215b..fbda299 100644
--- a/NewLife.Remoting/Clients/ClientBase.cs
+++ b/NewLife.Remoting/Clients/ClientBase.cs
@@ -477,6 +477,10 @@ public abstract class ClientBase : DisposeBase, IApiClient, ICommandClient, IEve
                 info.Compile = asm.Compile.ToUniversalTime().ToLong();
             }
 
+            info.IP = NetHelper.GetIPsWithCache().Where(e => e.IsIPv4() && e.GetAddressBytes()[0] != 169).Join();
+            info.Macs = NetHelper.GetMacs().Select(e => e.ToHex("-")).Where(e => e != "00-00-00-00-00-00").OrderBy(e => e).Join(",");
+            info.UUID = MachineInfo.GetCurrent().BuildCode();
+
             info.Time = DateTime.UtcNow.ToLong();
         }
     }
Modified +9 -0
diff --git a/NewLife.Remoting/Models/LoginRequest.cs b/NewLife.Remoting/Models/LoginRequest.cs
index c0eecf8..dcf4a1e 100644
--- a/NewLife.Remoting/Models/LoginRequest.cs
+++ b/NewLife.Remoting/Models/LoginRequest.cs
@@ -38,6 +38,15 @@ public class LoginRequest : ILoginRequest
     /// <summary>编译时间。UTC毫秒</summary>
     public Int64 Compile { get; set; }
 
+    /// <summary>本地IP地址</summary>
+    public String? IP { get; set; }
+
+    /// <summary>MAC地址</summary>
+    public String? Macs { get; set; }
+
+    /// <summary>唯一标识</summary>
+    public String? UUID { get; set; }
+
     /// <summary>本地时间。UTC毫秒</summary>
     public Int64 Time { get; set; }
     #endregion
Modified +2 -2
diff --git a/NewLife.Remoting/Models/PingRequest.cs b/NewLife.Remoting/Models/PingRequest.cs
index 17a8e94..9f2b6d7 100644
--- a/NewLife.Remoting/Models/PingRequest.cs
+++ b/NewLife.Remoting/Models/PingRequest.cs
@@ -38,8 +38,8 @@ public class PingRequest : IPingRequest
     /// <summary>电量</summary>
     public Double Battery { get; set; }
 
-    ///// <summary>信号强度。WiFi/4G</summary>
-    //public Int32 Signal { get; set; }
+    /// <summary>信号强度。WiFi/4G</summary>
+    public Int32 Signal { get; set; }
 
     /// <summary>上行速度。网络发送速度,字节每秒</summary>
     public UInt64 UplinkSpeed { get; set; }
Modified +8 -16
diff --git a/Samples/IoTZero/Clients/HttpDevice.cs b/Samples/IoTZero/Clients/HttpDevice.cs
index 6864337..37ba972 100644
--- a/Samples/IoTZero/Clients/HttpDevice.cs
+++ b/Samples/IoTZero/Clients/HttpDevice.cs
@@ -1,7 +1,6 @@
 using NewLife;
 using NewLife.IoT.Models;
 using NewLife.IoT.ThingModels;
-using NewLife.Log;
 using NewLife.Model;
 using NewLife.Remoting;
 using NewLife.Remoting.Clients;
@@ -64,15 +63,12 @@ public class HttpDevice : ClientBase
     #region 登录
     public override ILoginRequest BuildLoginRequest()
     {
-        var request = base.BuildLoginRequest();
-        if (request is LoginInfo info)
-        {
-            info.ProductKey = ProductKey;
-            info.ProductSecret = ProductSecret;
-            info.Name = Environment.MachineName;
-            info.IP = NetHelper.MyIP() + "";
-            info.UUID = MachineInfo.GetCurrent().BuildCode();
-        }
+        var request = new LoginInfo();
+        FillLoginRequest(request);
+
+        request.ProductKey = ProductKey;
+        request.ProductSecret = ProductSecret;
+        request.Name = Environment.MachineName;
 
         return request;
     }
@@ -81,12 +77,8 @@ public class HttpDevice : ClientBase
     #region 心跳
     public override IPingRequest BuildPingRequest()
     {
-        var request = base.BuildPingRequest();
-        if (request is PingInfo info)
-        {
-            info.Memory = 0;
-            info.TotalSize = 0;
-        }
+        var request = new PingInfo();
+        FillPingRequest(request);
 
         return request;
     }
Modified +4 -4
diff --git a/Samples/IoTZero/Models/LoginInfo.cs b/Samples/IoTZero/Models/LoginInfo.cs
index f42973a..26b25cb 100644
--- a/Samples/IoTZero/Models/LoginInfo.cs
+++ b/Samples/IoTZero/Models/LoginInfo.cs
@@ -27,11 +27,11 @@ public class LoginInfo : LoginRequest
     ///// <summary>版本</summary>
     //public String Version { get; set; }
 
-    /// <summary>本地IP地址</summary>
-    public String IP { get; set; }
+    ///// <summary>本地IP地址</summary>
+    //public String IP { get; set; }
 
-    /// <summary>唯一标识</summary>
-    public String UUID { get; set; }
+    ///// <summary>唯一标识</summary>
+    //public String UUID { get; set; }
 
     ///// <summary>本地UTC时间</summary>
     //public Int64 Time { get; set; }
Modified +4 -4
diff --git a/Samples/IoTZero/Models/PingInfo.cs b/Samples/IoTZero/Models/PingInfo.cs
index 04ebe73..9b2c1a8 100644
--- a/Samples/IoTZero/Models/PingInfo.cs
+++ b/Samples/IoTZero/Models/PingInfo.cs
@@ -6,14 +6,14 @@ namespace NewLife.IoT.Models;
 public class PingInfo : PingRequest
 {
     #region 属性
-    /// <summary>内存大小</summary>
-    public UInt64 Memory { get; set; }
+    ///// <summary>内存大小</summary>
+    //public UInt64 Memory { get; set; }
 
     ///// <summary>可用内存大小</summary>
     //public UInt64 AvailableMemory { get; set; }
 
-    /// <summary>磁盘大小。应用所在盘</summary>
-    public UInt64 TotalSize { get; set; }
+    ///// <summary>磁盘大小。应用所在盘</summary>
+    //public UInt64 TotalSize { get; set; }
 
     ///// <summary>磁盘可用空间。应用所在盘</summary>
     //public UInt64 AvailableFreeSpace { get; set; }
Modified +1 -1
diff --git a/Samples/ZeroServer/appsettings.json b/Samples/ZeroServer/appsettings.json
index 4898adc..1338702 100644
--- a/Samples/ZeroServer/appsettings.json
+++ b/Samples/ZeroServer/appsettings.json
@@ -7,7 +7,7 @@
     }
   },
   "AllowedHosts": "*",
-  "Urls": "http://*:1880",
+  "Urls": "http://*:6080",
   "ConnectionStrings": {
     "IoT": "Data Source=..\\Data\\IoT.db;Provider=Sqlite",
     "IoTData": "Data Source=..\\Data\\IoTData.db;ShowSql=false;Provider=Sqlite",
Modified +1 -1
diff --git a/Samples/ZeroServer/Clients/ClientSetting.cs b/Samples/ZeroServer/Clients/ClientSetting.cs
index 5fd18c3..5dce91e 100644
--- a/Samples/ZeroServer/Clients/ClientSetting.cs
+++ b/Samples/ZeroServer/Clients/ClientSetting.cs
@@ -11,7 +11,7 @@ public class ClientSetting : Config<ClientSetting>, IClientSetting
     #region 属性
     /// <summary>服务端地址。IoT服务平台地址</summary>
     [Description("服务端地址。IoT服务平台地址")]
-    public String Server { get; set; } = "http://localhost:2880";
+    public String Server { get; set; } = "http://localhost:6080";
 
     /// <summary>客户端编码</summary>
     [Description("客户端编码")]
Modified +3 -3
diff --git a/Samples/ZeroServer/Clients/ClientTest.cs b/Samples/ZeroServer/Clients/ClientTest.cs
index e3aa12d..f908441 100644
--- a/Samples/ZeroServer/Clients/ClientTest.cs
+++ b/Samples/ZeroServer/Clients/ClientTest.cs
@@ -1,12 +1,12 @@
 using NewLife.Log;
 
-namespace ZeroClient.Clients;
+namespace ZeroClient;
 
 /// <summary>客户端测试入口。主程序通过反射调用</summary>
 public static class ClientTest
 {
     private static ITracer _tracer;
-    private static HttpDevice _device;
+    private static NodeClient _device;
 
     public static async Task Main(IServiceProvider serviceProvider)
     {
@@ -19,7 +19,7 @@ public static class ClientTest
         var set = ClientSetting.Current;
 
         // 产品编码、产品密钥从IoT管理平台获取,设备编码支持自动注册
-        var device = new HttpDevice(set)
+        var device = new NodeClient(set)
         {
             Tracer = _tracer,
             Log = XTrace.Log,
Renamed +36 -40
Samples/ZeroServer/Clients/HttpDevice.cs → Samples/ZeroServer/Clients/NodeClient.cs
diff --git a/Samples/ZeroServer/Clients/HttpDevice.cs b/Samples/ZeroServer/Clients/NodeClient.cs
similarity index 64%
rename from Samples/ZeroServer/Clients/HttpDevice.cs
rename to Samples/ZeroServer/Clients/NodeClient.cs
index 5893244..3cc81a9 100644
--- a/Samples/ZeroServer/Clients/HttpDevice.cs
+++ b/Samples/ZeroServer/Clients/NodeClient.cs
@@ -1,23 +1,28 @@
 using NewLife;
-using NewLife.IoT.Models;
-using NewLife.IoT.ThingModels;
 using NewLife.Model;
-using NewLife.Remoting;
 using NewLife.Remoting.Clients;
 using NewLife.Remoting.Models;
 using NewLife.Security;
+using ZeroServer.Models;
+using MigrationEventArgs = Stardust.Models.MigrationEventArgs;
 
 namespace ZeroClient;
 
 /// <summary>Http协议设备</summary>
-public class HttpDevice : ClientBase
+public class NodeClient : ClientBase
 {
     #region 属性
+    /// <summary>产品编码</summary>
+    public String? ProductCode { get; set; }
+
+    /// <summary>服务迁移</summary>
+    public event EventHandler<MigrationEventArgs>? OnMigration;
+
     private readonly ClientSetting _setting;
     #endregion
 
     #region 构造
-    public HttpDevice(ClientSetting setting) : base(setting)
+    public NodeClient(ClientSetting setting) : base(setting)
     {
         // 设置动作,开启下行通知
         Features = Features.Login | Features.Logout | Features.Ping | Features.Notify | Features.Upgrade;
@@ -49,13 +54,11 @@ public class HttpDevice : ClientBase
     #region 登录
     public override ILoginRequest BuildLoginRequest()
     {
-        var request = base.BuildLoginRequest();
-        if (request is LoginInfo info)
-        {
-            info.Name = Environment.MachineName;
-            info.IP = NetHelper.MyIP() + "";
-            info.UUID = MachineInfo.GetCurrent().BuildCode();
-        }
+        var request = new LoginInfo();
+        FillLoginRequest(request);
+
+        request.ProductCode = ProductCode;
+        request.Name = Environment.MachineName;
 
         return request;
     }
@@ -64,46 +67,39 @@ public class HttpDevice : ClientBase
     #region 心跳
     public override IPingRequest BuildPingRequest()
     {
-        var request = base.BuildPingRequest();
-        if (request is PingInfo info)
-        {
-            info.Memory = 0;
-            info.TotalSize = 0;
-        }
+        var request = new PingInfo();
+        FillPingRequest(request);
 
         return request;
     }
-    #endregion
 
-    #region 数据
-    /// <summary>上传数据</summary>
+    /// <summary>心跳</summary>
     /// <returns></returns>
-    public async Task PostDataAsync()
+    public override async Task<IPingResponse?> Ping(CancellationToken cancellationToken = default)
     {
-        //if (Tracer != null) DefaultSpan.Current = null;
-
-        using var span = Tracer?.NewSpan("PostData");
-        try
+        var rs = await base.Ping(cancellationToken);
+        if (rs != null)
         {
-            var items = new List<DataModel>
+            // 迁移到新服务器
+            if (rs is PingResponse prs && !prs.NewServer.IsNullOrEmpty() && prs.NewServer != Server)
             {
-                new() {
-                    Time = DateTime.UtcNow.ToLong(),
-                    Name = "TestValue",
-                    Value = Rand.Next(0, 100) + ""
-                }
-            };
+                var arg = new MigrationEventArgs { NewServer = prs.NewServer };
 
-            var data = new DataModels { DeviceCode = Code, Items = items.ToArray() };
+                OnMigration?.Invoke(this, arg);
+                if (!arg.Cancel)
+                {
+                    await Logout("切换新服务器");
 
-            await InvokeAsync<Int32>("Thing/PostData", data);
-        }
-        catch (Exception ex)
-        {
-            span?.SetError(ex, null);
+                    // 清空原有链接,添加新链接
+                    Server = prs.NewServer;
+                    Client = null;
 
-            throw;
+                    await Login();
+                }
+            }
         }
+
+        return rs;
     }
     #endregion
 }
\ No newline at end of file
Modified +37 -4
diff --git a/Samples/ZeroServer/Controllers/NodeController.cs b/Samples/ZeroServer/Controllers/NodeController.cs
index d4728f0..982dee1 100644
--- a/Samples/ZeroServer/Controllers/NodeController.cs
+++ b/Samples/ZeroServer/Controllers/NodeController.cs
@@ -1,9 +1,12 @@
-using Microsoft.AspNetCore.Mvc;
+using System.Net.WebSockets;
+using Microsoft.AspNetCore.Mvc;
 using NewLife.Log;
 using NewLife.Remoting;
 using NewLife.Remoting.Extensions;
+using NewLife.Remoting.Extensions.Services;
 using NewLife.Remoting.Models;
 using Zero.Data.Nodes;
+using ZeroServer.Services;
 
 namespace ZeroServer.Controllers;
 
@@ -14,8 +17,9 @@ namespace ZeroServer.Controllers;
 public class NodeController : BaseDeviceController
 {
     /// <summary>当前设备</summary>
-    public Node Node { get; set; }
+    public Node? Node { get; set; }
 
+    private readonly NodeService _nodeService;
     private readonly ITracer _tracer;
 
     #region 构造
@@ -27,6 +31,7 @@ public class NodeController : BaseDeviceController
     /// <param name="tracer"></param>
     public NodeController(IServiceProvider serviceProvider, ITracer tracer) : base(serviceProvider)
     {
+        _nodeService = serviceProvider.GetRequiredService<IDeviceService>() as NodeService;
         _tracer = tracer;
     }
 
@@ -44,15 +49,43 @@ public class NodeController : BaseDeviceController
     /// <summary>心跳</summary>
     /// <param name="request"></param>
     /// <returns></returns>
-    protected override IPingResponse OnPing(IPingRequest request)
+    protected override IPingResponse OnPing(IPingRequest? request)
     {
         var rs = base.OnPing(request);
 
         var device = Node;
-        if (device != null && rs != null)
+        if (device != null)
             rs.Period = device.Period;
 
         return rs;
     }
+
+    protected override async Task HandleNotify(WebSocket socket, String token)
+    {
+        NodeOnline online = null;
+        var node = Node;
+        if (node != null)
+        {
+            online = _nodeService.GetOnline(node, UserHost);
+            if (online != null)
+            {
+                online.WebSocket = true;
+                online.Update();
+            }
+        }
+
+        try
+        {
+            await base.HandleNotify(socket, token);
+        }
+        finally
+        {
+            if (online != null)
+            {
+                online.WebSocket = false;
+                online.Update();
+            }
+        }
+    }
     #endregion
 }
\ No newline at end of file
Modified +21 -21
diff --git a/Samples/ZeroServer/Models/LoginInfo.cs b/Samples/ZeroServer/Models/LoginInfo.cs
index f42973a..d4cb36b 100644
--- a/Samples/ZeroServer/Models/LoginInfo.cs
+++ b/Samples/ZeroServer/Models/LoginInfo.cs
@@ -1,39 +1,39 @@
 using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models;
+namespace ZeroServer.Models;
 
 /// <summary>节点登录信息</summary>
 public class LoginInfo : LoginRequest
 {
     #region 属性
-    ///// <summary>设备编码</summary>
-    //public String Code { get; set; }
+    /// <summary>产品编码</summary>
+    public String? ProductCode { get; set; }
 
-    ///// <summary>设备密钥</summary>
-    //public String Secret { get; set; }
+    /// <summary>名称。可用于标识设备的名称</summary>
+    public String? Name { get; set; }
 
-    /// <summary>产品证书</summary>
-    public String ProductKey { get; set; }
+    /// <summary>系统名</summary>
+    public String? OSName { get; set; }
 
-    /// <summary>产品密钥</summary>
-    public String ProductSecret { get; set; }
+    /// <summary>系统版本</summary>
+    public String? OSVersion { get; set; }
 
-    ///// <summary>实例。应用可能多实例部署,ip@proccessid</summary>
-    //public String ClientId { get; set; }
+    /// <summary>处理器架构</summary>
+    public String? Architecture { get; set; }
 
-    /// <summary>名称。可用于标识设备的名称</summary>
-    public String Name { get; set; }
+    /// <summary>机器名</summary>
+    public String? MachineName { get; set; }
 
-    ///// <summary>版本</summary>
-    //public String Version { get; set; }
+    /// <summary>用户名</summary>
+    public String? UserName { get; set; }
 
-    /// <summary>本地IP地址</summary>
-    public String IP { get; set; }
+    /// <summary>核心数</summary>
+    public Int32 ProcessorCount { get; set; }
 
-    /// <summary>唯一标识</summary>
-    public String UUID { get; set; }
+    /// <summary>内存大小</summary>
+    public UInt64 Memory { get; set; }
 
-    ///// <summary>本地UTC时间</summary>
-    //public Int64 Time { get; set; }
+    /// <summary>磁盘大小。应用所在盘</summary>
+    public UInt64 TotalSize { get; set; }
     #endregion
 }
\ No newline at end of file
Modified +5 -5
diff --git a/Samples/ZeroServer/Models/PingInfo.cs b/Samples/ZeroServer/Models/PingInfo.cs
index 04ebe73..e67a364 100644
--- a/Samples/ZeroServer/Models/PingInfo.cs
+++ b/Samples/ZeroServer/Models/PingInfo.cs
@@ -1,19 +1,19 @@
 using NewLife.Remoting.Models;
 
-namespace NewLife.IoT.Models;
+namespace ZeroServer.Models;
 
 /// <summary>心跳信息</summary>
 public class PingInfo : PingRequest
 {
     #region 属性
-    /// <summary>内存大小</summary>
-    public UInt64 Memory { get; set; }
+    ///// <summary>内存大小</summary>
+    //public UInt64 Memory { get; set; }
 
     ///// <summary>可用内存大小</summary>
     //public UInt64 AvailableMemory { get; set; }
 
-    /// <summary>磁盘大小。应用所在盘</summary>
-    public UInt64 TotalSize { get; set; }
+    ///// <summary>磁盘大小。应用所在盘</summary>
+    //public UInt64 TotalSize { get; set; }
 
     ///// <summary>磁盘可用空间。应用所在盘</summary>
     //public UInt64 AvailableFreeSpace { get; set; }
Modified +4 -15
diff --git "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271.Biz.cs" "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271.Biz.cs"
index 4e15d59..ceec499 100644
--- "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271.Biz.cs"
+++ "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271.Biz.cs"
@@ -4,10 +4,10 @@ using System.Xml.Serialization;
 using NewLife;
 using NewLife.Data;
 using NewLife.Remoting.Models;
-using Stardust.Models;
 using XCode;
 using XCode.Cache;
 using XCode.Membership;
+using ZeroServer.Models;
 
 namespace Zero.Data.Nodes;
 
@@ -386,7 +386,7 @@ public partial class Node : Entity<Node>, IDeviceModel
     /// <summary>登录并保存信息</summary>
     /// <param name="di"></param>
     /// <param name="ip"></param>
-    public void Login(NodeInfo di, String ip)
+    public void Login(LoginInfo di, String ip)
     {
         var node = this;
 
@@ -410,7 +410,7 @@ public partial class Node : Entity<Node>, IDeviceModel
 
     /// <summary>填充</summary>
     /// <param name="di"></param>
-    public void Fill(NodeInfo di)
+    public void Fill(LoginInfo di)
     {
         var node = this;
 
@@ -418,28 +418,17 @@ public partial class Node : Entity<Node>, IDeviceModel
         if (!di.OSVersion.IsNullOrEmpty()) node.OSVersion = di.OSVersion;
         if (!di.Architecture.IsNullOrEmpty()) node.Architecture = di.Architecture;
         if (!di.Version.IsNullOrEmpty()) node.Version = di.Version;
-        if (di.Compile.Year > 2000) node.CompileTime = di.Compile;
+        if (di.Compile > 2000) node.CompileTime = di.Compile.ToDateTime().ToLocalTime();
 
         if (!di.MachineName.IsNullOrEmpty()) node.MachineName = di.MachineName;
         if (!di.UserName.IsNullOrEmpty()) node.UserName = di.UserName;
         if (!di.IP.IsNullOrEmpty()) node.IP = di.IP;
-        if (!di.Processor.IsNullOrEmpty()) node.Processor = di.Processor;
-        //if (!di.CpuID.IsNullOrEmpty()) node.CpuID = di.CpuID;
         if (!di.UUID.IsNullOrEmpty()) node.Uuid = di.UUID;
-        if (!di.MachineGuid.IsNullOrEmpty()) node.MachineGuid = di.MachineGuid;
-        if (!di.DiskID.IsNullOrEmpty()) node.DiskID = di.DiskID;
 
         if (di.ProcessorCount > 0) node.Cpu = di.ProcessorCount;
         if (di.Memory > 0) node.Memory = (Int32)(di.Memory / 1024 / 1024);
         if (di.TotalSize > 0) node.TotalSize = (Int32)(di.TotalSize / 1024 / 1024);
-        if (di.MaxOpenFiles > 0) node.MaxOpenFiles = di.MaxOpenFiles;
-        if (!di.Dpi.IsNullOrEmpty()) node.Dpi = di.Dpi;
-        if (!di.Resolution.IsNullOrEmpty()) node.Resolution = di.Resolution;
         if (!di.Macs.IsNullOrEmpty()) node.MACs = di.Macs;
-        //if (!di.COMs.IsNullOrEmpty()) node.COMs = di.COMs;
-        if (!di.InstallPath.IsNullOrEmpty()) node.InstallPath = di.InstallPath;
-        if (!di.Runtime.IsNullOrEmpty()) node.Runtime = di.Runtime;
-        if (!di.Framework.IsNullOrEmpty()) node.Framework = di.Framework;
     }
 
     /// <summary>修正地区</summary>
Modified +24 -23
diff --git "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\216\206\345\217\262.Biz.cs" "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\216\206\345\217\262.Biz.cs"
index 78ca684..6ed27eb 100644
--- "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\216\206\345\217\262.Biz.cs"
+++ "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\216\206\345\217\262.Biz.cs"
@@ -19,6 +19,7 @@ public partial class NodeHistory : Entity<NodeHistory>
 
         Meta.Modules.Add<TimeModule>();
         Meta.Modules.Add<IPModule>();
+        Meta.Modules.Add<TraceModule>();
     }
 
     /// <summary>插入或修改时</summary>
@@ -79,34 +80,34 @@ public partial class NodeHistory : Entity<NodeHistory>
         return Find(_.Id == id);
     }
 
-/// <summary>根据编号查找</summary>
-/// <param name="id">编号</param>
-/// <returns>实体对象</returns>
-public static NodeHistory FindById(Int64 id)
-{
-    if (id <= 0) return null;
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static NodeHistory FindById(Int64 id)
+    {
+        if (id <= 0) return null;
 
-    // 实体缓存
-    if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
 
-    // 单对象缓存
-    return Meta.SingleCache[id];
+        // 单对象缓存
+        return Meta.SingleCache[id];
 
-    //return Find(_.Id == id);
-}
+        //return Find(_.Id == id);
+    }
 
-/// <summary>根据节点、操作查找</summary>
-/// <param name="nodeId">节点</param>
-/// <param name="action">操作</param>
-/// <returns>实体列表</returns>
-public static IList<NodeHistory> FindAllByNodeIDAndAction(Int32 nodeId, String action)
-{
+    /// <summary>根据节点、操作查找</summary>
+    /// <param name="nodeId">节点</param>
+    /// <param name="action">操作</param>
+    /// <returns>实体列表</returns>
+    public static IList<NodeHistory> FindAllByNodeIDAndAction(Int32 nodeId, String action)
+    {
 
-    // 实体缓存
-    if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.NodeId == nodeId && e.Action.EqualIgnoreCase(action));
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.NodeId == nodeId && e.Action.EqualIgnoreCase(action));
 
-    return FindAll(_.NodeId == nodeId & _.Action == action);
-}
+        return FindAll(_.NodeId == nodeId & _.Action == action);
+    }
     #endregion
 
     #region 高级查询
@@ -163,7 +164,7 @@ public static IList<NodeHistory> FindAllByNodeIDAndAction(Int32 nodeId, String a
     /// <returns></returns>
     public static NodeHistory Create(Node node, String action, Boolean success, String remark, String creator, String ip)
     {
-        if (node == null) node = new Node();
+        node ??= new Node();
 
         var history = new NodeHistory
         {
Modified +33 -47
diff --git "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\234\250\347\272\277.Biz.cs" "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\234\250\347\272\277.Biz.cs"
index 0adaa36..7dd2313 100644
--- "a/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\234\250\347\272\277.Biz.cs"
+++ "b/Samples/ZeroServer/Nodes/\350\212\202\347\202\271\345\234\250\347\272\277.Biz.cs"
@@ -5,9 +5,9 @@ using NewLife;
 using NewLife.Data;
 using NewLife.Remoting.Models;
 using NewLife.Serialization;
-using Stardust.Models;
 using XCode;
 using XCode.Membership;
+using ZeroServer.Models;
 
 namespace Zero.Data.Nodes;
 
@@ -193,90 +193,76 @@ public partial class NodeOnline : Entity<NodeOnline>, IOnlineModel
     }
 
     /// <summary>更新并保存在线状态</summary>
-    /// <param name="di"></param>
-    /// <param name="pi"></param>
+    /// <param name="login"></param>
+    /// <param name="ping"></param>
     /// <param name="token"></param>
     /// <param name="ip"></param>
-    public void Save(NodeInfo di, PingInfo pi, String token, String ip)
+    public void Save(LoginInfo login, PingInfo ping, String token, String ip)
     {
-        var olt = this;
+        var online = this;
 
-        if (di != null)
+        if (login != null)
         {
-            olt.Fill(di);
-            olt.LocalTime = di.Time.ToLocalTime();
-            olt.MACs = di.Macs;
-            //olt.COMs = di.COMs;
+            online.Fill(login);
+            online.LocalTime = login.Time.ToDateTime().ToLocalTime();
+            online.MACs = login.Macs;
         }
         else
         {
-            olt.Fill(pi);
-            //olt.CreateData(pi, ip);
+            online.Fill(ping);
         }
 
-        olt.Token = token;
-        olt.PingCount++;
-        olt.UpdateIP = ip;
+        online.Token = token;
+        online.PingCount++;
+        online.UpdateIP = ip;
 
         // 5秒内直接保存
-        if (olt.CreateTime.AddSeconds(5) > DateTime.Now)
-            olt.Save();
+        if (online.CreateTime.AddSeconds(5) > DateTime.Now)
+            online.Save();
         else
-            olt.SaveAsync();
+            online.SaveAsync();
     }
 
     /// <summary>填充节点信息</summary>
     /// <param name="di"></param>
-    public void Fill(NodeInfo di)
+    public void Fill(LoginInfo di)
     {
         var online = this;
 
-        online.LocalTime = di.Time.ToLocalTime();
+        online.LocalTime = di.Time.ToDateTime().ToLocalTime();
         online.MACs = di.Macs;
         //online.COMs = di.COMs;
         online.IP = di.IP;
-
-        if (di.AvailableMemory > 0) online.AvailableMemory = (Int32)(di.AvailableMemory / 1024 / 1024);
-        if (di.AvailableFreeSpace > 0) online.AvailableFreeSpace = (Int32)(di.AvailableFreeSpace / 1024 / 1024);
     }
 
     /// <summary>填充在线节点信息</summary>
     /// <param name="inf"></param>
     private void Fill(PingInfo inf)
     {
-        var olt = this;
-
-        if (inf.AvailableMemory > 0) olt.AvailableMemory = (Int32)(inf.AvailableMemory / 1024 / 1024);
-        if (inf.AvailableFreeSpace > 0) olt.AvailableFreeSpace = (Int32)(inf.AvailableFreeSpace / 1024 / 1024);
-        if (inf.CpuRate > 0) olt.CpuRate = inf.CpuRate;
-        if (inf.Temperature > 0) olt.Temperature = inf.Temperature;
-        if (inf.Battery > 0) olt.Battery = inf.Battery;
-        if (inf.UplinkSpeed > 0) olt.UplinkSpeed = (Int64)inf.UplinkSpeed;
-        if (inf.DownlinkSpeed > 0) olt.DownlinkSpeed = (Int64)inf.DownlinkSpeed;
-        if (inf.ProcessCount > 0) olt.ProcessCount = inf.ProcessCount;
-        if (inf.TcpConnections > 0) olt.TcpConnections = inf.TcpConnections;
-        if (inf.TcpTimeWait > 0) olt.TcpTimeWait = inf.TcpTimeWait;
-        if (inf.TcpCloseWait > 0) olt.TcpCloseWait = inf.TcpCloseWait;
-        if (inf.Uptime > 0) olt.Uptime = inf.Uptime;
-        if (inf.Delay > 0) olt.Delay = inf.Delay;
+        var online = this;
+
+        if (inf.AvailableMemory > 0) online.AvailableMemory = (Int32)(inf.AvailableMemory / 1024 / 1024);
+        if (inf.AvailableFreeSpace > 0) online.AvailableFreeSpace = (Int32)(inf.AvailableFreeSpace / 1024 / 1024);
+        if (inf.CpuRate > 0) online.CpuRate = inf.CpuRate;
+        if (inf.Temperature > 0) online.Temperature = inf.Temperature;
+        if (inf.Battery > 0) online.Battery = inf.Battery;
+        if (inf.UplinkSpeed > 0) online.UplinkSpeed = (Int64)inf.UplinkSpeed;
+        if (inf.DownlinkSpeed > 0) online.DownlinkSpeed = (Int64)inf.DownlinkSpeed;
+        if (inf.Uptime > 0) online.Uptime = inf.Uptime;
+        if (inf.Delay > 0) online.Delay = inf.Delay;
 
         var dt = inf.Time.ToDateTime().ToLocalTime();
         if (dt.Year > 2000)
         {
-            olt.LocalTime = dt;
-            //olt.Offset = (Int32)Math.Round((dt - DateTime.Now).TotalSeconds);
-            olt.Offset = (Int32)(inf.Time - DateTime.UtcNow.ToLong());
+            online.LocalTime = dt;
+            online.Offset = (Int32)(inf.Time - DateTime.UtcNow.ToLong());
         }
 
-        if (!inf.Processes.IsNullOrEmpty()) olt.Processes = inf.Processes;
-        if (!inf.Macs.IsNullOrEmpty()) olt.MACs = inf.Macs;
-        //if (!inf.COMs.IsNullOrEmpty()) olt.COMs = inf.COMs;
-        if (!inf.IP.IsNullOrEmpty()) olt.IP = inf.IP;
+        if (!inf.IP.IsNullOrEmpty()) online.IP = inf.IP;
 
-        //olt.Data = inf.ToJson();
         var dic = inf.ToDictionary();
         dic.Remove("Processes");
-        olt.Data = dic.ToJson();
+        online.Data = dic.ToJson();
     }
 
     //private void CreateData(PingInfo inf, String ip)
Modified +1 -1
diff --git a/Samples/ZeroServer/Program.cs b/Samples/ZeroServer/Program.cs
index 97f3fb3..59d97b4 100644
--- a/Samples/ZeroServer/Program.cs
+++ b/Samples/ZeroServer/Program.cs
@@ -67,7 +67,7 @@ app.MapControllerRoute(
 app.RegisterService("ZeroServer", null, app.Environment.EnvironmentName);
 
 // 反射查找并调用客户端测试,该代码仅用于测试,实际项目中不要这样做
-var clientType = "ZeroServer.Clients.ClientTest".GetTypeEx();
+var clientType = "ZeroClient.ClientTest".GetTypeEx();
 var test = clientType?.GetMethodEx("Main").As<Func<IServiceProvider, Task>>();
 if (test != null) _ = Task.Run(() => test(app.Services));
 
Modified +1 -1
diff --git a/Samples/ZeroServer/Properties/launchSettings.json b/Samples/ZeroServer/Properties/launchSettings.json
index c4b50be..7efa44c 100644
--- a/Samples/ZeroServer/Properties/launchSettings.json
+++ b/Samples/ZeroServer/Properties/launchSettings.json
@@ -6,7 +6,7 @@
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
       },
-      "applicationUrl": "http://localhost:2880"
+      "applicationUrl": "http://localhost:6080"
     }
   }
 }
\ No newline at end of file
Modified +2 -4
diff --git a/Samples/ZeroServer/Services/IoTExtensions.cs b/Samples/ZeroServer/Services/IoTExtensions.cs
index 96f6885..92a8c16 100644
--- a/Samples/ZeroServer/Services/IoTExtensions.cs
+++ b/Samples/ZeroServer/Services/IoTExtensions.cs
@@ -1,8 +1,8 @@
-using NewLife.IoT.Models;
-using NewLife.Remoting.Extensions;
+using NewLife.Remoting.Extensions;
 using NewLife.Remoting.Extensions.Models;
 using NewLife.Remoting.Extensions.Services;
 using NewLife.Remoting.Models;
+using ZeroServer.Models;
 
 namespace ZeroServer.Services;
 
@@ -14,8 +14,6 @@ public static class IoTExtensions
         ArgumentNullException.ThrowIfNull(setting);
 
         // 逐个注册每一个用到的服务,必须做到清晰明了
-        services.AddSingleton<QueueService>();
-
         services.AddSingleton<IDeviceService, NodeService>();
 
         services.AddTransient<ILoginRequest, LoginInfo>();
Modified +1 -1
diff --git a/Samples/ZeroServer/Services/NodeOnlineService.cs b/Samples/ZeroServer/Services/NodeOnlineService.cs
index 3e1598c..4c5d320 100644
--- a/Samples/ZeroServer/Services/NodeOnlineService.cs
+++ b/Samples/ZeroServer/Services/NodeOnlineService.cs
@@ -80,7 +80,7 @@ public class NodeOnlineService : IHostedService
                         if (olt.CreateTime.Year > 2000 && olt.UpdateTime.Year > 2000)
                         {
                             node.OnlineTime += (Int32)(olt.UpdateTime - olt.CreateTime).TotalSeconds;
-                            node.Logout();
+                            node.Update();
                         }
 
                         CheckOffline(node, "超时下线");
Modified +76 -150
diff --git a/Samples/ZeroServer/Services/NodeService.cs b/Samples/ZeroServer/Services/NodeService.cs
index b5ab14a..3ad2ab4 100644
--- a/Samples/ZeroServer/Services/NodeService.cs
+++ b/Samples/ZeroServer/Services/NodeService.cs
@@ -2,7 +2,6 @@
 using NewLife;
 using NewLife.Caching;
 using NewLife.Caching.Queues;
-using NewLife.IoT.Models;
 using NewLife.Log;
 using NewLife.Remoting;
 using NewLife.Remoting.Extensions.Services;
@@ -11,6 +10,7 @@ using NewLife.Security;
 using NewLife.Serialization;
 using NewLife.Web;
 using Zero.Data.Nodes;
+using ZeroServer.Models;
 
 namespace ZeroServer.Services;
 
@@ -56,107 +56,72 @@ public class NodeService : IDeviceService
         var code = inf.Code;
         var secret = inf.Secret;
 
-        var dv = Node.FindByCode(code);
+        var node = Node.FindByCode(code!, true);
+        if (node != null && !node.Enable) throw new ApiException(99, "禁止登录");
 
         var autoReg = false;
-        if (dv == null)
+        if (node == null)
         {
-            if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(ApiCode.NotFound, "找不到设备,且产品证书为空,无法登录");
-
-            dv = AutoRegister(null, inf, ip);
+            node = AutoRegister(null, inf, ip);
             autoReg = true;
         }
         else
         {
-            if (!dv.Enable) throw new ApiException(ApiCode.Forbidden, "禁止登录");
+            if (!node.Enable) throw new ApiException(ApiCode.Forbidden, "禁止登录");
 
             // 校验唯一编码,防止客户端拷贝配置
             var uuid = inf.UUID;
-            if (!uuid.IsNullOrEmpty() && !dv.Uuid.IsNullOrEmpty() && uuid != dv.Uuid)
-                WriteHistory(dv, source + "登录校验", false, $"新旧唯一标识不一致!(新){uuid}!={dv.Uuid}(旧)", ip);
+            if (!uuid.IsNullOrEmpty() && !node.Uuid.IsNullOrEmpty() && uuid != node.Uuid)
+                WriteHistory(node, source + "登录校验", false, $"新旧唯一标识不一致!(新){uuid}!={node.Uuid}(旧)", ip);
 
             // 登录密码未设置或者未提交,则执行动态注册
-            if (dv == null || !dv.Secret.IsNullOrEmpty()
-                && (secret.IsNullOrEmpty() || !_passwordProvider.Verify(dv.Secret, secret)))
+            if (node == null || !node.Secret.IsNullOrEmpty()
+                && (secret.IsNullOrEmpty() || !_passwordProvider.Verify(node.Secret, secret)))
             {
-                if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "设备验证失败,且产品证书为空,无法登录");
-
-                dv = AutoRegister(dv, inf, ip);
+                node = AutoRegister(node, inf, ip);
                 autoReg = true;
             }
         }
 
-        //if (dv != null && !dv.Enable) throw new ApiException(99, "禁止登录");
-
-        if (dv == null) throw new ApiException(ApiCode.Unauthorized, "节点鉴权失败");
+        if (node == null) throw new ApiException(ApiCode.Unauthorized, "节点鉴权失败");
 
-        dv.Login(inf, ip);
+        node.Login(inf, ip);
 
         // 在线记录
-        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
-        olt.Save(inf, null, null);
-
-        //SetChildOnline(dv, ip);
+        var olt = GetOnline(node, ip) ?? CreateOnline(node, ip);
+        olt.Save(inf, null, null, ip);
 
         // 登录历史
-        WriteHistory(dv, source + "设备鉴权", true, $"[{dv.Name}/{dv.Code}]鉴权成功 " + inf.ToJson(false, false, false), ip);
+        WriteHistory(node, source + "节点鉴权", true, $"[{node.Name}/{node.Code}]鉴权成功 " + inf.ToJson(false, false, false), ip);
 
         var rs = new LoginResponse
         {
-            Name = dv.Name
+            Name = node.Name
         };
 
         // 动态注册,下发节点证书
-        if (autoReg) rs.Secret = dv.Secret;
-
-        return (dv, olt, rs);
-    }
-
-    /// <summary>设置设备在线,同时检查在线表</summary>
-    /// <param name="dv"></param>
-    /// <param name="ip"></param>
-    /// <param name="reason"></param>
-    public void SetDeviceOnline(Node dv, String ip, String reason)
-    {
-        // 如果已上线,则不需要埋点
-        var tracer = _tracer;
-        //if (dv.Online) tracer = null;
-        using var span = tracer?.NewSpan(nameof(SetDeviceOnline), new { dv.Name, dv.Code, ip, reason });
+        if (autoReg) rs.Secret = node.Secret;
 
-        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
-
-        dv.SetOnline(ip, reason);
-
-        // 避免频繁更新心跳数
-        if (olt.UpdateTime.AddSeconds(60) < DateTime.Now)
-            olt.Save(null, null, null);
+        return (node, olt, rs);
     }
 
     /// <summary>自动注册</summary>
-    /// <param name="device"></param>
+    /// <param name="node"></param>
     /// <param name="inf"></param>
     /// <param name="ip"></param>
     /// <returns></returns>
     /// <exception cref="ApiException"></exception>
-    public Node AutoRegister(Node device, LoginInfo inf, String ip)
+    public Node AutoRegister(Node? node, LoginInfo inf, String ip)
     {
         // 全局开关,是否允许自动注册新产品
         if (!_setting.AutoRegister) throw new ApiException(12, "禁止自动注册");
 
-        // 验证产品,即使产品不给自动注册,也会插入一个禁用的设备
-        var product = Product.FindByCode(inf.ProductKey);
-        if (product == null || !product.Enable)
-            throw new ApiException(13, $"无效产品[{inf.ProductKey}]!");
-        //if (!product.Secret.IsNullOrEmpty() && !_passwordProvider.Verify(product.Secret, inf.ProductSecret))
-        //    throw new ApiException(13, $"非法产品[{product}]!");
-
-        //// 检查白名单
-        //if (!product.IsMatchWhiteIP(ip)) throw new ApiException(13, "非法来源,禁止注册");
-
         var code = inf.Code;
+        if (code.IsNullOrEmpty()) code = inf.UUID.GetBytes().Crc().ToString("X8");
         if (code.IsNullOrEmpty()) code = Rand.NextString(8);
 
-        device ??= new Node
+        node ??= Node.FindByCode(code, false);
+        node ??= new Node
         {
             Code = code,
             CreateIP = ip,
@@ -165,55 +130,50 @@ public class NodeService : IDeviceService
         };
 
         // 如果未打开动态注册,则把节点修改为禁用
-        device.Enable = true;
-
-        if (device.Name.IsNullOrEmpty()) device.Name = inf.Name;
+        node.Enable = true;
 
-        device.ProductId = product.Id;
-        //device.Secret = Rand.NextString(16);
-        device.UpdateIP = ip;
-        device.UpdateTime = DateTime.Now;
+        if (node.Name.IsNullOrEmpty()) node.Name = inf.Name;
 
-        device.Save();
+        node.ProductCode = inf.ProductCode;
+        node.Secret = Rand.NextString(16);
+        node.UpdateIP = ip;
+        node.UpdateTime = DateTime.Now;
 
-        // 更新产品设备总量避免界面无法及时获取设备数量信息
-        device.Product.Fix();
+        node.Save();
 
-        WriteHistory(device, "动态注册", true, inf.ToJson(false, false, false), ip);
+        WriteHistory(node, "动态注册", true, inf.ToJson(false, false, false), ip);
 
-        return device;
+        return node;
     }
 
     /// <summary>注销</summary>
-    /// <param name="device">设备</param>
+    /// <param name="model">设备</param>
     /// <param name="reason">注销原因</param>
     /// <param name="source">登录来源</param>
     /// <param name="ip">远程IP</param>
     /// <returns></returns>
-    public IOnlineModel Logout(IDeviceModel device, String reason, String source, String ip)
+    public IOnlineModel Logout(IDeviceModel model, String reason, String source, String ip)
     {
-        var dv = device as Node;
-        var olt = GetOnline(dv, ip);
-        if (olt != null)
+        var node = model as Node;
+        var online = GetOnline(node, ip);
+        if (online != null)
         {
-            var msg = $"{reason} [{device}]]登录于{olt.CreateTime.ToFullString()},最后活跃于{olt.UpdateTime.ToFullString()}";
-            WriteHistory(device, source + "设备下线", true, msg, ip);
-            olt.Delete();
+            var msg = $"{reason} [{model}]]登录于{online.CreateTime.ToFullString()},最后活跃于{online.UpdateTime.ToFullString()}";
+            WriteHistory(model, source + "设备下线", true, msg, ip);
+            online.Delete();
 
-            var sid = $"{dv.Id}@{ip}";
+            var sid = $"{node.Id}@{ip}";
             _cache.Remove($"NodeOnline:{sid}");
 
             // 计算在线时长
-            if (olt.CreateTime.Year > 2000)
+            if (online.CreateTime.Year > 2000)
             {
-                dv.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
-                dv.Logout();
+                node.OnlineTime += (Int32)(DateTime.Now - online.CreateTime).TotalSeconds;
+                node.Update();
             }
-
-            //DeviceOnlineService.CheckOffline(device, "注销");
         }
 
-        return olt;
+        return online;
     }
     #endregion
 
@@ -223,63 +183,57 @@ public class NodeService : IDeviceService
     /// <param name="token"></param>
     /// <param name="ip"></param>
     /// <returns></returns>
-    public IOnlineModel Ping(IDeviceModel device, IPingRequest request, String token, String ip)
+    public IOnlineModel Ping(IDeviceModel model, IPingRequest request, String token, String ip)
     {
-        var dv = device as Node;
+        var node = model as Node;
         var inf = request as PingInfo;
-        if (inf != null && !inf.IP.IsNullOrEmpty()) dv.IP = inf.IP;
+        if (inf != null && !inf.IP.IsNullOrEmpty()) node.IP = inf.IP;
 
-        // 自动上线
-        if (dv != null && !dv.Online) dv.SetOnline(ip, "心跳");
+        node.UpdateIP = ip;
+        node.SaveAsync();
 
-        dv.UpdateIP = ip;
-        dv.SaveAsync();
+        var online = GetOnline(node, ip) ?? CreateOnline(node, ip);
+        online.Name = model.Name;
+        online.Save(null, inf, token, ip);
 
-        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
-        olt.Name = device.Name;
-        olt.GroupPath = dv.GroupPath;
-        olt.ProductId = dv.ProductId;
-        olt.Save(null, inf, token);
-
-        return olt;
+        return online;
     }
 
     /// <summary></summary>
-    /// <param name="device"></param>
+    /// <param name="node"></param>
     /// <param name="ip"></param>
     /// <returns></returns>
-    protected virtual NodeOnline GetOnline(Node device, String ip)
+    public virtual NodeOnline GetOnline(Node node, String ip)
     {
-        var sid = $"{device.Id}@{ip}";
-        var olt = _cache.Get<NodeOnline>($"NodeOnline:{sid}");
-        if (olt != null)
+        var sid = $"{node.Id}@{ip}";
+        var online = _cache.Get<NodeOnline>($"NodeOnline:{sid}");
+        if (online != null)
         {
             _cache.SetExpire($"NodeOnline:{sid}", TimeSpan.FromSeconds(600));
-            return olt;
+            return online;
         }
 
-        return NodeOnline.FindBySessionId(sid);
+        return NodeOnline.FindBySessionID(sid);
     }
 
     /// <summary>检查在线</summary>
-    /// <param name="device"></param>
+    /// <param name="node"></param>
     /// <param name="ip"></param>
     /// <returns></returns>
-    protected virtual NodeOnline CreateOnline(Node device, String ip)
+    public virtual NodeOnline CreateOnline(Node node, String ip)
     {
-        var sid = $"{device.Id}@{ip}";
-        var olt = NodeOnline.GetOrAdd(sid);
-        olt.ProductId = device.ProductId;
-        olt.DeviceId = device.Id;
-        olt.Name = device.Name;
-        olt.IP = device.IP;
-        olt.CreateIP = ip;
+        var sid = $"{node.Id}@{ip}";
+        var online = NodeOnline.GetOrAdd(sid);
+        online.NodeId = node.Id;
+        online.Name = node.Name;
+        online.IP = node.IP;
+        online.CreateIP = ip;
 
-        olt.Creator = Environment.MachineName;
+        online.Creator = Environment.MachineName;
 
-        _cache.Set($"NodeOnline:{sid}", olt, 600);
+        _cache.Set($"NodeOnline:{sid}", online, 600);
 
-        return olt;
+        return online;
     }
 
     /// <summary>删除在线</summary>
@@ -337,33 +291,6 @@ public class NodeService : IDeviceService
     }
 
     /// <summary>
-    /// 解码令牌,并验证有效性
-    /// </summary>
-    /// <param name="token"></param>
-    /// <param name="tokenSecret"></param>
-    /// <returns></returns>
-    /// <exception cref="ApiException"></exception>
-    public Node DecodeToken(String token, String tokenSecret)
-    {
-        //if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
-        if (token.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "节点未登录");
-
-        // 解码令牌
-        var ss = tokenSecret.Split(':');
-        var jwt = new JwtBuilder
-        {
-            Algorithm = ss[0],
-            Secret = ss[1],
-        };
-
-        var rs = jwt.TryDecode(token, out var message);
-        var node = Node.FindByCode(jwt.Subject);
-        if (!rs) throw new ApiException(ApiCode.Forbidden, $"非法访问 {message}");
-
-        return node;
-    }
-
-    /// <summary>
     /// 验证并颁发令牌
     /// </summary>
     /// <param name="deviceCode"></param>
@@ -409,15 +336,14 @@ public class NodeService : IDeviceService
     /// <summary>
     /// 写设备历史
     /// </summary>
-    /// <param name="device"></param>
+    /// <param name="model"></param>
     /// <param name="action"></param>
     /// <param name="success"></param>
     /// <param name="remark"></param>
     /// <param name="ip"></param>
-    public void WriteHistory(IDeviceModel device, String action, Boolean success, String remark, String ip)
+    public void WriteHistory(IDeviceModel model, String action, Boolean success, String remark, String ip)
     {
-        var traceId = DefaultSpan.Current?.TraceId;
-        var hi = NodeHistory.Create(device as Node, action, success, remark, Environment.MachineName, ip, traceId);
+        NodeHistory.Create(model as Node, action, success, remark, Environment.MachineName, ip);
     }
     #endregion
 }
\ No newline at end of file
Deleted +0 -69
Samples/ZeroServer/Services/QueueService.cs
Modified +0 -1
diff --git a/Samples/ZeroServer/ZeroServer.csproj b/Samples/ZeroServer/ZeroServer.csproj
index e0e948f..52af78d 100644
--- a/Samples/ZeroServer/ZeroServer.csproj
+++ b/Samples/ZeroServer/ZeroServer.csproj
@@ -20,7 +20,6 @@
 
   <ItemGroup>
     <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.403" />
-    <PackageReference Include="NewLife.IoT" Version="2.2.2024.501" />
     <PackageReference Include="NewLife.Redis" Version="5.7.2024.602" />
     <PackageReference Include="NewLife.Stardust.Extensions" Version="2.9.2024.402" />
   </ItemGroup>