NewLife/NewLife.Remoting

直接引入ZeroIoT源码,简化用户操作与理解
智能大石头 authored at 2024-05-15 21:27:26
5cdb1c7
Tree
1 Parent(s) e8a7f30
Summary: 57 changed files with 8180 additions and 80 deletions.
Deleted +0 -3
.gitmodules
Added +16 -0
Added +98 -0
Added +225 -0
Added +43 -0
Added +34 -0
Added +49 -0
Added +122 -0
Added +41 -0
Added +13 -0
Added +9 -0
Added +6 -0
Added +14 -0
Added +10 -0
Added +16 -0
Added +9 -0
Added +10 -0
Added +27 -0
Added +320 -0
Added +67 -0
Added +87 -0
Added +191 -0
Added +159 -0
Added +1299 -0
Added +206 -0
Added +0 -0
Added +138 -0
Added +293 -0
Added +279 -0
Added +502 -0
Added +181 -0
Added +292 -0
Added +249 -0
Added +221 -0
Added +284 -0
Added +334 -0
Added +194 -0
Added +274 -0
Added +200 -0
Added +226 -0
Added +69 -0
Modified +0 -76
Added +37 -0
Added +34 -0
Added +22 -0
Added +43 -0
Added +21 -0
Added +13 -0
Added +26 -0
Added +108 -0
Added +77 -0
Added +102 -0
Added +404 -0
Added +69 -0
Added +115 -0
Added +302 -0
Deleted +0 -1
Samples/ZeroIoT
Deleted +0 -3
.gitmodules
Added +16 -0
diff --git a/Samples/IoTZero/appsettings.json b/Samples/IoTZero/appsettings.json
new file mode 100644
index 0000000..4898adc
--- /dev/null
+++ b/Samples/IoTZero/appsettings.json
@@ -0,0 +1,16 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  },
+  "AllowedHosts": "*",
+  "Urls": "http://*:1880",
+  "ConnectionStrings": {
+    "IoT": "Data Source=..\\Data\\IoT.db;Provider=Sqlite",
+    "IoTData": "Data Source=..\\Data\\IoTData.db;ShowSql=false;Provider=Sqlite",
+    "Membership": "Data Source=..\\Data\\Membership.db;Provider=Sqlite"
+  }
+}
Added +98 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DeviceController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DeviceController.cs
new file mode 100644
index 0000000..086cae9
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DeviceController.cs
@@ -0,0 +1,98 @@
+using System.ComponentModel;
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Log;
+using NewLife.Web;
+using XCode;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+[IoTArea]
+//[DisplayName("设备管理")]
+[Menu(80, true, Icon = "fa-mobile")]
+public class DeviceController : EntityController<Device>
+{
+    private readonly ITracer _tracer;
+
+    static DeviceController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("Secret", "Uuid", "ProvinceId", "IP", "Period", "Address", "Location", "Logins", "LastLogin", "LastLoginIP", "OnlineTime", "RegisterTime", "Remark", "AreaName");
+        ListFields.RemoveCreateField();
+        ListFields.RemoveUpdateField();
+
+        {
+            var df = ListFields.AddListField("history", "Online");
+            df.DisplayName = "历史";
+            df.Url = "/IoT/DeviceHistory?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("property", "Online");
+            df.DisplayName = "属性";
+            df.Url = "/IoT/DeviceProperty?deviceId={Id}";
+        }
+
+        {
+            var df = ListFields.AddListField("data", "Online");
+            df.DisplayName = "数据";
+            df.Url = "/IoT/DeviceData?deviceId={Id}";
+        }
+    }
+
+    public DeviceController(ITracer tracer) => _tracer = tracer;
+
+    protected override IEnumerable<Device> Search(Pager p)
+    {
+        var id = p["Id"].ToInt(-1);
+        if (id > 0)
+        {
+            var node = Device.FindById(id);
+            if (node != null) return new[] { node };
+        }
+
+        var productId = p["productId"].ToInt(-1);
+        var groupId = p["groupId"].ToInt(-1);
+        var enable = p["enable"]?.ToBoolean();
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        //// 如果没有指定产品和主设备,则过滤掉子设备
+        //if (productId < 0 && parentId < 0) parentId = 0;
+
+        return Device.Search(productId, groupId, enable, start, end, p["Q"], p);
+    }
+
+    protected override Int32 OnInsert(Device entity)
+    {
+        var rs = base.OnInsert(entity);
+
+        entity.Product?.Fix();
+        return rs;
+    }
+
+    protected override Int32 OnUpdate(Device entity)
+    {
+        var rs = base.OnUpdate(entity);
+
+        entity.Product?.Fix();
+
+        return rs;
+    }
+
+    protected override Int32 OnDelete(Device entity)
+    {
+        // 删除设备时需要顺便把设备属性删除
+        var dpList = DeviceProperty.FindAllByDeviceId(entity.Id);
+        _ = dpList.Delete();
+
+        var rs = base.OnDelete(entity);
+
+        entity.Product?.Fix();
+
+        return rs;
+    }
+}
\ No newline at end of file
Added +225 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
new file mode 100644
index 0000000..ee684cb
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
@@ -0,0 +1,225 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Algorithms;
+using NewLife.Cube;
+using NewLife.Cube.Charts;
+using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
+using NewLife.Data;
+using NewLife.Web;
+using XCode;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(0, false)]
+public class DeviceDataController : EntityController<DeviceData>
+{
+    static DeviceDataController()
+    {
+        ListFields.RemoveField("Id");
+        ListFields.AddListField("Value", null, "Kind");
+
+        {
+            var df = ListFields.GetField("Name") as ListField;
+            //df.DisplayName = "主题";
+            df.Url = "/IoT/DeviceData?deviceId={DeviceId}&name={Name}";
+        }
+        ListFields.TraceUrl("TraceId");
+    }
+
+    protected override IEnumerable<DeviceData> Search(Pager p)
+    {
+        var deviceId = p["deviceId"].ToInt(-1);
+        var name = p["name"];
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        if (start.Year < 2000)
+        {
+            start = DateTime.Today;
+            p["dtStart"] = start.ToString("yyyy-MM-dd");
+            p["dtEnd"] = start.ToString("yyyy-MM-dd");
+        }
+
+        if (deviceId > 0 && p.PageSize == 20 && !name.IsNullOrEmpty() && !name.StartsWithIgnoreCase("raw-", "channel-")) p.PageSize = 14400;
+
+        var list = DeviceData.Search(deviceId, name, start, end, p["Q"], p);
+
+        // 单一设备绘制曲线
+        if (list.Count > 0 && deviceId > 0)
+        {
+            var list2 = list.Where(e => !e.Name.StartsWithIgnoreCase("raw-", "channel-") && e.Value.ToDouble(-1) >= 0).OrderBy(e => e.Id).ToList();
+
+            // 绘制曲线图
+            if (list2.Count > 0)
+            {
+                var topics = list2.Select(e => e.Name).Distinct().ToList();
+                var datax = list2.GroupBy(e => e.CreateTime).ToDictionary(e => e.Key, e => e.ToList());
+                //var topics = list2.GroupBy(e => e.Topic).ToDictionary(e => e.Key, e => e.ToList());
+                var chart = new ECharts
+                {
+                    Height = 400,
+                };
+                //chart.SetX(list2, _.CreateTime, e => e.CreateTime.ToString("mm:ss"));
+
+                // 构建X轴
+                var minT = datax.Keys.Min();
+                var maxT = datax.Keys.Max();
+                var step = p["sample"].ToInt(-1);
+                if (step > 0)
+                {
+                    if (step <= 60)
+                    {
+                        minT = new DateTime(minT.Year, minT.Month, minT.Day, minT.Hour, minT.Minute, 0, minT.Kind);
+                        maxT = new DateTime(maxT.Year, maxT.Month, maxT.Day, maxT.Hour, maxT.Minute, 0, maxT.Kind);
+                    }
+                    else
+                    {
+                        minT = new DateTime(minT.Year, minT.Month, minT.Day, minT.Hour, 0, 0, minT.Kind);
+                        maxT = new DateTime(maxT.Year, maxT.Month, maxT.Day, maxT.Hour, 0, 0, maxT.Kind);
+                        //step = 3600;
+                    }
+                    var times = new List<DateTime>();
+                    for (var dt = minT; dt <= maxT; dt = dt.AddSeconds(step))
+                    {
+                        times.Add(dt);
+                    }
+
+                    if (step < 60)
+                    {
+                        chart.XAxis = new
+                        {
+                            data = times.Select(e => e.ToString("HH:mm:ss")).ToArray(),
+                        };
+                    }
+                    else
+                    {
+                        chart.XAxis = new
+                        {
+                            data = times.Select(e => e.ToString("dd-HH:mm")).ToArray(),
+                        };
+                    }
+                }
+                else
+                {
+                    chart.XAxis = new
+                    {
+                        data = datax.Keys.Select(e => e.ToString("HH:mm:ss")).ToArray(),
+                    };
+                }
+                chart.SetY("数值");
+
+                var max = -9999.0;
+                var min = 9999.0;
+                var dps = DeviceProperty.FindAllByDeviceId(deviceId);
+                var sample = new AverageSampling();
+                //var sample = new LTTBSampling();
+                foreach (var item in topics)
+                {
+                    var name2 = item;
+
+                    // 使用属性名
+                    var dp = dps.FirstOrDefault(e => e.Name == item);
+                    if (dp != null && !dp.NickName.IsNullOrEmpty()) name2 = dp.NickName;
+
+                    var series = new Series
+                    {
+                        Name = name2,
+                        Type = "line",
+                        //Data = tps2.Select(e => Math.Round(e.Value)).ToArray(),
+                        Smooth = true,
+                    };
+
+                    if (step > 0)
+                    {
+                        //var minD = minT.Date.ToInt();
+                        var tps = new List<TimePoint>();
+                        foreach (var elm in datax)
+                        {
+                            // 可能该Topic在这个时刻没有数据,写入空
+                            var v = elm.Value.FirstOrDefault(e => e.Name == item);
+                            if (v != null)
+                                tps.Add(new TimePoint { Time = v.CreateTime.ToInt(), Value = v.Value.ToDouble() });
+                        }
+
+                        var tps2 = sample.Process(tps.ToArray(), step);
+
+                        series.Data = tps2.Select(e => Math.Round(e.Value, 2)).ToArray();
+
+                        var m1 = tps2.Select(e => e.Value).Min();
+                        if (m1 < min) min = m1;
+                        var m2 = tps2.Select(e => e.Value).Max();
+                        if (m2 > max) max = m2;
+                    }
+                    else
+                    {
+                        var list3 = new List<Object>();
+                        foreach (var elm in datax)
+                        {
+                            // 可能该Topic在这个时刻没有数据,写入空
+                            var v = elm.Value.FirstOrDefault(e => e.Name == item);
+                            if (v != null)
+                                list3.Add(v.Value);
+                            else
+                                list3.Add('-');
+                        }
+                        series.Data = list3;
+
+                        var m1 = list3.Where(e => e + "" != "-").Select(e => e.ToDouble()).Min();
+                        if (m1 < min) min = m1;
+                        var m2 = list3.Where(e => e + "" != "-").Select(e => e.ToDouble()).Max();
+                        if (m2 > max) max = m2;
+                    }
+
+                    // 单一曲线,显示最大最小和平均
+                    if (topics.Count == 1)
+                    {
+                        name = name2;
+                        series["markPoint"] = new
+                        {
+                            data = new[] {
+                                new{ type="max",name="Max"},
+                                new{ type="min",name="Min"},
+                            }
+                        };
+                        series["markLine"] = new
+                        {
+                            data = new[] {
+                                new{ type="average",name="Avg"},
+                            }
+                        };
+                    }
+
+                    // 降采样策略 lttb/average/max/min/sum
+                    series["sampling"] = "lttb";
+                    series["symbol"] = "none";
+
+                    // 开启动画
+                    series["animation"] = true;
+
+                    chart.Add(series);
+                }
+                chart.SetTooltip();
+                chart.YAxis = new
+                {
+                    name = "数值",
+                    type = "value",
+                    min = Math.Ceiling(min) - 1,
+                    max = Math.Ceiling(max),
+                };
+                ViewBag.Charts = new[] { chart };
+
+                // 减少数据显示,避免卡页面
+                list = list.Take(100).ToList();
+
+                var ar = Device.FindById(deviceId);
+                if (ar != null) ViewBag.Title = topics.Count == 1 ? $"{name} - {ar}数据" : $"{ar}数据";
+            }
+        }
+
+        return list;
+    }
+}
\ No newline at end of file
Added +43 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs
new file mode 100644
index 0000000..ddb050d
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs
@@ -0,0 +1,43 @@
+using IoT.Data;
+using Microsoft.AspNetCore.Mvc;
+using NewLife.Cube;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+/// <summary>设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。</summary>
+[Menu(50, true, Icon = "fa-table")]
+[IoTArea]
+public class DeviceGroupController : EntityController<DeviceGroup>
+{
+    static DeviceGroupController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("UpdateUserId", "UpdateIP");
+        ListFields.RemoveCreateField().RemoveRemarkField();
+    }
+
+    /// <summary>高级搜索。列表页查询、导出Excel、导出Json、分享页等使用</summary>
+    /// <param name="p">分页器。包含分页排序参数,以及Http请求参数</param>
+    /// <returns></returns>
+    protected override IEnumerable<DeviceGroup> Search(Pager p)
+    {
+        var name = p["name"];
+        var parentid = p["parentid"].ToInt(-1);
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return DeviceGroup.Search(name, parentid, start, end, p["Q"], p);
+    }
+
+    [EntityAuthorize(PermissionFlags.Update)]
+    public ActionResult Refresh()
+    {
+        DeviceGroup.Refresh();
+
+        return JsonRefresh("成功!", 1);
+    }
+}
\ No newline at end of file
Added +34 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs
new file mode 100644
index 0000000..1da5870
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DeviceHistoryController.cs
@@ -0,0 +1,34 @@
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(60, true)]
+public class DeviceHistoryController : ReadOnlyEntityController<DeviceHistory>
+{
+    protected override IEnumerable<DeviceHistory> Search(Pager p)
+    {
+        var deviceId = p["deviceId"].ToInt(-1);
+        var action = p["action"];
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        //if (start.Year < 2000)
+        //{
+        //    start = new DateTime(DateTime.Today.Year, 1, 1);
+        //    p["dtStart"] = start.ToString("yyyy-MM-dd");
+        //}
+
+        if (start.Year < 2000)
+        {
+            using var split = DeviceHistory.Meta.CreateShard(DateTime.Today);
+            return DeviceHistory.Search(deviceId, action, start, end, p["Q"], p);
+        }
+        else
+            return DeviceHistory.Search(deviceId, action, start, end, p["Q"], p);
+    }
+}
\ No newline at end of file
Added +49 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DeviceOnlineController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DeviceOnlineController.cs
new file mode 100644
index 0000000..140afa0
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DeviceOnlineController.cs
@@ -0,0 +1,49 @@
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Cube.ViewModels;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+/// <summary>设备在线</summary>
+[Menu(70, true, Icon = "fa-table")]
+[IoTArea]
+public class DeviceOnlineController : EntityController<DeviceOnline>
+{
+    static DeviceOnlineController()
+    {
+        //LogOnChange = true;
+
+        //ListFields.RemoveField("Id", "Creator");
+        ListFields.RemoveCreateField().RemoveRemarkField();
+
+        {
+            var df = ListFields.GetField("DeviceName") as ListField;
+            df.Url = "/IoT/Device?Id={DeviceId}";
+        }
+        {
+            var df = ListFields.AddListField("property", "Pings");
+            df.DisplayName = "属性";
+            df.Url = "/IoT/DeviceProperty?deviceId={DeviceId}";
+        }
+        {
+            var df = ListFields.AddListField("data", "Pings");
+            df.DisplayName = "数据";
+            df.Url = "/IoT/DeviceData?deviceId={DeviceId}";
+        }
+    }
+
+    /// <summary>高级搜索。列表页查询、导出Excel、导出Json、分享页等使用</summary>
+    /// <param name="p">分页器。包含分页排序参数,以及Http请求参数</param>
+    /// <returns></returns>
+    protected override IEnumerable<DeviceOnline> Search(Pager p)
+    {
+        var productId = p["productId"].ToInt(-1);
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return DeviceOnline.Search(null, productId, start, end, p["Q"], p);
+    }
+}
\ No newline at end of file
Added +122 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs b/Samples/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs
new file mode 100644
index 0000000..3ba846d
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/DevicePropertyController.cs
@@ -0,0 +1,122 @@
+using System.ComponentModel;
+using IoT.Data;
+using IoTZero.Services;
+using Microsoft.AspNetCore.Mvc;
+using NewLife;
+using NewLife.Cube;
+using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
+using NewLife.IoT;
+using NewLife.IoT.ThingModels;
+using NewLife.Serialization;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(0, false)]
+public class DevicePropertyController : EntityController<DeviceProperty>
+{
+    private readonly ThingService _thingService;
+
+    static DevicePropertyController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("UnitName", "Length", "Rule", "Readonly", "Locked", "Timestamp", "FunctionId", "Remark");
+        ListFields.RemoveCreateField();
+
+        ListFields.TraceUrl("TraceId");
+
+        {
+            var df = ListFields.GetField("DeviceName") as ListField;
+            df.Url = "/IoT/Device?Id={DeviceId}";
+        }
+        {
+            var df = ListFields.GetField("Name") as ListField;
+            df.Url = "/IoT/DeviceData?deviceId={DeviceId}&name={Name}";
+        }
+        {
+            var df = ListFields.AddDataField("Value", "Unit") as ListField;
+        }
+        {
+            var df = ListFields.AddDataField("Switch", "Enable") as ListField;
+            df.DisplayName = "翻转";
+            df.Url = "/IoT/DeviceProperty/Switch?id={Id}";
+            df.DataAction = "action";
+            df.DataVisible = e => (e as DeviceProperty).Type.EqualIgnoreCase("bool");
+        }
+    }
+
+    public DevicePropertyController(ThingService thingService) => _thingService = thingService;
+
+    protected override Boolean Valid(DeviceProperty entity, DataObjectMethodType type, Boolean post)
+    {
+        var fs = type switch
+        {
+            DataObjectMethodType.Insert => AddFormFields,
+            DataObjectMethodType.Update => EditFormFields,
+            _ => null,
+        };
+
+        if (fs != null)
+        {
+            var df = fs.FirstOrDefault(e => e.Name == "Type");
+            if (df != null)
+            {
+                // 基础类型,加上所有产品类型
+                var dic = new Dictionary<String, String>(TypeHelper.GetIoTTypes(true), StringComparer.OrdinalIgnoreCase);
+
+                if (!entity.Type.IsNullOrEmpty() && !dic.ContainsKey(entity.Type)) dic[entity.Type] = entity.Type;
+                df.DataSource = e => dic;
+            }
+        }
+
+        return base.Valid(entity, type, post);
+    }
+
+    protected override IEnumerable<DeviceProperty> Search(Pager p)
+    {
+        var deviceId = p["deviceId"].ToInt(-1);
+        var name = p["name"];
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return DeviceProperty.Search(deviceId, name, start, end, p["Q"], p);
+    }
+
+    [EntityAuthorize(PermissionFlags.Insert)]
+    public async Task<ActionResult> Switch(Int32 id)
+    {
+        var msg = "";
+        var entity = DeviceProperty.FindById(id);
+        if (entity != null && entity.Enable)
+        {
+            var value = entity.Value.ToBoolean();
+            value = !value;
+            entity.Value = value + "";
+            entity.Update();
+
+            var model = new PropertyModel { Name = entity.Name, Value = value };
+
+            // 执行远程调用
+            var dp = entity;
+            if (dp != null)
+            {
+                var input = new
+                {
+                    model.Name,
+                    model.Value,
+                };
+
+                var rs = await _thingService.InvokeServiceAsync(entity.Device, "SetProperty", input.ToJson(), DateTime.Now.AddSeconds(5), 5000);
+                if (rs != null && rs.Status >= ServiceStatus.已完成)
+                    msg = $"{rs.Status} {rs.Data}";
+            }
+        }
+
+        return JsonRefresh("成功!" + msg, 1000);
+    }
+}
\ No newline at end of file
Added +41 -0
diff --git a/Samples/IoTZero/Areas/IoT/Controllers/ProductController.cs b/Samples/IoTZero/Areas/IoT/Controllers/ProductController.cs
new file mode 100644
index 0000000..ec5857c
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Controllers/ProductController.cs
@@ -0,0 +1,41 @@
+using IoT.Data;
+using NewLife.Cube;
+using NewLife.Web;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+[IoTArea]
+[Menu(30, true, Icon = "fa-product-hunt")]
+public class ProductController : EntityController<Product>
+{
+    static ProductController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("Secret", "DataFormat", "DynamicRegister", "FixedDeviceCode", "AuthType", "WhiteIP", "Remark");
+        ListFields.RemoveCreateField();
+
+        {
+            var df = ListFields.AddListField("Log");
+            df.DisplayName = "日志";
+            df.Url = "/Admin/Log?category=产品&linkId={Id}";
+        }
+    }
+
+    protected override IEnumerable<Product> Search(Pager p)
+    {
+        var id = p["Id"].ToInt(-1);
+        if (id > 0)
+        {
+            var entity = Product.FindById(id);
+            if (entity != null) return new[] { entity };
+        }
+
+        var code = p["code"];
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return Product.Search(code, start, end, p["Q"], p);
+    }
+}
\ No newline at end of file
Added +13 -0
diff --git a/Samples/IoTZero/Areas/IoT/IoTArea.cs b/Samples/IoTZero/Areas/IoT/IoTArea.cs
new file mode 100644
index 0000000..39c4fe5
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/IoTArea.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel;
+using NewLife;
+using NewLife.Cube;
+
+namespace IoTZero.Areas.IoT;
+
+[DisplayName("设备管理")]
+public class IoTArea : AreaBase
+{
+    public IoTArea() : base(nameof(IoTArea).TrimEnd("Area")) { }
+
+    static IoTArea() => RegisterArea<IoTArea>();
+}
\ No newline at end of file
Added +9 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/_ViewImports.cshtml b/Samples/IoTZero/Areas/IoT/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..e8fcb7a
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/_ViewImports.cshtml
@@ -0,0 +1,9 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@using NewLife
+@using NewLife.Cube
+@using NewLife.Cube.Extensions
+@using NewLife.Reflection
+@using NewLife.Web
+@using XCode
+@using XCode.Membership
+@using IoT.Data
\ No newline at end of file
Added +6 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/_ViewStart.cshtml b/Samples/IoTZero/Areas/IoT/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..ab02818
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/_ViewStart.cshtml
@@ -0,0 +1,6 @@
+@{
+    var theme = CubeSetting.Current.Theme;
+    if (String.IsNullOrEmpty(theme)) theme = "ACE";
+
+    Layout = "~/Views/" + theme + "/_Layout.cshtml";
+}
\ No newline at end of file
Added +14 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml b/Samples/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml
new file mode 100644
index 0000000..eadaed2
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/Device/_List_Search.cshtml
@@ -0,0 +1,14 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+<div class="form-group">
+    <label for="productId" class="control-label">产品:</label>
+    @Html.ForDropDownList("productId", Product.FindAllWithCache(), page["productId"], "全部", true)
+</div>
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +10 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml b/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml
new file mode 100644
index 0000000..730699b
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Search.cshtml
@@ -0,0 +1,10 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +16 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml b/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml
new file mode 100644
index 0000000..5115a96
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/DeviceData/_List_Toolbar_Custom.cshtml
@@ -0,0 +1,16 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+    var url = page.GetBaseUrl(true, true, false, new[] { "sample" });
+}
+<div class="form-group">
+    <a href="?@url">原始数据</a> &nbsp;
+    <a href="?@url&sample=60">每分钟</a> &nbsp;
+    <a href="?@url&sample=900">每15钟</a> &nbsp;
+    <a href="?@url&sample=3600">每小时</a> &nbsp;
+</div>
Added +9 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/DeviceGroup/_List_Toolbar_Batch.cshtml b/Samples/IoTZero/Areas/IoT/Views/DeviceGroup/_List_Toolbar_Batch.cshtml
new file mode 100644
index 0000000..897a0ec
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/DeviceGroup/_List_Toolbar_Batch.cshtml
@@ -0,0 +1,9 @@
+@using NewLife.Common;
+@{
+    var user = ViewBag.User as IUser ?? User.Identity as IUser;
+    var fact = ViewBag.Factory as IEntityFactory;
+    var set = ViewBag.PageSetting as PageSetting;
+}
+<button type="button" class="btn btn-purple btn-sm" data-action="action" data-url="@Url.Action("Refresh")" data-fields="keys">
+    刷新数据
+</button>
\ No newline at end of file
Added +10 -0
diff --git a/Samples/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml b/Samples/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml
new file mode 100644
index 0000000..730699b
--- /dev/null
+++ b/Samples/IoTZero/Areas/IoT/Views/DeviceHistory/_List_Search.cshtml
@@ -0,0 +1,10 @@
+@using NewLife;
+@using NewLife.Web;
+@using NewLife.Cube;
+@using XCode;
+@using IoT.Data;
+@{
+    var fact = ViewBag.Factory as IEntityFactory;
+    var page = ViewBag.Page as Pager;
+}
+@await Html.PartialAsync("_DateRange")
\ No newline at end of file
Added +27 -0
diff --git a/Samples/IoTZero/Clients/ClientSetting.cs b/Samples/IoTZero/Clients/ClientSetting.cs
new file mode 100644
index 0000000..749f03d
--- /dev/null
+++ b/Samples/IoTZero/Clients/ClientSetting.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using NewLife.Configuration;
+
+namespace IoTEdge;
+
+/// <summary>配置</summary>
+[Config("IoTClient")]
+public class ClientSetting : Config<ClientSetting>
+{
+    #region 属性
+    /// <summary>服务端地址。IoT服务平台地址</summary>
+    [Description("服务端地址。IoT服务平台地址")]
+    public String Server { get; set; } = "http://localhost:1880";
+
+    /// <summary>设备证书。在一机一密时手工填写,一型一密时自动下发</summary>
+    [Description("设备证书。在一机一密时手工填写,一型一密时自动下发")]
+    public String DeviceCode { get; set; }
+
+    /// <summary>设备密钥。在一机一密时手工填写,一型一密时自动下发</summary>
+    [Description("设备密钥。在一机一密时手工填写,一型一密时自动下发")]
+    public String DeviceSecret { get; set; }
+
+    /// <summary>产品证书。用于一型一密验证,对一机一密无效</summary>
+    [Description("产品证书。用于一型一密验证,对一机一密无效")]
+    public String ProductKey { get; set; } = "EdgeGateway";
+    #endregion
+}
\ No newline at end of file
Added +320 -0
diff --git a/Samples/IoTZero/Clients/HttpDevice.cs b/Samples/IoTZero/Clients/HttpDevice.cs
new file mode 100644
index 0000000..05a2ef0
--- /dev/null
+++ b/Samples/IoTZero/Clients/HttpDevice.cs
@@ -0,0 +1,320 @@
+using System.Diagnostics;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.WebSockets;
+using NewLife;
+using NewLife.Caching;
+using NewLife.IoT.Models;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Remoting;
+using NewLife.Security;
+using NewLife.Serialization;
+using NewLife.Threading;
+
+namespace IoTEdge;
+
+/// <summary>Http协议设备</summary>
+public class HttpDevice : DisposeBase
+{
+    #region 属性
+    /// <summary>服务器地址</summary>
+    public String Server { get; set; }
+
+    /// <summary>设备编码。从IoT管理平台获取(需提前分配),或者本地提交后动态注册</summary>
+    public String DeviceCode { get; set; }
+
+    /// <summary>密钥。设备密钥或产品密钥,分别用于一机一密和一型一密,从IoT管理平台获取</summary>
+    public String DeviceSecret { get; set; }
+
+    /// <summary>产品编码。从IoT管理平台获取</summary>
+    public String ProductKey { get; set; }
+
+    /// <summary>密码散列提供者。避免密码明文提交</summary>
+    public IPasswordProvider PasswordProvider { get; set; } = new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 };
+
+    /// <summary>收到命令时触发</summary>
+    public event EventHandler<ServiceEventArgs> Received;
+
+    private readonly ClientSetting _setting;
+    private ApiHttpClient _client;
+    private Int32 _delay;
+    #endregion
+
+    #region 构造
+    public HttpDevice() { }
+
+    public HttpDevice(ClientSetting setting)
+    {
+        _setting = setting;
+
+        Server = setting.Server;
+        DeviceCode = setting.DeviceCode;
+        DeviceSecret = setting.DeviceSecret;
+        ProductKey = setting.ProductKey;
+    }
+
+    protected override void Dispose(Boolean disposing)
+    {
+        base.Dispose(disposing);
+
+        StopTimer();
+    }
+    #endregion
+
+    #region 登录注销
+    /// <summary>
+    /// 登录
+    /// </summary>
+    /// <param name="inf"></param>
+    /// <returns></returns>
+    public async Task LoginAsync()
+    {
+        var client = new ApiHttpClient(Server)
+        {
+            Tracer = Tracer,
+            Log = XTrace.Log,
+        };
+
+        var info = new LoginInfo
+        {
+            Code = DeviceCode,
+            Secret = DeviceSecret.IsNullOrEmpty() ? null : PasswordProvider.Hash(DeviceSecret),
+            ProductKey = ProductKey,
+        };
+        var rs = await client.PostAsync<LoginResponse>("Device/Login", info);
+        client.Token = rs.Token;
+
+        if (!rs.Code.IsNullOrEmpty() && !rs.Secret.IsNullOrEmpty())
+        {
+            WriteLog("下发证书:{0}/{1}", rs.Code, rs.Secret);
+            DeviceCode = rs.Code;
+            DeviceSecret = rs.Secret;
+
+            _setting.DeviceCode = rs.Code;
+            _setting.DeviceSecret = rs.Secret;
+            _setting.Save();
+        }
+
+        _client = client;
+
+        StartTimer();
+    }
+
+    /// <summary>注销</summary>
+    /// <param name="reason"></param>
+    /// <returns></returns>
+    public async Task LogoutAsync(String reason) => await _client.PostAsync<LogoutResponse>("Device/Logout", new { reason });
+    #endregion
+
+    #region 心跳&长连接
+    /// <summary>心跳</summary>
+    /// <returns></returns>
+    public virtual async Task PingAsync()
+    {
+        if (Tracer != null) DefaultSpan.Current = null;
+
+        using var span = Tracer?.NewSpan("Ping");
+        try
+        {
+            var info = GetHeartInfo();
+
+            var rs = await _client.PostAsync<PingResponse>("Device/Ping", info);
+
+            var dt = rs.Time.ToDateTime();
+            if (dt.Year > 2000)
+            {
+                // 计算延迟
+                var ts = DateTime.UtcNow - dt;
+                var ms = (Int32)ts.TotalMilliseconds;
+                _delay = (_delay + ms) / 2;
+            }
+
+            var svc = _client.Services.FirstOrDefault();
+            if (svc != null && _client.Token != null && (_websocket == null || _websocket.State != WebSocketState.Open))
+            {
+                var url = svc.Address.ToString().Replace("http://", "ws://").Replace("https://", "wss://");
+                var uri = new Uri(new Uri(url), "/Device/Notify");
+                var client = new ClientWebSocket();
+                client.Options.SetRequestHeader("Authorization", "Bearer " + _client.Token);
+                await client.ConnectAsync(uri, default);
+
+                _websocket = client;
+
+                _source = new CancellationTokenSource();
+                _ = Task.Run(() => DoPull(client, _source.Token));
+            }
+
+            // 令牌
+            if (rs is PingResponse pr && !pr.Token.IsNullOrEmpty())
+                _client.Token = pr.Token;
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, null);
+
+            throw;
+        }
+    }
+
+    /// <summary>获取心跳信息</summary>
+    public PingInfo GetHeartInfo()
+    {
+        var mi = MachineInfo.GetCurrent();
+        mi.Refresh();
+
+        var properties = IPGlobalProperties.GetIPGlobalProperties();
+        var connections = properties.GetActiveTcpConnections();
+
+        var mcs = NetHelper.GetMacs().Select(e => e.ToHex("-")).OrderBy(e => e).Join(",");
+        var driveInfo = new DriveInfo(Path.GetPathRoot(".".GetFullPath()));
+        var ip = NetHelper.GetIPs().Where(ip => ip.IsIPv4() && !IPAddress.IsLoopback(ip) && ip.GetAddressBytes()[0] != 169).Join();
+        var ext = new PingInfo
+        {
+            Memory = mi.Memory,
+            AvailableMemory = mi.AvailableMemory,
+            TotalSize = (UInt64)driveInfo.TotalSize,
+            AvailableFreeSpace = (UInt64)driveInfo.AvailableFreeSpace,
+            CpuRate = (Single)Math.Round(mi.CpuRate, 4),
+            Temperature = mi.Temperature,
+            Battery = mi.Battery,
+            Uptime = Environment.TickCount / 1000,
+
+            IP = ip,
+
+            Time = DateTime.UtcNow.ToLong(),
+            Delay = _delay,
+        };
+        // 开始时间 Environment.TickCount 很容易溢出,导致开机24天后变成负数。
+        // 后来在 netcore3.0 增加了Environment.TickCount64
+        // 现在借助 Stopwatch 来解决
+        if (Stopwatch.IsHighResolution) ext.Uptime = (Int32)(Stopwatch.GetTimestamp() / Stopwatch.Frequency);
+
+        return ext;
+    }
+
+    private TimerX _timer;
+    /// <summary>开始心跳定时器</summary>
+    protected virtual void StartTimer()
+    {
+        if (_timer == null)
+            lock (this)
+                _timer ??= new TimerX(async s => await PingAsync(), null, 3_000, 60_000, "Device") { Async = true };
+    }
+
+    /// <summary>停止心跳定时器</summary>
+    protected void StopTimer()
+    {
+        if (_websocket != null && _websocket.State == WebSocketState.Open)
+            _websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "finish", default).Wait();
+        _source?.Cancel();
+
+        _websocket = null;
+
+        _timer.TryDispose();
+        _timer = null;
+    }
+
+    private WebSocket _websocket;
+    private CancellationTokenSource _source;
+    private ICache _cache = new MemoryCache();
+
+    private async Task DoPull(WebSocket socket, CancellationToken cancellationToken)
+    {
+        DefaultSpan.Current = null;
+        try
+        {
+            var buf = new Byte[4 * 1024];
+            while (!cancellationToken.IsCancellationRequested && socket.State == WebSocketState.Open)
+            {
+                var data = await socket.ReceiveAsync(new ArraySegment<Byte>(buf), cancellationToken);
+                var model = buf.ToStr(null, 0, data.Count).ToJsonEntity<ServiceModel>();
+                if (model != null && _cache.Add($"cmd:{model.Id}", model, 3600))
+                {
+                    // 建立追踪链路
+                    using var span = Tracer?.NewSpan("service:" + model.Name, model);
+                    span?.Detach(model.TraceId);
+                    try
+                    {
+                        if (model.Expire.Year < 2000 || model.Expire > DateTime.Now)
+                            await OnReceiveCommand(model);
+                        else
+                            await ServiceReply(new ServiceReplyModel { Id = model.Id, Status = ServiceStatus.取消 });
+                    }
+                    catch (Exception ex)
+                    {
+                        span?.SetError(ex, null);
+                    }
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            XTrace.WriteException(ex);
+        }
+
+        if (socket.State == WebSocketState.Open) await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "finish", default);
+    }
+    #endregion
+
+    #region 服务
+    /// <summary>
+    /// 触发收到命令的动作
+    /// </summary>
+    /// <param name="model"></param>
+    protected virtual async Task OnReceiveCommand(ServiceModel model)
+    {
+        var e = new ServiceEventArgs { Model = model };
+        Received?.Invoke(this, e);
+
+        if (e.Reply != null) await ServiceReply(e.Reply);
+    }
+
+    /// <summary>上报服务调用结果</summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    public virtual async Task<Object> ServiceReply(ServiceReplyModel model) => await _client.PostAsync<Object>("Thing/ServiceReply", model);
+    #endregion
+
+    public async Task PostDataAsync()
+    {
+        if (Tracer != null) DefaultSpan.Current = null;
+
+        using var span = Tracer?.NewSpan("PostData");
+        try
+        {
+            var items = new List<DataModel>
+            {
+                new DataModel
+                {
+                    Time = DateTime.UtcNow.ToLong(),
+                    Name = "TestValue",
+                    Value = Rand.Next(0, 100) + ""
+                }
+            };
+
+            var data = new DataModels { DeviceCode = DeviceCode, Items = items.ToArray() };
+
+            await _client.PostAsync<Int32>("Thing/PostData", data);
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, null);
+
+            throw;
+        }
+    }
+
+    #region 日志
+    /// <summary>链路追踪</summary>
+    public ITracer Tracer { get; set; }
+
+    /// <summary>日志</summary>
+    public ILog Log { get; set; }
+
+    /// <summary>写日志</summary>
+    /// <param name="format"></param>
+    /// <param name="args"></param>
+    public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
+    #endregion
+}
\ No newline at end of file
Added +67 -0
diff --git a/Samples/IoTZero/Common/ApiFilterAttribute.cs b/Samples/IoTZero/Common/ApiFilterAttribute.cs
new file mode 100644
index 0000000..44e4af4
--- /dev/null
+++ b/Samples/IoTZero/Common/ApiFilterAttribute.cs
@@ -0,0 +1,67 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using NewLife;
+using NewLife.Log;
+using NewLife.Serialization;
+
+namespace IoTZero.Common;
+
+/// <summary>统一Api过滤处理</summary>
+public sealed class ApiFilterAttribute : ActionFilterAttribute
+{
+    /// <summary>执行前,验证模型</summary>
+    /// <param name="context"></param>
+    public override void OnActionExecuting(ActionExecutingContext context)
+    {
+        if (!context.ModelState.IsValid)
+            throw new ApplicationException(context.ModelState.Values.First(p => p.Errors.Count > 0).Errors[0].ErrorMessage);
+
+        // 访问令牌
+        var request = context.HttpContext.Request;
+        var token = request.Query["Token"] + "";
+        if (token.IsNullOrEmpty()) token = (request.Headers["Authorization"] + "").TrimStart("Bearer ");
+        if (token.IsNullOrEmpty()) token = request.Headers["X-Token"] + "";
+        if (token.IsNullOrEmpty()) token = request.Cookies["Token"] + "";
+        context.HttpContext.Items["Token"] = token;
+        if (!context.ActionArguments.ContainsKey("token")) context.ActionArguments.Add("token", token);
+
+        base.OnActionExecuting(context);
+    }
+
+    /// <summary>执行后,包装结果和异常</summary>
+    /// <param name="context"></param>
+    public override void OnActionExecuted(ActionExecutedContext context)
+    {
+        var traceId = DefaultSpan.Current?.TraceId;
+
+        if (context.Result != null)
+            if (context.Result is ObjectResult obj)
+            {
+                //context.Result = new JsonResult(new { code = obj.StatusCode ?? 0, data = obj.Value });
+                var rs = new { code = obj.StatusCode ?? 0, data = obj.Value, traceId };
+                context.Result = new ContentResult
+                {
+                    Content = rs.ToJson(false, true, true),
+                    ContentType = "application/json",
+                    StatusCode = 200
+                };
+            }
+            else if (context.Result is EmptyResult)
+                context.Result = new JsonResult(new { code = 0, data = new { }, traceId });
+        else if (context.Exception != null && !context.ExceptionHandled)
+        {
+            var ex = context.Exception.GetTrue();
+            if (ex is NewLife.Remoting.ApiException aex)
+                context.Result = new JsonResult(new { code = aex.Code, data = aex.Message, traceId });
+            else
+                context.Result = new JsonResult(new { code = 500, data = ex.Message, traceId });
+
+            context.ExceptionHandled = true;
+
+            // 输出异常日志
+            if (XTrace.Debug) XTrace.WriteException(ex);
+        }
+
+        base.OnActionExecuted(context);
+    }
+}
\ No newline at end of file
Added +87 -0
diff --git a/Samples/IoTZero/Controllers/AppController.cs b/Samples/IoTZero/Controllers/AppController.cs
new file mode 100644
index 0000000..74f75c5
--- /dev/null
+++ b/Samples/IoTZero/Controllers/AppController.cs
@@ -0,0 +1,87 @@
+using IoT.Data;
+using IoTZero.Services;
+using Microsoft.AspNetCore.Mvc;
+using NewLife;
+using NewLife.IoT.Models;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Remoting;
+using NewLife.Remoting.Extensions;
+
+namespace IoTZero.Controllers;
+
+/// <summary>物模型Api控制器。用于应用系统调用</summary>
+[ApiFilter]
+[ApiController]
+[Route("[controller]")]
+public class AppController : BaseController
+{
+    private readonly MyDeviceService _deviceService;
+    private readonly ThingService _thingService;
+    private readonly ITracer _tracer;
+    private IDictionary<String, Object> _args;
+
+    #region 构造
+    /// <summary>
+    /// 实例化应用管理服务
+    /// </summary>
+    /// <param name="queue"></param>
+    /// <param name="deviceService"></param>
+    /// <param name="thingService"></param>
+    /// <param name="tracer"></param>
+    public AppController(IServiceProvider serviceProvider, MyDeviceService deviceService, ThingService thingService, ITracer tracer) : base(serviceProvider)
+    {
+        _deviceService = deviceService;
+        _thingService = thingService;
+        _tracer = tracer;
+    }
+    #endregion
+
+    #region 物模型
+    /// <summary>获取设备属性</summary>
+    /// <param name="deviceId">设备编号</param>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetProperty))]
+    public PropertyModel[] GetProperty(Int32 deviceId, String deviceCode)
+    {
+        var dv = Device.FindById(deviceId) ?? Device.FindByCode(deviceCode);
+        if (dv == null) return null;
+
+        return _thingService.QueryProperty(dv, null);
+    }
+
+    /// <summary>设置设备属性</summary>
+    /// <param name="model">数据</param>
+    /// <returns></returns>
+    [HttpPost(nameof(SetProperty))]
+    public Task<ServiceReplyModel> SetProperty(DevicePropertyModel model)
+    {
+        var dv = Device.FindByCode(model.DeviceCode);
+        if (dv == null) return null;
+
+        throw new NotImplementedException();
+    }
+
+    /// <summary>调用设备服务</summary>
+    /// <param name="service">服务</param>
+    /// <returns></returns>
+    [HttpPost(nameof(InvokeService))]
+    public async Task<ServiceReplyModel> InvokeService(ServiceRequest service)
+    {
+        Device dv = null;
+        if (service.DeviceId > 0) dv = Device.FindById(service.DeviceId);
+        if (dv == null)
+        {
+            if (!service.DeviceCode.IsNullOrWhiteSpace())
+                dv = Device.FindByCode(service.DeviceCode);
+            else
+                throw new ArgumentNullException(nameof(service.DeviceCode));
+        }
+
+        if (dv == null) throw new ArgumentException($"找不到该设备:DeviceId={service.DeviceId},DeviceCode={service.DeviceCode}");
+
+        return await _thingService.InvokeServiceAsync(dv, service.ServiceName, service.InputData, service.Expire, service.Timeout);
+    }
+    #endregion
+}
\ No newline at end of file
Added +191 -0
diff --git a/Samples/IoTZero/Controllers/DeviceController.cs b/Samples/IoTZero/Controllers/DeviceController.cs
new file mode 100644
index 0000000..e4481fe
--- /dev/null
+++ b/Samples/IoTZero/Controllers/DeviceController.cs
@@ -0,0 +1,191 @@
+using IoT.Data;
+using IoTZero.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NewLife.Http;
+using NewLife.IoT.Drivers;
+using NewLife.IoT.Models;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Remoting;
+using NewLife.Remoting.Extensions;
+using WebSocket = System.Net.WebSockets.WebSocket;
+
+namespace IoTZero.Controllers;
+
+/// <summary>设备控制器</summary>
+[ApiFilter]
+[ApiController]
+[Route("[controller]")]
+public class DeviceController : BaseController
+{
+    /// <summary>当前设备</summary>
+    public Device Device => App as Device;
+
+    private readonly QueueService _queue;
+    private readonly MyDeviceService _deviceService;
+    private readonly ThingService _thingService;
+    private readonly ITracer _tracer;
+
+    #region 构造
+    /// <summary>实例化设备控制器</summary>
+    /// <param name="serviceProvider"></param>
+    /// <param name="queue"></param>
+    /// <param name="deviceService"></param>
+    /// <param name="thingService"></param>
+    /// <param name="tracer"></param>
+    public DeviceController(IServiceProvider serviceProvider, QueueService queue, MyDeviceService deviceService, ThingService thingService, ITracer tracer) : base(serviceProvider)
+    {
+        _queue = queue;
+        _deviceService = deviceService;
+        _thingService = thingService;
+        _tracer = tracer;
+    }
+
+    #endregion
+
+    #region 登录
+    /// <summary>设备登录</summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost(nameof(Login))]
+    public LoginResponse Login(LoginInfo model) => _deviceService.Login(model, "Http", UserHost);
+
+    /// <summary>设备注销</summary>
+    /// <param name="reason">注销原因</param>
+    /// <returns></returns>
+    [HttpGet(nameof(Logout))]
+    public LogoutResponse Logout(String reason)
+    {
+        var device = Device;
+        if (device != null) _deviceService.Logout(device, reason, "Http", UserHost);
+
+        return new LogoutResponse
+        {
+            Name = device?.Name,
+            Token = null,
+        };
+    }
+    #endregion
+
+    #region 心跳
+    /// <summary>设备心跳</summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    [HttpPost(nameof(Ping))]
+    public PingResponse Ping(PingInfo model)
+    {
+        var rs = new PingResponse
+        {
+            Time = model.Time,
+            ServerTime = DateTime.UtcNow.ToLong(),
+        };
+
+        var device = Device;
+        if (device != null)
+        {
+            rs.Period = device.Period;
+
+            var olt = _deviceService.Ping(device, model, Token, UserHost);
+
+            // 令牌有效期检查,10分钟内到期的令牌,颁发新令牌。
+            // 这里将来由客户端提交刷新令牌,才能颁发新的访问令牌。
+            var tm = _deviceService.ValidAndIssueToken(device.Code, Token);
+            if (tm != null)
+            {
+                rs.Token = tm.AccessToken;
+
+                //_deviceService.WriteHistory(device, "刷新令牌", true, tm.ToJson(), UserHost);
+            }
+        }
+
+        return rs;
+    }
+
+    [HttpGet(nameof(Ping))]
+    public PingResponse Ping() => new() { Time = 0, ServerTime = DateTime.UtcNow.ToLong(), };
+    #endregion
+
+    #region 升级
+    /// <summary>升级检查</summary>
+    /// <returns></returns>
+    [HttpGet(nameof(Upgrade))]
+    public UpgradeInfo Upgrade()
+    {
+        var device = Device ?? throw new ApiException(402, "节点未登录");
+
+        throw new NotImplementedException();
+    }
+    #endregion
+
+    #region 设备通道
+    /// <summary>获取设备信息,包括主设备和子设备</summary>
+    /// <returns></returns>
+    [HttpGet(nameof(GetDevices))]
+    public DeviceModel[] GetDevices() => throw new NotImplementedException();
+
+    /// <summary>设备上线。驱动打开后调用,子设备发现,或者上报主设备/子设备的默认参数模版</summary>
+    /// <remarks>
+    /// 有些设备驱动具备扫描发现子设备能力,通过该方法上报设备。
+    /// 主设备或子设备,也可通过该方法上报驱动的默认参数模版。
+    /// 根据需要,驱动内可能多次调用该方法。
+    /// </remarks>
+    /// <param name="devices">设备信息集合。可传递参数模版</param>
+    /// <returns>返回上报信息对应的反馈,如果新增子设备,则返回子设备信息</returns>
+    [HttpPost(nameof(SetOnline))]
+    public IDeviceInfo[] SetOnline(DeviceModel[] devices) => throw new NotImplementedException();
+
+    /// <summary>设备下线。驱动内子设备变化后调用</summary>
+    /// <remarks>
+    /// 根据需要,驱动内可能多次调用该方法。
+    /// </remarks>
+    /// <param name="devices">设备编码集合。用于子设备离线</param>
+    /// <returns>返回上报信息对应的反馈,如果新增子设备,则返回子设备信息</returns>
+    [HttpPost(nameof(SetOffline))]
+    public IDeviceInfo[] SetOffline(String[] devices) => throw new NotImplementedException();
+
+    /// <summary>获取设备点位表</summary>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetPoints))]
+    public PointModel[] GetPoints(String deviceCode) => throw new NotImplementedException();
+
+    /// <summary>提交驱动信息。客户端把自己的驱动信息提交到平台</summary>
+    /// <param name="drivers"></param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostDriver))]
+    public Int32 PostDriver(DriverInfo[] drivers) => throw new NotImplementedException();
+    #endregion
+
+    #region 下行通知
+    /// <summary>下行通知</summary>
+    /// <returns></returns>
+    [HttpGet("/Device/Notify")]
+    public async Task Notify()
+    {
+        if (HttpContext.WebSockets.IsWebSocketRequest)
+        {
+            using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
+
+            await Handle(socket, Token);
+        }
+        else
+        {
+            HttpContext.Response.StatusCode = 400;
+        }
+    }
+
+    private async Task Handle(WebSocket socket, String token)
+    {
+        var device = Device ?? throw new InvalidOperationException("未登录!");
+
+        _deviceService.WriteHistory(device, "WebSocket连接", true, socket.State + "", UserHost);
+
+        var source = new CancellationTokenSource();
+        var queue = _queue.GetQueue(device.Code);
+        _ = Task.Run(() => socket.ConsumeAndPushAsync(queue, onProcess: null, source));
+        await socket.WaitForClose(null, source);
+    }
+    #endregion
+}
\ No newline at end of file
Added +159 -0
diff --git a/Samples/IoTZero/Controllers/ThingController.cs b/Samples/IoTZero/Controllers/ThingController.cs
new file mode 100644
index 0000000..725f14b
--- /dev/null
+++ b/Samples/IoTZero/Controllers/ThingController.cs
@@ -0,0 +1,159 @@
+using IoT.Data;
+using IoTZero.Services;
+using Microsoft.AspNetCore.Mvc;
+using NewLife;
+using NewLife.IoT.Models;
+using NewLife.IoT.ThingModels;
+using NewLife.IoT.ThingSpecification;
+using NewLife.Remoting.Extensions;
+
+namespace IoTZero.Controllers;
+
+/// <summary>物模型控制器</summary>
+[ApiFilter]
+[ApiController]
+[Route("[controller]")]
+public class ThingController : BaseController
+{
+    /// <summary>当前设备</summary>
+    public Device Device => App as Device;
+
+    private readonly QueueService _queue;
+    private readonly ThingService _thingService;
+
+    #region 构造
+    /// <summary>实例化物模型控制器</summary>
+    /// <param name="serviceProvider"></param>
+    /// <param name="queue"></param>
+    /// <param name="thingService"></param>
+    public ThingController(IServiceProvider serviceProvider, QueueService queue, ThingService thingService) : base(serviceProvider)
+    {
+        _queue = queue;
+        _thingService = thingService;
+    }
+
+    #endregion
+
+    #region 设备属性
+    /// <summary>上报设备属性</summary>
+    /// <param name="model">属性集合</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostProperty))]
+    public Int32 PostProperty(PropertyModels model) => throw new NotImplementedException();
+
+    /// <summary>批量上报设备属性,融合多个子设备数据批量上传</summary>
+    /// <param name="models">属性集合</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostProperties))]
+    public Int32 PostProperties(PropertyModels[] models) => throw new NotImplementedException();
+
+    /// <summary>获取设备属性</summary>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetProperty))]
+    public PropertyModel[] GetProperty(String deviceCode) => throw new NotImplementedException();
+
+    /// <summary>设备数据上报</summary>
+    /// <param name="model">模型</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostData))]
+    public Int32 PostData(DataModels model)
+    {
+        var device = GetDevice(model.DeviceCode);
+
+        return _thingService.PostData(device, model, "PostData", UserHost);
+    }
+
+    /// <summary>批量设备数据上报,融合多个子设备数据批量上传</summary>
+    /// <param name="models">模型</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostDatas))]
+    public Int32 PostDatas(DataModels[] models) => throw new NotImplementedException();
+    #endregion
+
+    #region 设备事件
+    /// <summary>设备事件上报</summary>
+    /// <param name="model">模型</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostEvent))]
+    public Int32 PostEvent(EventModels model) => throw new NotImplementedException();
+
+    /// <summary>批量设备事件上报,融合多个子设备数据批量上传</summary>
+    /// <param name="models">模型</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostEvents))]
+    public Int32 PostEvents(EventModels[] models) => throw new NotImplementedException();
+    #endregion
+
+    #region 设备服务
+    /// <summary>设备端响应服务调用</summary>
+    /// <param name="model">服务</param>
+    /// <returns></returns>
+    [HttpPost(nameof(ServiceReply))]
+    public Int32 ServiceReply(ServiceReplyModel model) => throw new NotImplementedException();
+    #endregion
+
+    #region 物模型
+    /// <summary>获取设备所属产品的物模型</summary>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetSpecification))]
+    public ThingSpec GetSpecification(String deviceCode) => throw new NotImplementedException();
+
+    /// <summary>上报物模型</summary>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostSpecification))]
+    public IPoint[] PostSpecification(ThingSpecModel model) => throw new NotImplementedException();
+    #endregion
+
+    #region 设备影子
+    /// <summary>上报设备影子</summary>
+    /// <remarks>
+    /// 设备影子是一个JSON文档,用于存储设备上报状态、应用程序期望状态信息。
+    /// 每个设备有且只有一个设备影子,设备可以通过MQTT获取和设置设备影子来同步状态,该同步可以是影子同步给设备,也可以是设备同步给影子。
+    /// 使用设备影子机制,设备状态变更,只需同步状态给设备影子一次,应用程序请求获取设备状态,不论应用程序请求数量,和设备是否联网在线,都可从设备影子中获取设备当前状态,实现应用程序与设备解耦。
+    /// </remarks>
+    /// <param name="model">数据</param>
+    /// <returns></returns>
+    [HttpPost(nameof(PostShadow))]
+    public Int32 PostShadow(ShadowModel model) => throw new NotImplementedException();
+
+    /// <summary>获取设备影子</summary>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetShadow))]
+    public String GetShadow(String deviceCode) => throw new NotImplementedException();
+    #endregion
+
+    #region 配置
+    /// <summary>设备端查询配置信息</summary>
+    /// <param name="deviceCode">设备编码</param>
+    /// <returns></returns>
+    [HttpGet(nameof(GetConfig))]
+    public IDictionary<String, Object> GetConfig(String deviceCode) => throw new NotImplementedException();
+    #endregion
+
+    #region 辅助
+
+    /// <summary>
+    /// 查找子设备
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <returns></returns>
+    protected Device GetDevice(String deviceCode)
+    {
+        var dv = Device;
+        if (dv == null) return null;
+
+        if (deviceCode.IsNullOrEmpty() || dv.Code == deviceCode) return dv;
+
+        var child = Device.FindByCode(deviceCode);
+
+        //dv = dv.Childs.FirstOrDefault(e => e.Code == deviceCode);
+        if (child == null || child.Id != dv.Id) throw new Exception($"非法设备编码,[{deviceCode}]并非当前登录设备[{Device}]的子设备");
+
+        return child;
+    }
+    #endregion
+}
\ No newline at end of file
Added +1299 -0
diff --git a/Samples/IoTZero/Entity/IoT.htm b/Samples/IoTZero/Entity/IoT.htm
new file mode 100644
index 0000000..9841b83
--- /dev/null
+++ b/Samples/IoTZero/Entity/IoT.htm
@@ -0,0 +1,1299 @@
+<style>
+    table {
+        border-collapse: collapse;
+        border: 1px solid;
+        border-color: rgb(211, 202, 221);
+    }
+
+    table thead,
+    table tr {
+        border-top-width: 1px;
+        border-top-style: solid;
+        border-top-color: rgb(211, 202, 221);
+    }
+
+    table {
+        border-bottom-width: 1px;
+        border-bottom-style: solid;
+        border-bottom-color: rgb(211, 202, 221);
+    }
+
+    table td,
+    table th {
+        padding: 5px 10px;
+        font-size: 14px;
+        font-family: Verdana;
+        color: rgb(95, 74, 121);
+    }
+
+    table tr:nth-child(even) {
+        background: rgb(223, 216, 232)
+    }
+
+    table tr:nth-child(odd) {
+        background: #FFF
+    }
+</style>
+<h3>产品(Product)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td title="自增">AI</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Code</td>
+            <td>编码</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td title="唯一索引">UQ</td>
+            <td></td>
+            <td>ProductKey</td>
+        </tr>
+
+        <tr>
+            <td>Enable</td>
+            <td>启用</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>开发中/已发布</td>
+        </tr>
+
+        <tr>
+            <td>DeviceCount</td>
+            <td>设备数量</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateUser</td>
+            <td>创建人</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateUserId</td>
+            <td>创建者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateUser</td>
+            <td>更新人</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateUserId</td>
+            <td>更新者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateTime</td>
+            <td>更新时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateIP</td>
+            <td>更新地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Remark</td>
+            <td>描述</td>
+            <td>String</td>
+            <td>500</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备(Device)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td title="自增">AI</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Code</td>
+            <td>编码</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td title="唯一索引">UQ</td>
+            <td></td>
+            <td>设备唯一证书DeviceName,用于设备认证,在注册时由系统生成</td>
+        </tr>
+
+        <tr>
+            <td>Secret</td>
+            <td>密钥</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>设备密钥DeviceSecret,用于设备认证,注册时由系统生成</td>
+        </tr>
+
+        <tr>
+            <td>ProductId</td>
+            <td>产品</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>GroupId</td>
+            <td>分组</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Enable</td>
+            <td>启用</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Online</td>
+            <td>在线</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Version</td>
+            <td>版本</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>IP</td>
+            <td>本地IP</td>
+            <td>String</td>
+            <td>200</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Uuid</td>
+            <td>唯一标识</td>
+            <td>String</td>
+            <td>200</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>硬件标识,或其它能够唯一区分设备的标记</td>
+        </tr>
+
+        <tr>
+            <td>Location</td>
+            <td>位置</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>场地安装位置,或者经纬度</td>
+        </tr>
+
+        <tr>
+            <td>Period</td>
+            <td>心跳周期</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>默认60秒</td>
+        </tr>
+
+        <tr>
+            <td>PollingTime</td>
+            <td>采集间隔</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>默认1000ms</td>
+        </tr>
+
+        <tr>
+            <td>Logins</td>
+            <td>登录次数</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>LastLogin</td>
+            <td>最后登录</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>LastLoginIP</td>
+            <td>最后IP</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>最后的公网IP地址</td>
+        </tr>
+
+        <tr>
+            <td>OnlineTime</td>
+            <td>在线时长</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>总时长,每次下线后累加,单位,秒</td>
+        </tr>
+
+        <tr>
+            <td>RegisterTime</td>
+            <td>激活时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateUserId</td>
+            <td>创建者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateUserId</td>
+            <td>更新者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateTime</td>
+            <td>更新时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateIP</td>
+            <td>更新地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Remark</td>
+            <td>描述</td>
+            <td>String</td>
+            <td>500</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备分组(DeviceGroup)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td title="自增">AI</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>ParentId</td>
+            <td>父级</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Sort</td>
+            <td>排序</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Devices</td>
+            <td>设备总数</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Activations</td>
+            <td>激活设备</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Onlines</td>
+            <td>当前在线</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateUserId</td>
+            <td>创建者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateUserId</td>
+            <td>更新者</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateTime</td>
+            <td>更新时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateIP</td>
+            <td>更新地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Remark</td>
+            <td>描述</td>
+            <td>String</td>
+            <td>500</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备在线(DeviceOnline)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td title="自增">AI</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>SessionId</td>
+            <td>会话</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td title="唯一索引">UQ</td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>ProductId</td>
+            <td>产品</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>DeviceId</td>
+            <td>设备</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>IP</td>
+            <td>本地IP</td>
+            <td>String</td>
+            <td>200</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>GroupPath</td>
+            <td>分组</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Pings</td>
+            <td>心跳</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Delay</td>
+            <td>延迟</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>网络延迟,单位ms</td>
+        </tr>
+
+        <tr>
+            <td>Offset</td>
+            <td>偏移</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>客户端时间减服务端时间,单位s</td>
+        </tr>
+
+        <tr>
+            <td>LocalTime</td>
+            <td>本地时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Token</td>
+            <td>令牌</td>
+            <td>String</td>
+            <td>200</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Creator</td>
+            <td>创建者</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>服务端设备</td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateTime</td>
+            <td>更新时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Remark</td>
+            <td>备注</td>
+            <td>String</td>
+            <td>500</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备历史(DeviceHistory)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int64</td>
+            <td></td>
+            <td></td>
+            <td title="主键">PK</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>DeviceId</td>
+            <td>设备</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Action</td>
+            <td>操作</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Success</td>
+            <td>成功</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>TraceId</td>
+            <td>追踪</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>用于记录调用链追踪标识,在APM查找调用链</td>
+        </tr>
+
+        <tr>
+            <td>Creator</td>
+            <td>创建者</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>服务端设备</td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Remark</td>
+            <td>内容</td>
+            <td>String</td>
+            <td>2000</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备属性(DeviceProperty)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td title="自增">AI</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>DeviceId</td>
+            <td>设备</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>NickName</td>
+            <td>昵称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Type</td>
+            <td>类型</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Value</td>
+            <td>数值</td>
+            <td>String</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>设备上报数值</td>
+        </tr>
+
+        <tr>
+            <td>Unit</td>
+            <td>单位</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Enable</td>
+            <td>启用</td>
+            <td>Boolean</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>TraceId</td>
+            <td>追踪</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>用于记录调用链追踪标识,在APM查找调用链</td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateTime</td>
+            <td>更新时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>UpdateIP</td>
+            <td>更新地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
+<h3>设备数据(DeviceData)</h3>
+<table>
+    <thead>
+        <tr>
+            <th>名称</th>
+            <th>显示名</th>
+            <th>类型</th>
+            <th>长度</th>
+            <th>精度</th>
+            <th>主键</th>
+            <th>允许空</th>
+            <th>备注</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>Id</td>
+            <td>编号</td>
+            <td>Int64</td>
+            <td></td>
+            <td></td>
+            <td title="主键">PK</td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>DeviceId</td>
+            <td>设备</td>
+            <td>Int32</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Name</td>
+            <td>名称</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>MQTT的Topic,或者属性名</td>
+        </tr>
+
+        <tr>
+            <td>Kind</td>
+            <td>类型</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>数据来源,如PostProperty/PostData/MqttPostData</td>
+        </tr>
+
+        <tr>
+            <td>Value</td>
+            <td>数值</td>
+            <td>String</td>
+            <td>2000</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>Timestamp</td>
+            <td>时间戳</td>
+            <td>Int64</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>N</td>
+            <td>设备生成数据时的UTC毫秒</td>
+        </tr>
+
+        <tr>
+            <td>TraceId</td>
+            <td>追踪标识</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>用于记录调用链追踪标识,在APM查找调用链</td>
+        </tr>
+
+        <tr>
+            <td>Creator</td>
+            <td>创建者</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td>服务端设备</td>
+        </tr>
+
+        <tr>
+            <td>CreateTime</td>
+            <td>创建时间</td>
+            <td>DateTime</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+
+        <tr>
+            <td>CreateIP</td>
+            <td>创建地址</td>
+            <td>String</td>
+            <td>50</td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+        </tr>
+    </tbody>
+</table>
+<br></br>
Added +206 -0
diff --git a/Samples/IoTZero/Entity/Model.xml b/Samples/IoTZero/Entity/Model.xml
new file mode 100644
index 0000000..5f49e3b
--- /dev/null
+++ b/Samples/IoTZero/Entity/Model.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EntityModel xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="https://newlifex.com https://newlifex.com/Model2023.xsd" xmlns="https://newlifex.com/Model2023.xsd">
+  <Option>
+    <!--输出目录-->
+    <Output>.\</Output>
+    <!--是否使用中文文件名。默认false-->
+    <ChineseFileName>False</ChineseFileName>
+    <!--基类。可能包含基类和接口,其中{name}替换为Table.Name-->
+    <BaseClass>Entity</BaseClass>
+    <!--命名空间-->
+    <Namespace>IoT.Data</Namespace>
+    <!--类名模板。其中{name}替换为Table.Name,如{name}Model/I{name}Dto等-->
+    <ClassNameTemplate />
+    <!--显示名模板。其中{displayName}替换为Table.DisplayName-->
+    <DisplayNameTemplate />
+    <!--用于生成拷贝函数的模型类。例如{name}或I{name}-->
+    <ModelNameForCopy />
+    <!--带有索引器。实现IModel接口-->
+    <HasIModel>False</HasIModel>
+    <!--模型类模版-->
+    <ModelClass />
+    <!--模型接口模版-->
+    <ModelInterface />
+    <!--数据库连接名-->
+    <ConnName>IoT</ConnName>
+    <!--模型类输出目录。默认当前目录的Models子目录-->
+    <ModelsOutput>.\Models\</ModelsOutput>
+    <!--模型接口输出目录。默认当前目录的Interfaces子目录-->
+    <InterfacesOutput>.\Interfaces\</InterfacesOutput>
+    <!--用户实体转为模型类的模型类。例如{name}或{name}DTO-->
+    <ModelNameForToModel />
+    <!--命名格式。Default/Upper/Lower/Underline-->
+    <NameFormat>Default</NameFormat>
+    <!--生成器版本-->
+    <Version>11.8.2023.0524</Version>
+    <!--帮助文档-->
+    <Document>https://newlifex.com/xcode/model</Document>
+    <!--魔方区域显示名-->
+    <DisplayName>设备管理</DisplayName>
+    <!--魔方控制器输出目录-->
+    <CubeOutput>../../IoTZero/Areas/IoT/</CubeOutput>
+  </Option>
+  <Table Name="Product" Description="产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Code" DataType="String" Description="编码。ProductKey" />
+      <Column Name="Enable" DataType="Boolean" Description="启用。开发中/已发布" />
+      <Column Name="DeviceCount" DataType="Int32" Description="设备数量" />
+      <Column Name="CreateUser" DataType="String" Description="创建人" Model="False" Category="扩展" />
+      <Column Name="CreateUserId" DataType="Int32" Description="创建者" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateUser" DataType="String" Description="更新人" Model="False" Category="扩展" />
+      <Column Name="UpdateUserId" DataType="Int32" Description="更新者" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+      <Column Name="Remark" DataType="String" Length="500" Description="描述" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="Code" Unique="True" />
+    </Indexes>
+  </Table>
+  <Table Name="Device" Description="设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Code" DataType="String" Description="编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成" />
+      <Column Name="Secret" DataType="String" Description="密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成" />
+      <Column Name="ProductId" DataType="Int32" Description="产品" />
+      <Column Name="GroupId" DataType="Int32" Map="DeviceGroup@Id@Name@GroupPath" Description="分组" />
+      <Column Name="Enable" DataType="Boolean" Description="启用" />
+      <Column Name="Online" DataType="Boolean" Description="在线" />
+      <Column Name="Version" DataType="String" Description="版本" />
+      <Column Name="IP" DataType="String" Length="200" Description="本地IP" />
+      <Column Name="Uuid" DataType="String" Length="200" Description="唯一标识。硬件标识,或其它能够唯一区分设备的标记" />
+      <Column Name="Location" DataType="String" Description="位置。场地安装位置,或者经纬度" Category="登录信息" />
+      <Column Name="Period" DataType="Int32" Description="心跳周期。默认60秒" Category="参数设置" />
+      <Column Name="PollingTime" DataType="Int32" Description="采集间隔。默认1000ms" Category="参数设置" />
+      <Column Name="Logins" DataType="Int32" Description="登录次数" Category="登录信息" />
+      <Column Name="LastLogin" DataType="DateTime" Description="最后登录" Category="登录信息" />
+      <Column Name="LastLoginIP" DataType="String" Description="最后IP。最后的公网IP地址" Category="登录信息" />
+      <Column Name="OnlineTime" DataType="Int32" Description="在线时长。总时长,每次下线后累加,单位,秒" Category="登录信息" />
+      <Column Name="RegisterTime" DataType="DateTime" Description="激活时间" Category="登录信息" />
+      <Column Name="CreateUserId" DataType="Int32" Description="创建者" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateUserId" DataType="Int32" Description="更新者" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+      <Column Name="Remark" DataType="String" Length="500" Description="描述" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="Code" Unique="True" />
+      <Index Columns="ProductId" />
+      <Index Columns="Uuid" />
+      <Index Columns="UpdateTime" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceGroup" Description="设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。" BaseType="EntityTree">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="ParentId" DataType="Int32" Description="父级" />
+      <Column Name="Sort" DataType="Int32" Description="排序" />
+      <Column Name="Devices" DataType="Int32" Description="设备总数" />
+      <Column Name="Activations" DataType="Int32" Description="激活设备" />
+      <Column Name="Onlines" DataType="Int32" Description="当前在线" />
+      <Column Name="CreateUserId" DataType="Int32" Description="创建者" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateUserId" DataType="Int32" Description="更新者" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+      <Column Name="Remark" DataType="String" Length="500" Description="描述" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="ParentId,Name" Unique="True" />
+      <Index Columns="Name" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceOnline" Description="设备在线">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="SessionId" DataType="String" Description="会话" />
+      <Column Name="ProductId" DataType="Int32" Description="产品" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="IP" DataType="String" Length="200" Description="本地IP" />
+      <Column Name="GroupPath" DataType="String" Description="分组" />
+      <Column Name="Pings" DataType="Int32" Description="心跳" />
+      <Column Name="Delay" DataType="Int32" Description="延迟。网络延迟,单位ms" />
+      <Column Name="Offset" DataType="Int32" Description="偏移。客户端时间减服务端时间,单位s" />
+      <Column Name="LocalTime" DataType="DateTime" Description="本地时间" />
+      <Column Name="Token" DataType="String" Length="200" Description="令牌" />
+      <Column Name="Creator" DataType="String" Description="创建者。服务端设备" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" />
+      <Column Name="Remark" DataType="String" Length="500" Description="备注" />
+    </Columns>
+    <Indexes>
+      <Index Columns="SessionId" Unique="True" />
+      <Index Columns="ProductId" />
+      <Index Columns="UpdateTime" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceHistory" Description="设备历史。记录设备上线下线等操作">
+    <Columns>
+      <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="Action" DataType="String" Description="操作" />
+      <Column Name="Success" DataType="Boolean" Description="成功" />
+      <Column Name="TraceId" DataType="String" Description="追踪。用于记录调用链追踪标识,在APM查找调用链" />
+      <Column Name="Creator" DataType="String" Description="创建者。服务端设备" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" />
+      <Column Name="Remark" DataType="String" Length="2000" Description="内容" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Id" />
+      <Index Columns="DeviceId,Action,Id" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceProperty" Description="设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表">
+    <Columns>
+      <Column Name="Id" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称" />
+      <Column Name="NickName" DataType="String" Description="昵称" />
+      <Column Name="Type" DataType="String" Description="类型" />
+      <Column Name="Value" DataType="String" Length="-1" Description="数值。设备上报数值" />
+      <Column Name="Unit" DataType="String" Description="单位" />
+      <Column Name="Enable" DataType="Boolean" Description="启用" />
+      <Column Name="TraceId" DataType="String" Description="追踪。用于记录调用链追踪标识,在APM查找调用链" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+      <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+      <Column Name="UpdateIP" DataType="String" Description="更新地址" Model="False" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Name" Unique="True" />
+      <Index Columns="UpdateTime" />
+    </Indexes>
+  </Table>
+  <Table Name="DeviceData" Description="设备数据。设备采集原始数据,按天分表存储">
+    <Columns>
+      <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+      <Column Name="DeviceId" DataType="Int32" Description="设备" />
+      <Column Name="Name" DataType="String" Master="True" Description="名称。MQTT的Topic,或者属性名" />
+      <Column Name="Kind" DataType="String" Description="类型。数据来源,如PostProperty/PostData/MqttPostData" />
+      <Column Name="Value" DataType="String" Length="2000" Description="数值" />
+      <Column Name="Timestamp" DataType="Int64" Description="时间戳。设备生成数据时的UTC毫秒" />
+      <Column Name="TraceId" DataType="String" Description="追踪标识。用于记录调用链追踪标识,在APM查找调用链" Model="False" Category="扩展" />
+      <Column Name="Creator" DataType="String" Description="创建者。服务端设备" Model="False" Category="扩展" />
+      <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+      <Column Name="CreateIP" DataType="String" Description="创建地址" Model="False" Category="扩展" />
+    </Columns>
+    <Indexes>
+      <Index Columns="DeviceId,Id" />
+      <Index Columns="DeviceId,Name,Id" />
+      <Index Columns="DeviceId,Kind,Id" />
+    </Indexes>
+  </Table>
+</EntityModel>
\ No newline at end of file
Added +0 -0
diff --git a/Samples/IoTZero/Entity/xcodetool.exe b/Samples/IoTZero/Entity/xcodetool.exe
new file mode 100644
index 0000000..51c2c0b
Binary files /dev/null and b/Samples/IoTZero/Entity/xcodetool.exe differ
Added +138 -0
diff --git "a/Samples/IoTZero/Entity/\344\272\247\345\223\201.Biz.cs" "b/Samples/IoTZero/Entity/\344\272\247\345\223\201.Biz.cs"
new file mode 100644
index 0000000..c4a792a
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\344\272\247\345\223\201.Biz.cs"
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using NewLife;
+using NewLife.Common;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Net;
+using XCode;
+using XCode.Membership;
+
+namespace IoT.Data;
+
+public partial class Product : Entity<Product>
+{
+    #region 对象操作
+    static Product()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        //var df = Meta.Factory.AdditionalFields;
+        //df.Add(nameof(DeviceCount));
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<UserModule>();
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 自动编码
+        if (Code.IsNullOrEmpty()) Code = PinYin.GetFirst(Name);
+        CheckExist(nameof(Code));
+    }
+
+    /// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    protected override void InitData()
+    {
+        // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+        if (Meta.Session.Count > 0) return;
+
+        if (XTrace.Debug) XTrace.WriteLine("开始初始化Product[产品]数据……");
+
+        var entity = new Product
+        {
+            Name = "边缘网关",
+            Code = "EdgeGateway",
+            Enable = true,
+        };
+        entity.Insert();
+
+        if (XTrace.Debug) XTrace.WriteLine("完成初始化Product[产品]数据!");
+    }
+    #endregion
+
+    #region 扩展属性
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static Product FindById(Int32 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据编码查找</summary>
+    /// <param name="code">编码</param>
+    /// <returns>实体对象</returns>
+    public static Product FindByCode(String code)
+    {
+        if (code.IsNullOrEmpty()) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Code.EqualIgnoreCase(code));
+
+        return Find(_.Code == code);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="code">编码。ProductKey</param>
+    /// <param name="start">更新时间开始</param>
+    /// <param name="end">更新时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<Product> Search(String code, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (!code.IsNullOrEmpty()) exp &= _.Code == code;
+        exp &= _.UpdateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.Code.Contains(key) | _.CreateUser.Contains(key) | _.CreateIP.Contains(key) | _.UpdateUser.Contains(key) | _.UpdateIP.Contains(key) | _.Remark.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From Product Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<Product> _CategoryCache = new FieldCache<Product>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+    /// <summary>更新设备所属产品下辖设备数量</summary>
+    public void Fix()
+    {
+        DeviceCount = Device.FindAllByProductId(Id).Count;
+
+        Update();
+    }
+    #endregion
+}
Added +293 -0
diff --git "a/Samples/IoTZero/Entity/\344\272\247\345\223\201.cs" "b/Samples/IoTZero/Entity/\344\272\247\345\223\201.cs"
new file mode 100644
index 0000000..4967f94
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\344\272\247\345\223\201.cs"
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。</summary>
+[Serializable]
+[DataObject]
+[Description("产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。")]
+[BindIndex("IU_Product_Code", true, "Code")]
+[BindTable("Product", Description = "产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class Product
+{
+    #region 属性
+    private Int32 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, true, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _Code;
+    /// <summary>编码。ProductKey</summary>
+    [DisplayName("编码")]
+    [Description("编码。ProductKey")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Code", "编码。ProductKey", "")]
+    public String Code { get => _Code; set { if (OnPropertyChanging("Code", value)) { _Code = value; OnPropertyChanged("Code"); } } }
+
+    private Boolean _Enable;
+    /// <summary>启用。开发中/已发布</summary>
+    [DisplayName("启用")]
+    [Description("启用。开发中/已发布")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Enable", "启用。开发中/已发布", "")]
+    public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+
+    private Int32 _DeviceCount;
+    /// <summary>设备数量</summary>
+    [DisplayName("设备数量")]
+    [Description("设备数量")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("DeviceCount", "设备数量", "")]
+    public Int32 DeviceCount { get => _DeviceCount; set { if (OnPropertyChanging("DeviceCount", value)) { _DeviceCount = value; OnPropertyChanged("DeviceCount"); } } }
+
+    private String _CreateUser;
+    /// <summary>创建人</summary>
+    [Category("扩展")]
+    [DisplayName("创建人")]
+    [Description("创建人")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateUser", "创建人", "")]
+    public String CreateUser { get => _CreateUser; set { if (OnPropertyChanging("CreateUser", value)) { _CreateUser = value; OnPropertyChanged("CreateUser"); } } }
+
+    private Int32 _CreateUserId;
+    /// <summary>创建者</summary>
+    [Category("扩展")]
+    [DisplayName("创建者")]
+    [Description("创建者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("CreateUserId", "创建者", "")]
+    public Int32 CreateUserId { get => _CreateUserId; set { if (OnPropertyChanging("CreateUserId", value)) { _CreateUserId = value; OnPropertyChanged("CreateUserId"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [Category("扩展")]
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [Category("扩展")]
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private String _UpdateUser;
+    /// <summary>更新人</summary>
+    [Category("扩展")]
+    [DisplayName("更新人")]
+    [Description("更新人")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("UpdateUser", "更新人", "")]
+    public String UpdateUser { get => _UpdateUser; set { if (OnPropertyChanging("UpdateUser", value)) { _UpdateUser = value; OnPropertyChanged("UpdateUser"); } } }
+
+    private Int32 _UpdateUserId;
+    /// <summary>更新者</summary>
+    [Category("扩展")]
+    [DisplayName("更新者")]
+    [Description("更新者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("UpdateUserId", "更新者", "")]
+    public Int32 UpdateUserId { get => _UpdateUserId; set { if (OnPropertyChanging("UpdateUserId", value)) { _UpdateUserId = value; OnPropertyChanged("UpdateUserId"); } } }
+
+    private DateTime _UpdateTime;
+    /// <summary>更新时间</summary>
+    [Category("扩展")]
+    [DisplayName("更新时间")]
+    [Description("更新时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("UpdateTime", "更新时间", "")]
+    public DateTime UpdateTime { get => _UpdateTime; set { if (OnPropertyChanging("UpdateTime", value)) { _UpdateTime = value; OnPropertyChanged("UpdateTime"); } } }
+
+    private String _UpdateIP;
+    /// <summary>更新地址</summary>
+    [Category("扩展")]
+    [DisplayName("更新地址")]
+    [Description("更新地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("UpdateIP", "更新地址", "")]
+    public String UpdateIP { get => _UpdateIP; set { if (OnPropertyChanging("UpdateIP", value)) { _UpdateIP = value; OnPropertyChanged("UpdateIP"); } } }
+
+    private String _Remark;
+    /// <summary>描述</summary>
+    [Category("扩展")]
+    [DisplayName("描述")]
+    [Description("描述")]
+    [DataObjectField(false, false, true, 500)]
+    [BindColumn("Remark", "描述", "")]
+    public String Remark { get => _Remark; set { if (OnPropertyChanging("Remark", value)) { _Remark = value; OnPropertyChanged("Remark"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "Name" => _Name,
+            "Code" => _Code,
+            "Enable" => _Enable,
+            "DeviceCount" => _DeviceCount,
+            "CreateUser" => _CreateUser,
+            "CreateUserId" => _CreateUserId,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "UpdateUser" => _UpdateUser,
+            "UpdateUserId" => _UpdateUserId,
+            "UpdateTime" => _UpdateTime,
+            "UpdateIP" => _UpdateIP,
+            "Remark" => _Remark,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "Code": _Code = Convert.ToString(value); break;
+                case "Enable": _Enable = value.ToBoolean(); break;
+                case "DeviceCount": _DeviceCount = value.ToInt(); break;
+                case "CreateUser": _CreateUser = Convert.ToString(value); break;
+                case "CreateUserId": _CreateUserId = value.ToInt(); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "UpdateUser": _UpdateUser = Convert.ToString(value); break;
+                case "UpdateUserId": _UpdateUserId = value.ToInt(); break;
+                case "UpdateTime": _UpdateTime = value.ToDateTime(); break;
+                case "UpdateIP": _UpdateIP = Convert.ToString(value); break;
+                case "Remark": _Remark = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得产品字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>编码。ProductKey</summary>
+        public static readonly Field Code = FindByName("Code");
+
+        /// <summary>启用。开发中/已发布</summary>
+        public static readonly Field Enable = FindByName("Enable");
+
+        /// <summary>设备数量</summary>
+        public static readonly Field DeviceCount = FindByName("DeviceCount");
+
+        /// <summary>创建人</summary>
+        public static readonly Field CreateUser = FindByName("CreateUser");
+
+        /// <summary>创建者</summary>
+        public static readonly Field CreateUserId = FindByName("CreateUserId");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>更新人</summary>
+        public static readonly Field UpdateUser = FindByName("UpdateUser");
+
+        /// <summary>更新者</summary>
+        public static readonly Field UpdateUserId = FindByName("UpdateUserId");
+
+        /// <summary>更新时间</summary>
+        public static readonly Field UpdateTime = FindByName("UpdateTime");
+
+        /// <summary>更新地址</summary>
+        public static readonly Field UpdateIP = FindByName("UpdateIP");
+
+        /// <summary>描述</summary>
+        public static readonly Field Remark = FindByName("Remark");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得产品字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>编码。ProductKey</summary>
+        public const String Code = "Code";
+
+        /// <summary>启用。开发中/已发布</summary>
+        public const String Enable = "Enable";
+
+        /// <summary>设备数量</summary>
+        public const String DeviceCount = "DeviceCount";
+
+        /// <summary>创建人</summary>
+        public const String CreateUser = "CreateUser";
+
+        /// <summary>创建者</summary>
+        public const String CreateUserId = "CreateUserId";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>更新人</summary>
+        public const String UpdateUser = "UpdateUser";
+
+        /// <summary>更新者</summary>
+        public const String UpdateUserId = "UpdateUserId";
+
+        /// <summary>更新时间</summary>
+        public const String UpdateTime = "UpdateTime";
+
+        /// <summary>更新地址</summary>
+        public const String UpdateIP = "UpdateIP";
+
+        /// <summary>描述</summary>
+        public const String Remark = "Remark";
+    }
+    #endregion
+}
Added +279 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207.Biz.cs"
new file mode 100644
index 0000000..add4f96
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207.Biz.cs"
@@ -0,0 +1,279 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Common;
+using NewLife.Data;
+using NewLife.IoT.Models;
+using NewLife.Log;
+using NewLife.Remoting;
+using XCode;
+using XCode.Cache;
+using XCode.Membership;
+
+namespace IoT.Data;
+
+public partial class Device : Entity<Device>
+{
+    #region 对象操作
+    static Device()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        var df = Meta.Factory.AdditionalFields;
+        df.Add(nameof(Logins));
+        df.Add(nameof(OnlineTime));
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<UserModule>();
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+
+        var sc = Meta.SingleCache;
+        sc.Expire = 20 * 60;
+        sc.MaxEntity = 200_000;
+        sc.FindSlaveKeyMethod = k => Find(_.Code == k);
+        sc.GetSlaveKeyMethod = e => e.Code;
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        if (ProductId <= 0) throw new ApiException(500, "产品Id错误");
+
+        var product = Product.FindById(ProductId);
+        if (product == null) throw new ApiException(500, "产品Id错误");
+
+        var len = _.IP.Length;
+        if (len > 0 && !IP.IsNullOrEmpty() && IP.Length > len) IP = IP[..len];
+
+        len = _.Uuid.Length;
+        if (len > 0 && !Uuid.IsNullOrEmpty() && Uuid.Length > len) Uuid = Uuid[..len];
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 自动编码
+        if (Code.IsNullOrEmpty()) Code = PinYin.GetFirst(Name);
+        CheckExist(nameof(Code));
+
+        if (Period <= 0) Period = 60;
+        if (PollingTime == 0) PollingTime = 1000;
+    }
+
+    /// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    protected override void InitData()
+    {
+        // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+        if (Meta.Session.Count > 0) return;
+
+        if (XTrace.Debug) XTrace.WriteLine("开始初始化Device[设备]数据……");
+
+        var entity = new Device
+        {
+            Name = "测试设备",
+            Code = "abc",
+            Secret = "abc",
+            ProductId = 1,
+            GroupId = 1,
+            Enable = true,
+        };
+        entity.Insert();
+
+        if (XTrace.Debug) XTrace.WriteLine("完成初始化Device[设备]数据!");
+    }
+    #endregion
+
+    #region 扩展属性
+    /// <summary>产品</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Product Product => Extends.Get(nameof(Product), k => Product.FindById(ProductId));
+
+    /// <summary>产品</summary>
+    [Map(nameof(ProductId), typeof(Product), "Id")]
+    public String ProductName => Product?.Name;
+
+    /// <summary>设备属性。借助扩展属性缓存</summary>
+    [XmlIgnore, IgnoreDataMember]
+    public IList<DeviceProperty> Properties => Extends.Get(nameof(Properties), k => DeviceProperty.FindAllByDeviceId(Id));
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static Device FindById(Int32 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据编码查找</summary>
+    /// <param name="code">编码</param>
+    /// <returns>实体对象</returns>
+    public static Device FindByCode(String code)
+    {
+        if (code.IsNullOrEmpty()) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Code.EqualIgnoreCase(code));
+
+        //return Find(_.Code == code);
+        return Meta.SingleCache.GetItemWithSlaveKey(code) as Device;
+    }
+
+    /// <summary>根据产品查找</summary>
+    /// <param name="productId">产品</param>
+    /// <returns>实体列表</returns>
+    public static IList<Device> FindAllByProductId(Int32 productId)
+    {
+        if (productId <= 0) return new List<Device>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.ProductId == productId);
+
+        return FindAll(_.ProductId == productId);
+    }
+
+    /// <summary>根据唯一标识查找</summary>
+    /// <param name="uuid">唯一标识</param>
+    /// <returns>实体列表</returns>
+    public static IList<Device> FindAllByUuid(String uuid)
+    {
+        if (uuid.IsNullOrEmpty()) return new List<Device>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.Uuid.EqualIgnoreCase(uuid));
+
+        return FindAll(_.Uuid == uuid);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="productId">产品</param>
+    /// <param name="groupId"></param>
+    /// <param name="enable"></param>
+    /// <param name="start">更新时间开始</param>
+    /// <param name="end">更新时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<Device> Search(Int32 productId, Int32 groupId, Boolean? enable, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (productId >= 0) exp &= _.ProductId == productId;
+        if (groupId >= 0) exp &= _.GroupId == groupId;
+        if (enable != null) exp &= _.Enable == enable;
+        exp &= _.UpdateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.Code.Contains(key) | _.Uuid.Contains(key) | _.Location.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key) | _.Remark.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Uuid From Device Where CreateTime>'2020-01-24 00:00:00' Group By Uuid Order By Id Desc limit 20
+    static readonly FieldCache<Device> _UuidCache = new FieldCache<Device>(nameof(Uuid))
+    {
+        //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    };
+
+    /// <summary>获取唯一标识列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    /// <returns></returns>
+    public static IDictionary<String, String> GetUuidList() => _UuidCache.FindAllName();
+
+    /// <summary>
+    /// 根据设备分组来分组
+    /// </summary>
+    /// <returns></returns>
+    public static IList<Device> SearchGroupByGroup()
+    {
+        var selects = _.Id.Count();
+        selects &= _.Enable.SumCase(1, "Activations");
+        selects &= _.Online.SumCase(1, "Onlines");
+        selects &= _.GroupId;
+
+        return FindAll(_.GroupId.GroupBy(), null, selects, 0, 0);
+    }
+    #endregion
+
+    #region 业务操作
+
+    /// <summary>登录并保存信息</summary>
+    /// <param name="di"></param>
+    /// <param name="ip"></param>
+    public void Login(LoginInfo di, String ip)
+    {
+        var dv = this;
+
+        if (di != null) dv.Fill(di);
+
+        // 如果节点本地IP为空,而来源IP是局域网,则直接取用
+        if (dv.IP.IsNullOrEmpty()) dv.IP = ip;
+
+        dv.Online = true;
+        dv.Logins++;
+        dv.LastLogin = DateTime.Now;
+        dv.LastLoginIP = ip;
+
+        if (dv.CreateIP.IsNullOrEmpty()) dv.CreateIP = ip;
+        dv.UpdateIP = ip;
+
+        dv.Save();
+    }
+
+    /// <summary>设备上线</summary>
+    /// <param name="ip"></param>
+    /// <param name="reason"></param>
+    public void SetOnline(String ip, String reason)
+    {
+        var dv = this;
+
+        if (!dv.Online && dv.Enable)
+        {
+            dv.Online = true;
+            dv.Update();
+
+            if (!reason.IsNullOrEmpty())
+                DeviceHistory.Create(dv, "上线", true, $"设备上线。{reason}", null, ip, null);
+        }
+    }
+
+    /// <summary>
+    /// 注销
+    /// </summary>
+    public void Logout()
+    {
+        Online = false;
+
+        Update();
+    }
+
+    /// <summary>填充</summary>
+    /// <param name="di"></param>
+    public void Fill(LoginInfo di)
+    {
+        var dv = this;
+
+        if (dv.Name.IsNullOrEmpty()) dv.Name = di.Name;
+        if (!di.Version.IsNullOrEmpty()) dv.Version = di.Version;
+
+        if (!di.IP.IsNullOrEmpty()) dv.IP = di.IP;
+        if (!di.UUID.IsNullOrEmpty()) dv.Uuid = di.UUID;
+    }
+    #endregion
+}
Added +502 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207.cs"
new file mode 100644
index 0000000..4c4d8c3
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207.cs"
@@ -0,0 +1,502 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。</summary>
+[Serializable]
+[DataObject]
+[Description("设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。")]
+[BindIndex("IU_Device_Code", true, "Code")]
+[BindIndex("IX_Device_ProductId", false, "ProductId")]
+[BindIndex("IX_Device_Uuid", false, "Uuid")]
+[BindIndex("IX_Device_UpdateTime", false, "UpdateTime")]
+[BindTable("Device", Description = "设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class Device
+{
+    #region 属性
+    private Int32 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, true, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _Code;
+    /// <summary>编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成</summary>
+    [DisplayName("编码")]
+    [Description("编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Code", "编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成", "")]
+    public String Code { get => _Code; set { if (OnPropertyChanging("Code", value)) { _Code = value; OnPropertyChanged("Code"); } } }
+
+    private String _Secret;
+    /// <summary>密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成</summary>
+    [DisplayName("密钥")]
+    [Description("密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Secret", "密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成", "")]
+    public String Secret { get => _Secret; set { if (OnPropertyChanging("Secret", value)) { _Secret = value; OnPropertyChanged("Secret"); } } }
+
+    private Int32 _ProductId;
+    /// <summary>产品</summary>
+    [DisplayName("产品")]
+    [Description("产品")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("ProductId", "产品", "")]
+    public Int32 ProductId { get => _ProductId; set { if (OnPropertyChanging("ProductId", value)) { _ProductId = value; OnPropertyChanged("ProductId"); } } }
+
+    private Int32 _GroupId;
+    /// <summary>分组</summary>
+    [DisplayName("分组")]
+    [Description("分组")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("GroupId", "分组", "")]
+    public Int32 GroupId { get => _GroupId; set { if (OnPropertyChanging("GroupId", value)) { _GroupId = value; OnPropertyChanged("GroupId"); } } }
+
+    private Boolean _Enable;
+    /// <summary>启用</summary>
+    [DisplayName("启用")]
+    [Description("启用")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Enable", "启用", "")]
+    public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+
+    private Boolean _Online;
+    /// <summary>在线</summary>
+    [DisplayName("在线")]
+    [Description("在线")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Online", "在线", "")]
+    public Boolean Online { get => _Online; set { if (OnPropertyChanging("Online", value)) { _Online = value; OnPropertyChanged("Online"); } } }
+
+    private String _Version;
+    /// <summary>版本</summary>
+    [DisplayName("版本")]
+    [Description("版本")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Version", "版本", "")]
+    public String Version { get => _Version; set { if (OnPropertyChanging("Version", value)) { _Version = value; OnPropertyChanged("Version"); } } }
+
+    private String _IP;
+    /// <summary>本地IP</summary>
+    [DisplayName("本地IP")]
+    [Description("本地IP")]
+    [DataObjectField(false, false, true, 200)]
+    [BindColumn("IP", "本地IP", "")]
+    public String IP { get => _IP; set { if (OnPropertyChanging("IP", value)) { _IP = value; OnPropertyChanged("IP"); } } }
+
+    private String _Uuid;
+    /// <summary>唯一标识。硬件标识,或其它能够唯一区分设备的标记</summary>
+    [DisplayName("唯一标识")]
+    [Description("唯一标识。硬件标识,或其它能够唯一区分设备的标记")]
+    [DataObjectField(false, false, true, 200)]
+    [BindColumn("Uuid", "唯一标识。硬件标识,或其它能够唯一区分设备的标记", "")]
+    public String Uuid { get => _Uuid; set { if (OnPropertyChanging("Uuid", value)) { _Uuid = value; OnPropertyChanged("Uuid"); } } }
+
+    private String _Location;
+    /// <summary>位置。场地安装位置,或者经纬度</summary>
+    [Category("登录信息")]
+    [DisplayName("位置")]
+    [Description("位置。场地安装位置,或者经纬度")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Location", "位置。场地安装位置,或者经纬度", "")]
+    public String Location { get => _Location; set { if (OnPropertyChanging("Location", value)) { _Location = value; OnPropertyChanged("Location"); } } }
+
+    private Int32 _Period;
+    /// <summary>心跳周期。默认60秒</summary>
+    [Category("参数设置")]
+    [DisplayName("心跳周期")]
+    [Description("心跳周期。默认60秒")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Period", "心跳周期。默认60秒", "")]
+    public Int32 Period { get => _Period; set { if (OnPropertyChanging("Period", value)) { _Period = value; OnPropertyChanged("Period"); } } }
+
+    private Int32 _PollingTime;
+    /// <summary>采集间隔。默认1000ms</summary>
+    [Category("参数设置")]
+    [DisplayName("采集间隔")]
+    [Description("采集间隔。默认1000ms")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("PollingTime", "采集间隔。默认1000ms", "")]
+    public Int32 PollingTime { get => _PollingTime; set { if (OnPropertyChanging("PollingTime", value)) { _PollingTime = value; OnPropertyChanged("PollingTime"); } } }
+
+    private Int32 _Logins;
+    /// <summary>登录次数</summary>
+    [Category("登录信息")]
+    [DisplayName("登录次数")]
+    [Description("登录次数")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Logins", "登录次数", "")]
+    public Int32 Logins { get => _Logins; set { if (OnPropertyChanging("Logins", value)) { _Logins = value; OnPropertyChanged("Logins"); } } }
+
+    private DateTime _LastLogin;
+    /// <summary>最后登录</summary>
+    [Category("登录信息")]
+    [DisplayName("最后登录")]
+    [Description("最后登录")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("LastLogin", "最后登录", "")]
+    public DateTime LastLogin { get => _LastLogin; set { if (OnPropertyChanging("LastLogin", value)) { _LastLogin = value; OnPropertyChanged("LastLogin"); } } }
+
+    private String _LastLoginIP;
+    /// <summary>最后IP。最后的公网IP地址</summary>
+    [Category("登录信息")]
+    [DisplayName("最后IP")]
+    [Description("最后IP。最后的公网IP地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("LastLoginIP", "最后IP。最后的公网IP地址", "")]
+    public String LastLoginIP { get => _LastLoginIP; set { if (OnPropertyChanging("LastLoginIP", value)) { _LastLoginIP = value; OnPropertyChanged("LastLoginIP"); } } }
+
+    private Int32 _OnlineTime;
+    /// <summary>在线时长。总时长,每次下线后累加,单位,秒</summary>
+    [Category("登录信息")]
+    [DisplayName("在线时长")]
+    [Description("在线时长。总时长,每次下线后累加,单位,秒")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("OnlineTime", "在线时长。总时长,每次下线后累加,单位,秒", "")]
+    public Int32 OnlineTime { get => _OnlineTime; set { if (OnPropertyChanging("OnlineTime", value)) { _OnlineTime = value; OnPropertyChanged("OnlineTime"); } } }
+
+    private DateTime _RegisterTime;
+    /// <summary>激活时间</summary>
+    [Category("登录信息")]
+    [DisplayName("激活时间")]
+    [Description("激活时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("RegisterTime", "激活时间", "")]
+    public DateTime RegisterTime { get => _RegisterTime; set { if (OnPropertyChanging("RegisterTime", value)) { _RegisterTime = value; OnPropertyChanged("RegisterTime"); } } }
+
+    private Int32 _CreateUserId;
+    /// <summary>创建者</summary>
+    [Category("扩展")]
+    [DisplayName("创建者")]
+    [Description("创建者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("CreateUserId", "创建者", "")]
+    public Int32 CreateUserId { get => _CreateUserId; set { if (OnPropertyChanging("CreateUserId", value)) { _CreateUserId = value; OnPropertyChanged("CreateUserId"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [Category("扩展")]
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [Category("扩展")]
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private Int32 _UpdateUserId;
+    /// <summary>更新者</summary>
+    [Category("扩展")]
+    [DisplayName("更新者")]
+    [Description("更新者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("UpdateUserId", "更新者", "")]
+    public Int32 UpdateUserId { get => _UpdateUserId; set { if (OnPropertyChanging("UpdateUserId", value)) { _UpdateUserId = value; OnPropertyChanged("UpdateUserId"); } } }
+
+    private DateTime _UpdateTime;
+    /// <summary>更新时间</summary>
+    [Category("扩展")]
+    [DisplayName("更新时间")]
+    [Description("更新时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("UpdateTime", "更新时间", "")]
+    public DateTime UpdateTime { get => _UpdateTime; set { if (OnPropertyChanging("UpdateTime", value)) { _UpdateTime = value; OnPropertyChanged("UpdateTime"); } } }
+
+    private String _UpdateIP;
+    /// <summary>更新地址</summary>
+    [Category("扩展")]
+    [DisplayName("更新地址")]
+    [Description("更新地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("UpdateIP", "更新地址", "")]
+    public String UpdateIP { get => _UpdateIP; set { if (OnPropertyChanging("UpdateIP", value)) { _UpdateIP = value; OnPropertyChanged("UpdateIP"); } } }
+
+    private String _Remark;
+    /// <summary>描述</summary>
+    [Category("扩展")]
+    [DisplayName("描述")]
+    [Description("描述")]
+    [DataObjectField(false, false, true, 500)]
+    [BindColumn("Remark", "描述", "")]
+    public String Remark { get => _Remark; set { if (OnPropertyChanging("Remark", value)) { _Remark = value; OnPropertyChanged("Remark"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "Name" => _Name,
+            "Code" => _Code,
+            "Secret" => _Secret,
+            "ProductId" => _ProductId,
+            "GroupId" => _GroupId,
+            "Enable" => _Enable,
+            "Online" => _Online,
+            "Version" => _Version,
+            "IP" => _IP,
+            "Uuid" => _Uuid,
+            "Location" => _Location,
+            "Period" => _Period,
+            "PollingTime" => _PollingTime,
+            "Logins" => _Logins,
+            "LastLogin" => _LastLogin,
+            "LastLoginIP" => _LastLoginIP,
+            "OnlineTime" => _OnlineTime,
+            "RegisterTime" => _RegisterTime,
+            "CreateUserId" => _CreateUserId,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "UpdateUserId" => _UpdateUserId,
+            "UpdateTime" => _UpdateTime,
+            "UpdateIP" => _UpdateIP,
+            "Remark" => _Remark,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "Code": _Code = Convert.ToString(value); break;
+                case "Secret": _Secret = Convert.ToString(value); break;
+                case "ProductId": _ProductId = value.ToInt(); break;
+                case "GroupId": _GroupId = value.ToInt(); break;
+                case "Enable": _Enable = value.ToBoolean(); break;
+                case "Online": _Online = value.ToBoolean(); break;
+                case "Version": _Version = Convert.ToString(value); break;
+                case "IP": _IP = Convert.ToString(value); break;
+                case "Uuid": _Uuid = Convert.ToString(value); break;
+                case "Location": _Location = Convert.ToString(value); break;
+                case "Period": _Period = value.ToInt(); break;
+                case "PollingTime": _PollingTime = value.ToInt(); break;
+                case "Logins": _Logins = value.ToInt(); break;
+                case "LastLogin": _LastLogin = value.ToDateTime(); break;
+                case "LastLoginIP": _LastLoginIP = Convert.ToString(value); break;
+                case "OnlineTime": _OnlineTime = value.ToInt(); break;
+                case "RegisterTime": _RegisterTime = value.ToDateTime(); break;
+                case "CreateUserId": _CreateUserId = value.ToInt(); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "UpdateUserId": _UpdateUserId = value.ToInt(); break;
+                case "UpdateTime": _UpdateTime = value.ToDateTime(); break;
+                case "UpdateIP": _UpdateIP = Convert.ToString(value); break;
+                case "Remark": _Remark = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    /// <summary>分组</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public DeviceGroup Group => Extends.Get(nameof(Group), k => DeviceGroup.FindById(GroupId));
+
+    /// <summary>分组</summary>
+    [Map(nameof(GroupId), typeof(DeviceGroup), "Id")]
+    public String GroupPath => Group?.Name;
+
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成</summary>
+        public static readonly Field Code = FindByName("Code");
+
+        /// <summary>密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成</summary>
+        public static readonly Field Secret = FindByName("Secret");
+
+        /// <summary>产品</summary>
+        public static readonly Field ProductId = FindByName("ProductId");
+
+        /// <summary>分组</summary>
+        public static readonly Field GroupId = FindByName("GroupId");
+
+        /// <summary>启用</summary>
+        public static readonly Field Enable = FindByName("Enable");
+
+        /// <summary>在线</summary>
+        public static readonly Field Online = FindByName("Online");
+
+        /// <summary>版本</summary>
+        public static readonly Field Version = FindByName("Version");
+
+        /// <summary>本地IP</summary>
+        public static readonly Field IP = FindByName("IP");
+
+        /// <summary>唯一标识。硬件标识,或其它能够唯一区分设备的标记</summary>
+        public static readonly Field Uuid = FindByName("Uuid");
+
+        /// <summary>位置。场地安装位置,或者经纬度</summary>
+        public static readonly Field Location = FindByName("Location");
+
+        /// <summary>心跳周期。默认60秒</summary>
+        public static readonly Field Period = FindByName("Period");
+
+        /// <summary>采集间隔。默认1000ms</summary>
+        public static readonly Field PollingTime = FindByName("PollingTime");
+
+        /// <summary>登录次数</summary>
+        public static readonly Field Logins = FindByName("Logins");
+
+        /// <summary>最后登录</summary>
+        public static readonly Field LastLogin = FindByName("LastLogin");
+
+        /// <summary>最后IP。最后的公网IP地址</summary>
+        public static readonly Field LastLoginIP = FindByName("LastLoginIP");
+
+        /// <summary>在线时长。总时长,每次下线后累加,单位,秒</summary>
+        public static readonly Field OnlineTime = FindByName("OnlineTime");
+
+        /// <summary>激活时间</summary>
+        public static readonly Field RegisterTime = FindByName("RegisterTime");
+
+        /// <summary>创建者</summary>
+        public static readonly Field CreateUserId = FindByName("CreateUserId");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>更新者</summary>
+        public static readonly Field UpdateUserId = FindByName("UpdateUserId");
+
+        /// <summary>更新时间</summary>
+        public static readonly Field UpdateTime = FindByName("UpdateTime");
+
+        /// <summary>更新地址</summary>
+        public static readonly Field UpdateIP = FindByName("UpdateIP");
+
+        /// <summary>描述</summary>
+        public static readonly Field Remark = FindByName("Remark");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成</summary>
+        public const String Code = "Code";
+
+        /// <summary>密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成</summary>
+        public const String Secret = "Secret";
+
+        /// <summary>产品</summary>
+        public const String ProductId = "ProductId";
+
+        /// <summary>分组</summary>
+        public const String GroupId = "GroupId";
+
+        /// <summary>启用</summary>
+        public const String Enable = "Enable";
+
+        /// <summary>在线</summary>
+        public const String Online = "Online";
+
+        /// <summary>版本</summary>
+        public const String Version = "Version";
+
+        /// <summary>本地IP</summary>
+        public const String IP = "IP";
+
+        /// <summary>唯一标识。硬件标识,或其它能够唯一区分设备的标记</summary>
+        public const String Uuid = "Uuid";
+
+        /// <summary>位置。场地安装位置,或者经纬度</summary>
+        public const String Location = "Location";
+
+        /// <summary>心跳周期。默认60秒</summary>
+        public const String Period = "Period";
+
+        /// <summary>采集间隔。默认1000ms</summary>
+        public const String PollingTime = "PollingTime";
+
+        /// <summary>登录次数</summary>
+        public const String Logins = "Logins";
+
+        /// <summary>最后登录</summary>
+        public const String LastLogin = "LastLogin";
+
+        /// <summary>最后IP。最后的公网IP地址</summary>
+        public const String LastLoginIP = "LastLoginIP";
+
+        /// <summary>在线时长。总时长,每次下线后累加,单位,秒</summary>
+        public const String OnlineTime = "OnlineTime";
+
+        /// <summary>激活时间</summary>
+        public const String RegisterTime = "RegisterTime";
+
+        /// <summary>创建者</summary>
+        public const String CreateUserId = "CreateUserId";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>更新者</summary>
+        public const String UpdateUserId = "UpdateUserId";
+
+        /// <summary>更新时间</summary>
+        public const String UpdateTime = "UpdateTime";
+
+        /// <summary>更新地址</summary>
+        public const String UpdateIP = "UpdateIP";
+
+        /// <summary>描述</summary>
+        public const String Remark = "Remark";
+    }
+    #endregion
+}
Added +181 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs"
new file mode 100644
index 0000000..ef5fe95
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs"
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.Reflection;
+using NewLife.Remoting;
+using NewLife.Threading;
+using NewLife.Web;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+using XCode.Membership;
+using XCode.Shards;
+
+namespace IoT.Data;
+
+public partial class DeviceGroup : Entity<DeviceGroup>
+{
+    #region 对象操作
+    static DeviceGroup()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        //var df = Meta.Factory.AdditionalFields;
+        //df.Add(nameof(ParentId));
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<UserModule>();
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        if (Name.IsNullOrEmpty()) throw new ApiException(500, "名称不能为空");
+    }
+
+    /// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    protected override void InitData()
+    {
+        // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+        if (Meta.Session.Count > 0) return;
+
+        if (XTrace.Debug) XTrace.WriteLine("开始初始化DeviceGroup[设备分组]数据……");
+
+        var entity = new DeviceGroup
+        {
+            Name = "默认分组",
+            ParentId = 0,
+            Devices = 0,
+            Activations = 0,
+            Onlines = 0,
+        };
+        entity.Insert();
+
+        if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceGroup[设备分组]数据!");
+    }
+    #endregion
+
+    #region 扩展属性
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static DeviceGroup FindById(Int32 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据父级、名称查找</summary>
+    /// <param name="parentId">父级</param>
+    /// <param name="name">名称</param>
+    /// <returns>实体对象</returns>
+    public static DeviceGroup FindByParentIdAndName(Int32 parentId, String name)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.ParentId == parentId && e.Name.EqualIgnoreCase(name));
+
+        return Find(_.ParentId == parentId & _.Name == name);
+    }
+
+    /// <summary>根据名称查找</summary>
+    /// <param name="name">名称</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceGroup> FindAllByName(String name)
+    {
+        if (name.IsNullOrEmpty()) return new List<DeviceGroup>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.Name.EqualIgnoreCase(name));
+
+        return FindAll(_.Name == name);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="name">名称</param>
+    /// <param name="parentId">父级</param>
+    /// <param name="start">更新时间开始</param>
+    /// <param name="end">更新时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceGroup> Search(String name, Int32 parentId, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (!name.IsNullOrEmpty()) exp &= _.Name == name;
+        if (parentId >= 0) exp &= _.ParentId == parentId;
+        exp &= _.UpdateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key) | _.Remark.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From DeviceGroup Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<DeviceGroup> _CategoryCache = new FieldCache<DeviceGroup>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+
+    public static Int32 Refresh()
+    {
+        var count = 0;
+        var groups = FindAll();
+        var list = Device.SearchGroupByGroup();
+        foreach (var item in list)
+        {
+            var gb = groups.FirstOrDefault(e => e.Id == item.GroupId);
+            if (gb != null)
+            {
+                gb.Devices = item.Id;
+                gb.Activations = item["Activations"].ToInt();
+                gb.Onlines = item["Onlines"].ToInt();
+                count += gb.Update();
+            }
+        }
+
+        return count;
+    }
+    #endregion
+}
Added +292 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.cs"
new file mode 100644
index 0000000..cac90b5
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.cs"
@@ -0,0 +1,292 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。</summary>
+[Serializable]
+[DataObject]
+[Description("设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。")]
+[BindIndex("IU_DeviceGroup_ParentId_Name", true, "ParentId,Name")]
+[BindIndex("IX_DeviceGroup_Name", false, "Name")]
+[BindTable("DeviceGroup", Description = "设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class DeviceGroup
+{
+    #region 属性
+    private Int32 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, true, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private Int32 _ParentId;
+    /// <summary>父级</summary>
+    [DisplayName("父级")]
+    [Description("父级")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("ParentId", "父级", "")]
+    public Int32 ParentId { get => _ParentId; set { if (OnPropertyChanging("ParentId", value)) { _ParentId = value; OnPropertyChanged("ParentId"); } } }
+
+    private Int32 _Sort;
+    /// <summary>排序</summary>
+    [DisplayName("排序")]
+    [Description("排序")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Sort", "排序", "")]
+    public Int32 Sort { get => _Sort; set { if (OnPropertyChanging("Sort", value)) { _Sort = value; OnPropertyChanged("Sort"); } } }
+
+    private Int32 _Devices;
+    /// <summary>设备总数</summary>
+    [DisplayName("设备总数")]
+    [Description("设备总数")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Devices", "设备总数", "")]
+    public Int32 Devices { get => _Devices; set { if (OnPropertyChanging("Devices", value)) { _Devices = value; OnPropertyChanged("Devices"); } } }
+
+    private Int32 _Activations;
+    /// <summary>激活设备</summary>
+    [DisplayName("激活设备")]
+    [Description("激活设备")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Activations", "激活设备", "")]
+    public Int32 Activations { get => _Activations; set { if (OnPropertyChanging("Activations", value)) { _Activations = value; OnPropertyChanged("Activations"); } } }
+
+    private Int32 _Onlines;
+    /// <summary>当前在线</summary>
+    [DisplayName("当前在线")]
+    [Description("当前在线")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Onlines", "当前在线", "")]
+    public Int32 Onlines { get => _Onlines; set { if (OnPropertyChanging("Onlines", value)) { _Onlines = value; OnPropertyChanged("Onlines"); } } }
+
+    private Int32 _CreateUserId;
+    /// <summary>创建者</summary>
+    [Category("扩展")]
+    [DisplayName("创建者")]
+    [Description("创建者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("CreateUserId", "创建者", "")]
+    public Int32 CreateUserId { get => _CreateUserId; set { if (OnPropertyChanging("CreateUserId", value)) { _CreateUserId = value; OnPropertyChanged("CreateUserId"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [Category("扩展")]
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [Category("扩展")]
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private Int32 _UpdateUserId;
+    /// <summary>更新者</summary>
+    [Category("扩展")]
+    [DisplayName("更新者")]
+    [Description("更新者")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("UpdateUserId", "更新者", "")]
+    public Int32 UpdateUserId { get => _UpdateUserId; set { if (OnPropertyChanging("UpdateUserId", value)) { _UpdateUserId = value; OnPropertyChanged("UpdateUserId"); } } }
+
+    private DateTime _UpdateTime;
+    /// <summary>更新时间</summary>
+    [Category("扩展")]
+    [DisplayName("更新时间")]
+    [Description("更新时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("UpdateTime", "更新时间", "")]
+    public DateTime UpdateTime { get => _UpdateTime; set { if (OnPropertyChanging("UpdateTime", value)) { _UpdateTime = value; OnPropertyChanged("UpdateTime"); } } }
+
+    private String _UpdateIP;
+    /// <summary>更新地址</summary>
+    [Category("扩展")]
+    [DisplayName("更新地址")]
+    [Description("更新地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("UpdateIP", "更新地址", "")]
+    public String UpdateIP { get => _UpdateIP; set { if (OnPropertyChanging("UpdateIP", value)) { _UpdateIP = value; OnPropertyChanged("UpdateIP"); } } }
+
+    private String _Remark;
+    /// <summary>描述</summary>
+    [Category("扩展")]
+    [DisplayName("描述")]
+    [Description("描述")]
+    [DataObjectField(false, false, true, 500)]
+    [BindColumn("Remark", "描述", "")]
+    public String Remark { get => _Remark; set { if (OnPropertyChanging("Remark", value)) { _Remark = value; OnPropertyChanged("Remark"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "Name" => _Name,
+            "ParentId" => _ParentId,
+            "Sort" => _Sort,
+            "Devices" => _Devices,
+            "Activations" => _Activations,
+            "Onlines" => _Onlines,
+            "CreateUserId" => _CreateUserId,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "UpdateUserId" => _UpdateUserId,
+            "UpdateTime" => _UpdateTime,
+            "UpdateIP" => _UpdateIP,
+            "Remark" => _Remark,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "ParentId": _ParentId = value.ToInt(); break;
+                case "Sort": _Sort = value.ToInt(); break;
+                case "Devices": _Devices = value.ToInt(); break;
+                case "Activations": _Activations = value.ToInt(); break;
+                case "Onlines": _Onlines = value.ToInt(); break;
+                case "CreateUserId": _CreateUserId = value.ToInt(); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "UpdateUserId": _UpdateUserId = value.ToInt(); break;
+                case "UpdateTime": _UpdateTime = value.ToDateTime(); break;
+                case "UpdateIP": _UpdateIP = Convert.ToString(value); break;
+                case "Remark": _Remark = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备分组字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>父级</summary>
+        public static readonly Field ParentId = FindByName("ParentId");
+
+        /// <summary>排序</summary>
+        public static readonly Field Sort = FindByName("Sort");
+
+        /// <summary>设备总数</summary>
+        public static readonly Field Devices = FindByName("Devices");
+
+        /// <summary>激活设备</summary>
+        public static readonly Field Activations = FindByName("Activations");
+
+        /// <summary>当前在线</summary>
+        public static readonly Field Onlines = FindByName("Onlines");
+
+        /// <summary>创建者</summary>
+        public static readonly Field CreateUserId = FindByName("CreateUserId");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>更新者</summary>
+        public static readonly Field UpdateUserId = FindByName("UpdateUserId");
+
+        /// <summary>更新时间</summary>
+        public static readonly Field UpdateTime = FindByName("UpdateTime");
+
+        /// <summary>更新地址</summary>
+        public static readonly Field UpdateIP = FindByName("UpdateIP");
+
+        /// <summary>描述</summary>
+        public static readonly Field Remark = FindByName("Remark");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备分组字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>父级</summary>
+        public const String ParentId = "ParentId";
+
+        /// <summary>排序</summary>
+        public const String Sort = "Sort";
+
+        /// <summary>设备总数</summary>
+        public const String Devices = "Devices";
+
+        /// <summary>激活设备</summary>
+        public const String Activations = "Activations";
+
+        /// <summary>当前在线</summary>
+        public const String Onlines = "Onlines";
+
+        /// <summary>创建者</summary>
+        public const String CreateUserId = "CreateUserId";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>更新者</summary>
+        public const String UpdateUserId = "UpdateUserId";
+
+        /// <summary>更新时间</summary>
+        public const String UpdateTime = "UpdateTime";
+
+        /// <summary>更新地址</summary>
+        public const String UpdateIP = "UpdateIP";
+
+        /// <summary>描述</summary>
+        public const String Remark = "Remark";
+    }
+    #endregion
+}
Added +249 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.Biz.cs"
new file mode 100644
index 0000000..9ce53ae
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.Biz.cs"
@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Caching;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.Reflection;
+using NewLife.Threading;
+using NewLife.Web;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+using XCode.Membership;
+using XCode.Shards;
+
+namespace IoT.Data;
+
+public partial class DeviceHistory : Entity<DeviceHistory>
+{
+    #region 对象操作
+    static DeviceHistory()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        //var df = Meta.Factory.AdditionalFields;
+        //df.Add(nameof(DeviceId));
+        // 按天分表
+        //Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
+        //{
+        //    TablePolicy = "{{0}}_{{1:yyyyMMdd}}",
+        //    Step = TimeSpan.FromDays(1),
+        //};
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+        Meta.Modules.Add<TraceModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 在新插入数据或者修改了指定字段时进行修正
+        //if (isNew && !Dirtys[nameof(CreateTime)]) CreateTime = DateTime.Now;
+        //if (isNew && !Dirtys[nameof(CreateIP)]) CreateIP = ManageProvider.UserHost;
+    }
+
+    ///// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    //[EditorBrowsable(EditorBrowsableState.Never)]
+    //protected override void InitData()
+    //{
+    //    // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+    //    if (Meta.Session.Count > 0) return;
+
+    //    if (XTrace.Debug) XTrace.WriteLine("开始初始化DeviceHistory[设备历史]数据……");
+
+    //    var entity = new DeviceHistory();
+    //    entity.Id = 0;
+    //    entity.DeviceId = 0;
+    //    entity.Name = "abc";
+    //    entity.Action = "abc";
+    //    entity.Success = true;
+    //    entity.TraceId = "abc";
+    //    entity.Creator = "abc";
+    //    entity.CreateTime = DateTime.Now;
+    //    entity.CreateIP = "abc";
+    //    entity.Remark = "abc";
+    //    entity.Insert();
+
+    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceHistory[设备历史]数据!");
+    //}
+
+    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
+    ///// <returns></returns>
+    //public override Int32 Insert()
+    //{
+    //    return base.Insert();
+    //}
+
+    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
+    ///// <returns></returns>
+    //protected override Int32 OnDelete()
+    //{
+    //    return base.OnDelete();
+    //}
+    #endregion
+
+    #region 扩展属性
+    /// <summary>设备</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId));
+
+    /// <summary>设备</summary>
+    [Map(nameof(DeviceId), typeof(Device), "Id")]
+    public String DeviceName => Device?.Name;
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static DeviceHistory FindById(Int64 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据设备、编号查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="id">编号</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceHistory> FindAllByDeviceIdAndId(Int32 deviceId, Int64 id)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId && e.Id == id);
+
+        return FindAll(_.DeviceId == deviceId & _.Id == id);
+    }
+
+    /// <summary>根据设备查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceHistory> FindAllByDeviceId(Int32 deviceId)
+    {
+        if (deviceId <= 0) return new List<DeviceHistory>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId);
+
+        return FindAll(_.DeviceId == deviceId);
+    }
+
+    /// <summary>根据设备、操作查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="action">操作</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceHistory> FindAllByDeviceIdAndAction(Int32 deviceId, String action)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId && e.Action.EqualIgnoreCase(action));
+
+        return FindAll(_.DeviceId == deviceId & _.Action == action);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="action">操作</param>
+    /// <param name="start">创建时间开始</param>
+    /// <param name="end">创建时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceHistory> Search(Int32 deviceId, String action, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (deviceId >= 0) exp &= _.DeviceId == deviceId;
+        if (!action.IsNullOrEmpty()) exp &= _.Action == action;
+        exp &= _.CreateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.Action.Contains(key) | _.TraceId.Contains(key) | _.Creator.Contains(key) | _.CreateIP.Contains(key) | _.Remark.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From DeviceHistory Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<DeviceHistory> _CategoryCache = new FieldCache<DeviceHistory>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+    /// <summary>删除指定日期之前的数据</summary>
+    /// <param name="date"></param>
+    /// <returns></returns>
+    public static Int32 DeleteBefore(DateTime date) => Delete(_.Id < Meta.Factory.Snow.GetId(date));
+
+    /// <summary>创建日志</summary>
+    /// <param name="device"></param>
+    /// <param name="action"></param>
+    /// <param name="success"></param>
+    /// <param name="remark"></param>
+    /// <param name="creator"></param>
+    /// <param name="ip"></param>
+    /// <param name="traceId"></param>
+    /// <returns></returns>
+    public static DeviceHistory Create(Device device, String action, Boolean success, String remark, String creator, String ip, String traceId)
+    {
+        if (device == null) device = new Device();
+
+        if (creator.IsNullOrEmpty()) creator = Environment.MachineName;
+        if (traceId.IsNullOrEmpty()) traceId = DefaultSpan.Current?.TraceId;
+        var history = new DeviceHistory
+        {
+            DeviceId = device.Id,
+            Name = device.Name,
+            Action = action,
+            Success = success,
+
+            Remark = remark,
+
+            TraceId = traceId,
+            Creator = creator,
+            CreateTime = DateTime.Now,
+            CreateIP = ip,
+        };
+
+        history.SaveAsync();
+
+        return history;
+    }
+
+    private static readonly Lazy<FieldCache<DeviceHistory>> NameCache = new(() => new FieldCache<DeviceHistory>(__.Action));
+    /// <summary>获取所有分类名称</summary>
+    /// <returns></returns>
+    public static IDictionary<String, String> FindAllAction() => NameCache.Value.FindAllName();
+    #endregion
+}
\ No newline at end of file
Added +221 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.cs"
new file mode 100644
index 0000000..71b3976
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\216\206\345\217\262.cs"
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备历史。记录设备上线下线等操作</summary>
+[Serializable]
+[DataObject]
+[Description("设备历史。记录设备上线下线等操作")]
+[BindIndex("IX_DeviceHistory_DeviceId_Id", false, "DeviceId,Id")]
+[BindIndex("IX_DeviceHistory_DeviceId_Action_Id", false, "DeviceId,Action,Id")]
+[BindTable("DeviceHistory", Description = "设备历史。记录设备上线下线等操作", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class DeviceHistory
+{
+    #region 属性
+    private Int64 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, false, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int64 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private Int32 _DeviceId;
+    /// <summary>设备</summary>
+    [DisplayName("设备")]
+    [Description("设备")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("DeviceId", "设备", "")]
+    public Int32 DeviceId { get => _DeviceId; set { if (OnPropertyChanging("DeviceId", value)) { _DeviceId = value; OnPropertyChanged("DeviceId"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _Action;
+    /// <summary>操作</summary>
+    [DisplayName("操作")]
+    [Description("操作")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Action", "操作", "")]
+    public String Action { get => _Action; set { if (OnPropertyChanging("Action", value)) { _Action = value; OnPropertyChanged("Action"); } } }
+
+    private Boolean _Success;
+    /// <summary>成功</summary>
+    [DisplayName("成功")]
+    [Description("成功")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Success", "成功", "")]
+    public Boolean Success { get => _Success; set { if (OnPropertyChanging("Success", value)) { _Success = value; OnPropertyChanged("Success"); } } }
+
+    private String _TraceId;
+    /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+    [DisplayName("追踪")]
+    [Description("追踪。用于记录调用链追踪标识,在APM查找调用链")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("TraceId", "追踪。用于记录调用链追踪标识,在APM查找调用链", "")]
+    public String TraceId { get => _TraceId; set { if (OnPropertyChanging("TraceId", value)) { _TraceId = value; OnPropertyChanged("TraceId"); } } }
+
+    private String _Creator;
+    /// <summary>创建者。服务端设备</summary>
+    [DisplayName("创建者")]
+    [Description("创建者。服务端设备")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Creator", "创建者。服务端设备", "")]
+    public String Creator { get => _Creator; set { if (OnPropertyChanging("Creator", value)) { _Creator = value; OnPropertyChanged("Creator"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private String _Remark;
+    /// <summary>内容</summary>
+    [DisplayName("内容")]
+    [Description("内容")]
+    [DataObjectField(false, false, true, 2000)]
+    [BindColumn("Remark", "内容", "")]
+    public String Remark { get => _Remark; set { if (OnPropertyChanging("Remark", value)) { _Remark = value; OnPropertyChanged("Remark"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "DeviceId" => _DeviceId,
+            "Name" => _Name,
+            "Action" => _Action,
+            "Success" => _Success,
+            "TraceId" => _TraceId,
+            "Creator" => _Creator,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "Remark" => _Remark,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToLong(); break;
+                case "DeviceId": _DeviceId = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "Action": _Action = Convert.ToString(value); break;
+                case "Success": _Success = value.ToBoolean(); break;
+                case "TraceId": _TraceId = Convert.ToString(value); break;
+                case "Creator": _Creator = Convert.ToString(value); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "Remark": _Remark = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备历史字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>设备</summary>
+        public static readonly Field DeviceId = FindByName("DeviceId");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>操作</summary>
+        public static readonly Field Action = FindByName("Action");
+
+        /// <summary>成功</summary>
+        public static readonly Field Success = FindByName("Success");
+
+        /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public static readonly Field TraceId = FindByName("TraceId");
+
+        /// <summary>创建者。服务端设备</summary>
+        public static readonly Field Creator = FindByName("Creator");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>内容</summary>
+        public static readonly Field Remark = FindByName("Remark");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备历史字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>设备</summary>
+        public const String DeviceId = "DeviceId";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>操作</summary>
+        public const String Action = "Action";
+
+        /// <summary>成功</summary>
+        public const String Success = "Success";
+
+        /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public const String TraceId = "TraceId";
+
+        /// <summary>创建者。服务端设备</summary>
+        public const String Creator = "Creator";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>内容</summary>
+        public const String Remark = "Remark";
+    }
+    #endregion
+}
Added +284 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs"
new file mode 100644
index 0000000..fdd13af
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.Biz.cs"
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using NewLife.IoT.Models;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.Reflection;
+using NewLife.Serialization;
+using NewLife.Threading;
+using NewLife.Web;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+using XCode.Membership;
+using XCode.Shards;
+
+namespace IoT.Data;
+
+public partial class DeviceOnline : Entity<DeviceOnline>
+{
+    #region 对象操作
+    static DeviceOnline()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        //var df = Meta.Factory.AdditionalFields;
+        //df.Add(nameof(ProductId));
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 在新插入数据或者修改了指定字段时进行修正
+        //if (isNew && !Dirtys[nameof(CreateTime)]) CreateTime = DateTime.Now;
+        //if (!Dirtys[nameof(UpdateTime)]) UpdateTime = DateTime.Now;
+        //if (isNew && !Dirtys[nameof(CreateIP)]) CreateIP = ManageProvider.UserHost;
+
+        // 检查唯一索引
+        // CheckExist(isNew, nameof(SessionId));
+    }
+
+    ///// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    //[EditorBrowsable(EditorBrowsableState.Never)]
+    //protected override void InitData()
+    //{
+    //    // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+    //    if (Meta.Session.Count > 0) return;
+
+    //    if (XTrace.Debug) XTrace.WriteLine("开始初始化DeviceOnline[设备在线]数据……");
+
+    //    var entity = new DeviceOnline();
+    //    entity.SessionId = "abc";
+    //    entity.ProductId = 0;
+    //    entity.DeviceId = 0;
+    //    entity.Name = "abc";
+    //    entity.IP = "abc";
+    //    entity.GroupPath = "abc";
+    //    entity.Pings = 0;
+    //    entity.Delay = 0;
+    //    entity.Offset = 0;
+    //    entity.LocalTime = DateTime.Now;
+    //    entity.Token = "abc";
+    //    entity.Creator = "abc";
+    //    entity.CreateTime = DateTime.Now;
+    //    entity.CreateIP = "abc";
+    //    entity.UpdateTime = DateTime.Now;
+    //    entity.Remark = "abc";
+    //    entity.Insert();
+
+    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceOnline[设备在线]数据!");
+    //}
+
+    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
+    ///// <returns></returns>
+    //public override Int32 Insert()
+    //{
+    //    return base.Insert();
+    //}
+
+    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
+    ///// <returns></returns>
+    //protected override Int32 OnDelete()
+    //{
+    //    return base.OnDelete();
+    //}
+    #endregion
+
+    #region 扩展属性
+    /// <summary>产品</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Product Product => Extends.Get(nameof(Product), k => Product.FindById(ProductId));
+
+    /// <summary>产品</summary>
+    [Map(nameof(ProductId), typeof(Product), "Id")]
+    public String ProductName => Product?.Name;
+    /// <summary>设备</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId));
+
+    /// <summary>设备</summary>
+    [Map(nameof(DeviceId), typeof(Device), "Id")]
+    public String DeviceName => Device?.Name;
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static DeviceOnline FindById(Int32 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据会话查找</summary>
+    /// <param name="sessionId">会话</param>
+    /// <returns>实体对象</returns>
+    public static DeviceOnline FindBySessionId(String sessionId)
+    {
+        if (sessionId.IsNullOrEmpty()) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.SessionId.EqualIgnoreCase(sessionId));
+
+        return Find(_.SessionId == sessionId);
+    }
+
+    /// <summary>根据产品查找</summary>
+    /// <param name="productId">产品</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceOnline> FindAllByProductId(Int32 productId)
+    {
+        if (productId <= 0) return new List<DeviceOnline>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.ProductId == productId);
+
+        return FindAll(_.ProductId == productId);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="sessionId">会话</param>
+    /// <param name="productId">产品</param>
+    /// <param name="start">更新时间开始</param>
+    /// <param name="end">更新时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceOnline> Search(String sessionId, Int32 productId, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (!sessionId.IsNullOrEmpty()) exp &= _.SessionId == sessionId;
+        if (productId >= 0) exp &= _.ProductId == productId;
+        exp &= _.UpdateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.SessionId.Contains(key) | _.Name.Contains(key) | _.IP.Contains(key) | _.GroupPath.Contains(key) | _.Token.Contains(key) | _.Creator.Contains(key) | _.CreateIP.Contains(key) | _.Remark.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From DeviceOnline Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<DeviceOnline> _CategoryCache = new FieldCache<DeviceOnline>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+    /// <summary>根据编码查询或添加</summary>
+    /// <param name="sessionid"></param>
+    /// <returns></returns>
+    public static DeviceOnline GetOrAdd(String sessionid) => GetOrAdd(sessionid, FindBySessionId, k => new DeviceOnline { SessionId = k });
+
+    /// <summary>删除过期,指定过期时间</summary>
+    /// <param name="expire">超时时间,秒</param>
+    /// <returns></returns>
+    public static IList<DeviceOnline> ClearExpire(TimeSpan expire)
+    {
+        if (Meta.Count == 0) return null;
+
+        // 10分钟不活跃将会被删除
+        var exp = _.UpdateTime < DateTime.Now.Subtract(expire);
+        var list = FindAll(exp, null, null, 0, 0);
+        list.Delete();
+
+        return list;
+    }
+
+    /// <summary>更新并保存在线状态</summary>
+    /// <param name="di"></param>
+    /// <param name="pi"></param>
+    /// <param name="token"></param>
+    public void Save(LoginInfo di, PingInfo pi, String token)
+    {
+        var olt = this;
+
+        // di不等于空,登录时调用;
+        // pi不为空,客户端发ping消息是调用;
+        // 两个都是空,收到mqtt协议ping报文时调用
+        if (di != null)
+        {
+            olt.Fill(di);
+            olt.LocalTime = di.Time.ToDateTime().ToLocalTime();
+        }
+        else if (pi != null)
+        {
+            olt.Fill(pi);
+        }
+
+        olt.Token = token;
+        olt.Pings++;
+
+        // 5秒内直接保存
+        if (olt.CreateTime.AddSeconds(5) > DateTime.Now)
+            olt.Save();
+        else
+            olt.SaveAsync();
+    }
+
+    /// <summary>填充节点信息</summary>
+    /// <param name="di"></param>
+    public void Fill(LoginInfo di)
+    {
+        var online = this;
+
+        online.LocalTime = di.Time.ToDateTime().ToLocalTime();
+        online.IP = di.IP;
+    }
+
+    /// <summary>填充在线节点信息</summary>
+    /// <param name="inf"></param>
+    private void Fill(PingInfo inf)
+    {
+        var olt = this;
+
+        if (inf.Delay > 0) olt.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);
+        }
+
+        if (!inf.IP.IsNullOrEmpty()) olt.IP = inf.IP;
+        olt.Remark = inf.ToJson();
+    }
+    #endregion
+}
Added +334 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.cs"
new file mode 100644
index 0000000..23168ac
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\234\250\347\272\277.cs"
@@ -0,0 +1,334 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备在线</summary>
+[Serializable]
+[DataObject]
+[Description("设备在线")]
+[BindIndex("IU_DeviceOnline_SessionId", true, "SessionId")]
+[BindIndex("IX_DeviceOnline_ProductId", false, "ProductId")]
+[BindIndex("IX_DeviceOnline_UpdateTime", false, "UpdateTime")]
+[BindTable("DeviceOnline", Description = "设备在线", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class DeviceOnline
+{
+    #region 属性
+    private Int32 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, true, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private String _SessionId;
+    /// <summary>会话</summary>
+    [DisplayName("会话")]
+    [Description("会话")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("SessionId", "会话", "")]
+    public String SessionId { get => _SessionId; set { if (OnPropertyChanging("SessionId", value)) { _SessionId = value; OnPropertyChanged("SessionId"); } } }
+
+    private Int32 _ProductId;
+    /// <summary>产品</summary>
+    [DisplayName("产品")]
+    [Description("产品")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("ProductId", "产品", "")]
+    public Int32 ProductId { get => _ProductId; set { if (OnPropertyChanging("ProductId", value)) { _ProductId = value; OnPropertyChanged("ProductId"); } } }
+
+    private Int32 _DeviceId;
+    /// <summary>设备</summary>
+    [DisplayName("设备")]
+    [Description("设备")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("DeviceId", "设备", "")]
+    public Int32 DeviceId { get => _DeviceId; set { if (OnPropertyChanging("DeviceId", value)) { _DeviceId = value; OnPropertyChanged("DeviceId"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _IP;
+    /// <summary>本地IP</summary>
+    [DisplayName("本地IP")]
+    [Description("本地IP")]
+    [DataObjectField(false, false, true, 200)]
+    [BindColumn("IP", "本地IP", "")]
+    public String IP { get => _IP; set { if (OnPropertyChanging("IP", value)) { _IP = value; OnPropertyChanged("IP"); } } }
+
+    private String _GroupPath;
+    /// <summary>分组</summary>
+    [DisplayName("分组")]
+    [Description("分组")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("GroupPath", "分组", "")]
+    public String GroupPath { get => _GroupPath; set { if (OnPropertyChanging("GroupPath", value)) { _GroupPath = value; OnPropertyChanged("GroupPath"); } } }
+
+    private Int32 _Pings;
+    /// <summary>心跳</summary>
+    [DisplayName("心跳")]
+    [Description("心跳")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Pings", "心跳", "")]
+    public Int32 Pings { get => _Pings; set { if (OnPropertyChanging("Pings", value)) { _Pings = value; OnPropertyChanged("Pings"); } } }
+
+    private Int32 _Delay;
+    /// <summary>延迟。网络延迟,单位ms</summary>
+    [DisplayName("延迟")]
+    [Description("延迟。网络延迟,单位ms")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Delay", "延迟。网络延迟,单位ms", "")]
+    public Int32 Delay { get => _Delay; set { if (OnPropertyChanging("Delay", value)) { _Delay = value; OnPropertyChanged("Delay"); } } }
+
+    private Int32 _Offset;
+    /// <summary>偏移。客户端时间减服务端时间,单位s</summary>
+    [DisplayName("偏移")]
+    [Description("偏移。客户端时间减服务端时间,单位s")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Offset", "偏移。客户端时间减服务端时间,单位s", "")]
+    public Int32 Offset { get => _Offset; set { if (OnPropertyChanging("Offset", value)) { _Offset = value; OnPropertyChanged("Offset"); } } }
+
+    private DateTime _LocalTime;
+    /// <summary>本地时间</summary>
+    [DisplayName("本地时间")]
+    [Description("本地时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("LocalTime", "本地时间", "")]
+    public DateTime LocalTime { get => _LocalTime; set { if (OnPropertyChanging("LocalTime", value)) { _LocalTime = value; OnPropertyChanged("LocalTime"); } } }
+
+    private String _Token;
+    /// <summary>令牌</summary>
+    [DisplayName("令牌")]
+    [Description("令牌")]
+    [DataObjectField(false, false, true, 200)]
+    [BindColumn("Token", "令牌", "")]
+    public String Token { get => _Token; set { if (OnPropertyChanging("Token", value)) { _Token = value; OnPropertyChanged("Token"); } } }
+
+    private String _Creator;
+    /// <summary>创建者。服务端设备</summary>
+    [DisplayName("创建者")]
+    [Description("创建者。服务端设备")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Creator", "创建者。服务端设备", "")]
+    public String Creator { get => _Creator; set { if (OnPropertyChanging("Creator", value)) { _Creator = value; OnPropertyChanged("Creator"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private DateTime _UpdateTime;
+    /// <summary>更新时间</summary>
+    [DisplayName("更新时间")]
+    [Description("更新时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("UpdateTime", "更新时间", "")]
+    public DateTime UpdateTime { get => _UpdateTime; set { if (OnPropertyChanging("UpdateTime", value)) { _UpdateTime = value; OnPropertyChanged("UpdateTime"); } } }
+
+    private String _Remark;
+    /// <summary>备注</summary>
+    [DisplayName("备注")]
+    [Description("备注")]
+    [DataObjectField(false, false, true, 500)]
+    [BindColumn("Remark", "备注", "")]
+    public String Remark { get => _Remark; set { if (OnPropertyChanging("Remark", value)) { _Remark = value; OnPropertyChanged("Remark"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "SessionId" => _SessionId,
+            "ProductId" => _ProductId,
+            "DeviceId" => _DeviceId,
+            "Name" => _Name,
+            "IP" => _IP,
+            "GroupPath" => _GroupPath,
+            "Pings" => _Pings,
+            "Delay" => _Delay,
+            "Offset" => _Offset,
+            "LocalTime" => _LocalTime,
+            "Token" => _Token,
+            "Creator" => _Creator,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "UpdateTime" => _UpdateTime,
+            "Remark" => _Remark,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToInt(); break;
+                case "SessionId": _SessionId = Convert.ToString(value); break;
+                case "ProductId": _ProductId = value.ToInt(); break;
+                case "DeviceId": _DeviceId = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "IP": _IP = Convert.ToString(value); break;
+                case "GroupPath": _GroupPath = Convert.ToString(value); break;
+                case "Pings": _Pings = value.ToInt(); break;
+                case "Delay": _Delay = value.ToInt(); break;
+                case "Offset": _Offset = value.ToInt(); break;
+                case "LocalTime": _LocalTime = value.ToDateTime(); break;
+                case "Token": _Token = Convert.ToString(value); break;
+                case "Creator": _Creator = Convert.ToString(value); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "UpdateTime": _UpdateTime = value.ToDateTime(); break;
+                case "Remark": _Remark = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备在线字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>会话</summary>
+        public static readonly Field SessionId = FindByName("SessionId");
+
+        /// <summary>产品</summary>
+        public static readonly Field ProductId = FindByName("ProductId");
+
+        /// <summary>设备</summary>
+        public static readonly Field DeviceId = FindByName("DeviceId");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>本地IP</summary>
+        public static readonly Field IP = FindByName("IP");
+
+        /// <summary>分组</summary>
+        public static readonly Field GroupPath = FindByName("GroupPath");
+
+        /// <summary>心跳</summary>
+        public static readonly Field Pings = FindByName("Pings");
+
+        /// <summary>延迟。网络延迟,单位ms</summary>
+        public static readonly Field Delay = FindByName("Delay");
+
+        /// <summary>偏移。客户端时间减服务端时间,单位s</summary>
+        public static readonly Field Offset = FindByName("Offset");
+
+        /// <summary>本地时间</summary>
+        public static readonly Field LocalTime = FindByName("LocalTime");
+
+        /// <summary>令牌</summary>
+        public static readonly Field Token = FindByName("Token");
+
+        /// <summary>创建者。服务端设备</summary>
+        public static readonly Field Creator = FindByName("Creator");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>更新时间</summary>
+        public static readonly Field UpdateTime = FindByName("UpdateTime");
+
+        /// <summary>备注</summary>
+        public static readonly Field Remark = FindByName("Remark");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备在线字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>会话</summary>
+        public const String SessionId = "SessionId";
+
+        /// <summary>产品</summary>
+        public const String ProductId = "ProductId";
+
+        /// <summary>设备</summary>
+        public const String DeviceId = "DeviceId";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>本地IP</summary>
+        public const String IP = "IP";
+
+        /// <summary>分组</summary>
+        public const String GroupPath = "GroupPath";
+
+        /// <summary>心跳</summary>
+        public const String Pings = "Pings";
+
+        /// <summary>延迟。网络延迟,单位ms</summary>
+        public const String Delay = "Delay";
+
+        /// <summary>偏移。客户端时间减服务端时间,单位s</summary>
+        public const String Offset = "Offset";
+
+        /// <summary>本地时间</summary>
+        public const String LocalTime = "LocalTime";
+
+        /// <summary>令牌</summary>
+        public const String Token = "Token";
+
+        /// <summary>创建者。服务端设备</summary>
+        public const String Creator = "Creator";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>更新时间</summary>
+        public const String UpdateTime = "UpdateTime";
+
+        /// <summary>备注</summary>
+        public const String Remark = "Remark";
+    }
+    #endregion
+}
Added +194 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.Biz.cs"
new file mode 100644
index 0000000..9af67e4
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.Biz.cs"
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.Reflection;
+using NewLife.Threading;
+using NewLife.Web;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+using XCode.Membership;
+using XCode.Shards;
+
+namespace IoT.Data;
+
+public partial class DeviceProperty : Entity<DeviceProperty>
+{
+    #region 对象操作
+    static DeviceProperty()
+    {
+        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+        //var df = Meta.Factory.AdditionalFields;
+        //df.Add(nameof(DeviceId));
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+        Meta.Modules.Add<TraceModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 在新插入数据或者修改了指定字段时进行修正
+        //if (isNew && !Dirtys[nameof(CreateTime)]) CreateTime = DateTime.Now;
+        //if (!Dirtys[nameof(UpdateTime)]) UpdateTime = DateTime.Now;
+        //if (isNew && !Dirtys[nameof(CreateIP)]) CreateIP = ManageProvider.UserHost;
+        //if (!Dirtys[nameof(UpdateIP)]) UpdateIP = ManageProvider.UserHost;
+
+        // 检查唯一索引
+        // CheckExist(isNew, nameof(DeviceId), nameof(Name));
+    }
+
+    ///// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    //[EditorBrowsable(EditorBrowsableState.Never)]
+    //protected override void InitData()
+    //{
+    //    // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+    //    if (Meta.Session.Count > 0) return;
+
+    //    if (XTrace.Debug) XTrace.WriteLine("开始初始化DeviceProperty[设备属性]数据……");
+
+    //    var entity = new DeviceProperty();
+    //    entity.DeviceId = 0;
+    //    entity.Name = "abc";
+    //    entity.NickName = "abc";
+    //    entity.Type = "abc";
+    //    entity.Value = "abc";
+    //    entity.Unit = "abc";
+    //    entity.Enable = true;
+    //    entity.TraceId = "abc";
+    //    entity.CreateTime = DateTime.Now;
+    //    entity.CreateIP = "abc";
+    //    entity.UpdateTime = DateTime.Now;
+    //    entity.UpdateIP = "abc";
+    //    entity.Insert();
+
+    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceProperty[设备属性]数据!");
+    //}
+
+    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
+    ///// <returns></returns>
+    //public override Int32 Insert()
+    //{
+    //    return base.Insert();
+    //}
+
+    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
+    ///// <returns></returns>
+    //protected override Int32 OnDelete()
+    //{
+    //    return base.OnDelete();
+    //}
+    #endregion
+
+    #region 扩展属性
+    /// <summary>设备</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId));
+
+    /// <summary>设备</summary>
+    [Map(nameof(DeviceId), typeof(Device), "Id")]
+    public String DeviceName => Device?.Name;
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static DeviceProperty FindById(Int32 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据设备、名称查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="name">名称</param>
+    /// <returns>实体对象</returns>
+    public static DeviceProperty FindByDeviceIdAndName(Int32 deviceId, String name)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.DeviceId == deviceId && e.Name.EqualIgnoreCase(name));
+
+        return Find(_.DeviceId == deviceId & _.Name == name);
+    }
+
+    /// <summary>根据设备查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <returns>实体对象</returns>
+    public static IList<DeviceProperty> FindAllByDeviceId(Int32 deviceId)
+    {
+        var list = new List<DeviceProperty>();
+        if (deviceId <= 0) return list;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId);
+
+        return FindAll(_.DeviceId == deviceId);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="name">名称</param>
+    /// <param name="start">更新时间开始</param>
+    /// <param name="end">更新时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceProperty> Search(Int32 deviceId, String name, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (deviceId >= 0) exp &= _.DeviceId == deviceId;
+        if (!name.IsNullOrEmpty()) exp &= _.Name == name;
+        exp &= _.UpdateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.NickName.Contains(key) | _.Type.Contains(key) | _.Value.Contains(key) | _.Unit.Contains(key) | _.TraceId.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From DeviceProperty Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<DeviceProperty> _CategoryCache = new FieldCache<DeviceProperty>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+    #endregion
+}
Added +274 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.cs"
new file mode 100644
index 0000000..69a6b06
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\345\261\236\346\200\247.cs"
@@ -0,0 +1,274 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表</summary>
+[Serializable]
+[DataObject]
+[Description("设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表")]
+[BindIndex("IU_DeviceProperty_DeviceId_Name", true, "DeviceId,Name")]
+[BindIndex("IX_DeviceProperty_UpdateTime", false, "UpdateTime")]
+[BindTable("DeviceProperty", Description = "设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class DeviceProperty
+{
+    #region 属性
+    private Int32 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, true, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int32 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private Int32 _DeviceId;
+    /// <summary>设备</summary>
+    [DisplayName("设备")]
+    [Description("设备")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("DeviceId", "设备", "")]
+    public Int32 DeviceId { get => _DeviceId; set { if (OnPropertyChanging("DeviceId", value)) { _DeviceId = value; OnPropertyChanged("DeviceId"); } } }
+
+    private String _Name;
+    /// <summary>名称</summary>
+    [DisplayName("名称")]
+    [Description("名称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _NickName;
+    /// <summary>昵称</summary>
+    [DisplayName("昵称")]
+    [Description("昵称")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("NickName", "昵称", "")]
+    public String NickName { get => _NickName; set { if (OnPropertyChanging("NickName", value)) { _NickName = value; OnPropertyChanged("NickName"); } } }
+
+    private String _Type;
+    /// <summary>类型</summary>
+    [DisplayName("类型")]
+    [Description("类型")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Type", "类型", "")]
+    public String Type { get => _Type; set { if (OnPropertyChanging("Type", value)) { _Type = value; OnPropertyChanged("Type"); } } }
+
+    private String _Value;
+    /// <summary>数值。设备上报数值</summary>
+    [DisplayName("数值")]
+    [Description("数值。设备上报数值")]
+    [DataObjectField(false, false, true, -1)]
+    [BindColumn("Value", "数值。设备上报数值", "")]
+    public String Value { get => _Value; set { if (OnPropertyChanging("Value", value)) { _Value = value; OnPropertyChanged("Value"); } } }
+
+    private String _Unit;
+    /// <summary>单位</summary>
+    [DisplayName("单位")]
+    [Description("单位")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Unit", "单位", "")]
+    public String Unit { get => _Unit; set { if (OnPropertyChanging("Unit", value)) { _Unit = value; OnPropertyChanged("Unit"); } } }
+
+    private Boolean _Enable;
+    /// <summary>启用</summary>
+    [DisplayName("启用")]
+    [Description("启用")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Enable", "启用", "")]
+    public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+
+    private String _TraceId;
+    /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+    [Category("扩展")]
+    [DisplayName("追踪")]
+    [Description("追踪。用于记录调用链追踪标识,在APM查找调用链")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("TraceId", "追踪。用于记录调用链追踪标识,在APM查找调用链", "")]
+    public String TraceId { get => _TraceId; set { if (OnPropertyChanging("TraceId", value)) { _TraceId = value; OnPropertyChanged("TraceId"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [Category("扩展")]
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [Category("扩展")]
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+
+    private DateTime _UpdateTime;
+    /// <summary>更新时间</summary>
+    [Category("扩展")]
+    [DisplayName("更新时间")]
+    [Description("更新时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("UpdateTime", "更新时间", "")]
+    public DateTime UpdateTime { get => _UpdateTime; set { if (OnPropertyChanging("UpdateTime", value)) { _UpdateTime = value; OnPropertyChanged("UpdateTime"); } } }
+
+    private String _UpdateIP;
+    /// <summary>更新地址</summary>
+    [Category("扩展")]
+    [DisplayName("更新地址")]
+    [Description("更新地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("UpdateIP", "更新地址", "")]
+    public String UpdateIP { get => _UpdateIP; set { if (OnPropertyChanging("UpdateIP", value)) { _UpdateIP = value; OnPropertyChanged("UpdateIP"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "DeviceId" => _DeviceId,
+            "Name" => _Name,
+            "NickName" => _NickName,
+            "Type" => _Type,
+            "Value" => _Value,
+            "Unit" => _Unit,
+            "Enable" => _Enable,
+            "TraceId" => _TraceId,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            "UpdateTime" => _UpdateTime,
+            "UpdateIP" => _UpdateIP,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToInt(); break;
+                case "DeviceId": _DeviceId = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "NickName": _NickName = Convert.ToString(value); break;
+                case "Type": _Type = Convert.ToString(value); break;
+                case "Value": _Value = Convert.ToString(value); break;
+                case "Unit": _Unit = Convert.ToString(value); break;
+                case "Enable": _Enable = value.ToBoolean(); break;
+                case "TraceId": _TraceId = Convert.ToString(value); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                case "UpdateTime": _UpdateTime = value.ToDateTime(); break;
+                case "UpdateIP": _UpdateIP = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备属性字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>设备</summary>
+        public static readonly Field DeviceId = FindByName("DeviceId");
+
+        /// <summary>名称</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>昵称</summary>
+        public static readonly Field NickName = FindByName("NickName");
+
+        /// <summary>类型</summary>
+        public static readonly Field Type = FindByName("Type");
+
+        /// <summary>数值。设备上报数值</summary>
+        public static readonly Field Value = FindByName("Value");
+
+        /// <summary>单位</summary>
+        public static readonly Field Unit = FindByName("Unit");
+
+        /// <summary>启用</summary>
+        public static readonly Field Enable = FindByName("Enable");
+
+        /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public static readonly Field TraceId = FindByName("TraceId");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        /// <summary>更新时间</summary>
+        public static readonly Field UpdateTime = FindByName("UpdateTime");
+
+        /// <summary>更新地址</summary>
+        public static readonly Field UpdateIP = FindByName("UpdateIP");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备属性字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>设备</summary>
+        public const String DeviceId = "DeviceId";
+
+        /// <summary>名称</summary>
+        public const String Name = "Name";
+
+        /// <summary>昵称</summary>
+        public const String NickName = "NickName";
+
+        /// <summary>类型</summary>
+        public const String Type = "Type";
+
+        /// <summary>数值。设备上报数值</summary>
+        public const String Value = "Value";
+
+        /// <summary>单位</summary>
+        public const String Unit = "Unit";
+
+        /// <summary>启用</summary>
+        public const String Enable = "Enable";
+
+        /// <summary>追踪。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public const String TraceId = "TraceId";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+
+        /// <summary>更新时间</summary>
+        public const String UpdateTime = "UpdateTime";
+
+        /// <summary>更新地址</summary>
+        public const String UpdateIP = "UpdateIP";
+    }
+    #endregion
+}
Added +200 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs"
new file mode 100644
index 0000000..84fcc8e
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs"
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Membership;
+using XCode.Shards;
+
+namespace IoT.Data;
+
+public partial class DeviceData : Entity<DeviceData>
+{
+    #region 对象操作
+    static DeviceData()
+    {
+        Meta.Table.DataTable.InsertOnly = true;
+
+        // 按天分表
+        Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
+        {
+            TablePolicy = "{0}_{1:yyyyMMdd}",
+            Step = TimeSpan.FromDays(1),
+        };
+
+        // 过滤器 UserModule、TimeModule、IPModule
+        Meta.Modules.Add<TimeModule>();
+        Meta.Modules.Add<IPModule>();
+        Meta.Modules.Add<TraceModule>();
+    }
+
+    /// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
+    /// <param name="isNew">是否插入</param>
+    public override void Valid(Boolean isNew)
+    {
+        // 如果没有脏数据,则不需要进行任何处理
+        if (!HasDirty) return;
+
+        // 建议先调用基类方法,基类方法会做一些统一处理
+        base.Valid(isNew);
+
+        // 在新插入数据或者修改了指定字段时进行修正
+        //if (isNew && !Dirtys[nameof(CreateTime)]) CreateTime = DateTime.Now;
+        //if (isNew && !Dirtys[nameof(CreateIP)]) CreateIP = ManageProvider.UserHost;
+    }
+
+    ///// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    //[EditorBrowsable(EditorBrowsableState.Never)]
+    //protected override void InitData()
+    //{
+    //    // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+    //    if (Meta.Session.Count > 0) return;
+
+    //    if (XTrace.Debug) XTrace.WriteLine("开始初始化DeviceData[设备数据]数据……");
+
+    //    var entity = new DeviceData();
+    //    entity.Id = 0;
+    //    entity.DeviceId = 0;
+    //    entity.Name = "abc";
+    //    entity.Kind = "abc";
+    //    entity.Value = "abc";
+    //    entity.Timestamp = 0;
+    //    entity.TraceId = "abc";
+    //    entity.Creator = "abc";
+    //    entity.CreateTime = DateTime.Now;
+    //    entity.CreateIP = "abc";
+    //    entity.Insert();
+
+    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceData[设备数据]数据!");
+    //}
+
+    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
+    ///// <returns></returns>
+    //public override Int32 Insert()
+    //{
+    //    return base.Insert();
+    //}
+
+    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
+    ///// <returns></returns>
+    //protected override Int32 OnDelete()
+    //{
+    //    return base.OnDelete();
+    //}
+    #endregion
+
+    #region 扩展属性
+    /// <summary>设备</summary>
+    [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+    public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId));
+
+    /// <summary>设备</summary>
+    [Map(nameof(DeviceId), typeof(Device), "Id")]
+    public String DeviceName => Device?.Name;
+    #endregion
+
+    #region 扩展查询
+    /// <summary>根据编号查找</summary>
+    /// <param name="id">编号</param>
+    /// <returns>实体对象</returns>
+    public static DeviceData FindById(Int64 id)
+    {
+        if (id <= 0) return null;
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Id == id);
+
+        // 单对象缓存
+        return Meta.SingleCache[id];
+
+        //return Find(_.Id == id);
+    }
+
+    /// <summary>根据设备、编号查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="id">编号</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceData> FindAllByDeviceIdAndId(Int32 deviceId, Int64 id)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId && e.Id == id);
+
+        return FindAll(_.DeviceId == deviceId & _.Id == id);
+    }
+
+    /// <summary>根据设备查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceData> FindAllByDeviceId(Int32 deviceId)
+    {
+        if (deviceId <= 0) return new List<DeviceData>();
+
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId);
+
+        return FindAll(_.DeviceId == deviceId);
+    }
+
+    /// <summary>根据设备、名称查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="name">名称</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceData> FindAllByDeviceIdAndName(Int32 deviceId, String name)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId && e.Name.EqualIgnoreCase(name));
+
+        return FindAll(_.DeviceId == deviceId & _.Name == name);
+    }
+
+    /// <summary>根据设备、类型查找</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="kind">类型</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceData> FindAllByDeviceIdAndKind(Int32 deviceId, String kind)
+    {
+        // 实体缓存
+        if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId && e.Kind.EqualIgnoreCase(kind));
+
+        return FindAll(_.DeviceId == deviceId & _.Kind == kind);
+    }
+    #endregion
+
+    #region 高级查询
+    /// <summary>高级查询</summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="name">名称。MQTT的Topic,或者属性名</param>
+    /// <param name="start">创建时间开始</param>
+    /// <param name="end">创建时间结束</param>
+    /// <param name="key">关键字</param>
+    /// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
+    /// <returns>实体列表</returns>
+    public static IList<DeviceData> Search(Int32 deviceId, String name, DateTime start, DateTime end, String key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+
+        if (deviceId >= 0) exp &= _.DeviceId == deviceId;
+        if (!name.IsNullOrEmpty()) exp &= _.Name == name;
+        exp &= _.CreateTime.Between(start, end);
+        if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.Value.Contains(key) | _.TraceId.Contains(key) | _.Creator.Contains(key) | _.CreateIP.Contains(key);
+
+        return FindAll(exp, page);
+    }
+
+    // Select Count(Id) as Id,Category From DeviceData Where CreateTime>'2020-01-24 00:00:00' Group By Category Order By Id Desc limit 20
+    //static readonly FieldCache<DeviceData> _CategoryCache = new FieldCache<DeviceData>(nameof(Category))
+    //{
+    //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+    //};
+
+    ///// <summary>获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择</summary>
+    ///// <returns></returns>
+    //public static IDictionary<String, String> GetCategoryList() => _CategoryCache.FindAllName();
+    #endregion
+
+    #region 业务操作
+    #endregion
+}
\ No newline at end of file
Added +226 -0
diff --git "a/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.cs" "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.cs"
new file mode 100644
index 0000000..3576208
--- /dev/null
+++ "b/Samples/IoTZero/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.cs"
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Serialization;
+using System.Web.Script.Serialization;
+using System.Xml.Serialization;
+using NewLife;
+using NewLife.Data;
+using XCode;
+using XCode.Cache;
+using XCode.Configuration;
+using XCode.DataAccessLayer;
+
+namespace IoT.Data;
+
+/// <summary>设备数据。设备采集原始数据,按天分表存储</summary>
+[Serializable]
+[DataObject]
+[Description("设备数据。设备采集原始数据,按天分表存储")]
+[BindIndex("IX_DeviceData_DeviceId_Id", false, "DeviceId,Id")]
+[BindIndex("IX_DeviceData_DeviceId_Name_Id", false, "DeviceId,Name,Id")]
+[BindIndex("IX_DeviceData_DeviceId_Kind_Id", false, "DeviceId,Kind,Id")]
+[BindTable("DeviceData", Description = "设备数据。设备采集原始数据,按天分表存储", ConnName = "IoT", DbType = DatabaseType.None)]
+public partial class DeviceData
+{
+    #region 属性
+    private Int64 _Id;
+    /// <summary>编号</summary>
+    [DisplayName("编号")]
+    [Description("编号")]
+    [DataObjectField(true, false, false, 0)]
+    [BindColumn("Id", "编号", "")]
+    public Int64 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
+
+    private Int32 _DeviceId;
+    /// <summary>设备</summary>
+    [DisplayName("设备")]
+    [Description("设备")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("DeviceId", "设备", "")]
+    public Int32 DeviceId { get => _DeviceId; set { if (OnPropertyChanging("DeviceId", value)) { _DeviceId = value; OnPropertyChanged("DeviceId"); } } }
+
+    private String _Name;
+    /// <summary>名称。MQTT的Topic,或者属性名</summary>
+    [DisplayName("名称")]
+    [Description("名称。MQTT的Topic,或者属性名")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Name", "名称。MQTT的Topic,或者属性名", "", Master = true)]
+    public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
+
+    private String _Kind;
+    /// <summary>类型。数据来源,如PostProperty/PostData/MqttPostData</summary>
+    [DisplayName("类型")]
+    [Description("类型。数据来源,如PostProperty/PostData/MqttPostData")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Kind", "类型。数据来源,如PostProperty/PostData/MqttPostData", "")]
+    public String Kind { get => _Kind; set { if (OnPropertyChanging("Kind", value)) { _Kind = value; OnPropertyChanged("Kind"); } } }
+
+    private String _Value;
+    /// <summary>数值</summary>
+    [DisplayName("数值")]
+    [Description("数值")]
+    [DataObjectField(false, false, true, 2000)]
+    [BindColumn("Value", "数值", "")]
+    public String Value { get => _Value; set { if (OnPropertyChanging("Value", value)) { _Value = value; OnPropertyChanged("Value"); } } }
+
+    private Int64 _Timestamp;
+    /// <summary>时间戳。设备生成数据时的UTC毫秒</summary>
+    [DisplayName("时间戳")]
+    [Description("时间戳。设备生成数据时的UTC毫秒")]
+    [DataObjectField(false, false, false, 0)]
+    [BindColumn("Timestamp", "时间戳。设备生成数据时的UTC毫秒", "")]
+    public Int64 Timestamp { get => _Timestamp; set { if (OnPropertyChanging("Timestamp", value)) { _Timestamp = value; OnPropertyChanged("Timestamp"); } } }
+
+    private String _TraceId;
+    /// <summary>追踪标识。用于记录调用链追踪标识,在APM查找调用链</summary>
+    [Category("扩展")]
+    [DisplayName("追踪标识")]
+    [Description("追踪标识。用于记录调用链追踪标识,在APM查找调用链")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("TraceId", "追踪标识。用于记录调用链追踪标识,在APM查找调用链", "")]
+    public String TraceId { get => _TraceId; set { if (OnPropertyChanging("TraceId", value)) { _TraceId = value; OnPropertyChanged("TraceId"); } } }
+
+    private String _Creator;
+    /// <summary>创建者。服务端设备</summary>
+    [Category("扩展")]
+    [DisplayName("创建者")]
+    [Description("创建者。服务端设备")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("Creator", "创建者。服务端设备", "")]
+    public String Creator { get => _Creator; set { if (OnPropertyChanging("Creator", value)) { _Creator = value; OnPropertyChanged("Creator"); } } }
+
+    private DateTime _CreateTime;
+    /// <summary>创建时间</summary>
+    [Category("扩展")]
+    [DisplayName("创建时间")]
+    [Description("创建时间")]
+    [DataObjectField(false, false, true, 0)]
+    [BindColumn("CreateTime", "创建时间", "")]
+    public DateTime CreateTime { get => _CreateTime; set { if (OnPropertyChanging("CreateTime", value)) { _CreateTime = value; OnPropertyChanged("CreateTime"); } } }
+
+    private String _CreateIP;
+    /// <summary>创建地址</summary>
+    [Category("扩展")]
+    [DisplayName("创建地址")]
+    [Description("创建地址")]
+    [DataObjectField(false, false, true, 50)]
+    [BindColumn("CreateIP", "创建地址", "")]
+    public String CreateIP { get => _CreateIP; set { if (OnPropertyChanging("CreateIP", value)) { _CreateIP = value; OnPropertyChanged("CreateIP"); } } }
+    #endregion
+
+    #region 获取/设置 字段值
+    /// <summary>获取/设置 字段值</summary>
+    /// <param name="name">字段名</param>
+    /// <returns></returns>
+    public override Object this[String name]
+    {
+        get => name switch
+        {
+            "Id" => _Id,
+            "DeviceId" => _DeviceId,
+            "Name" => _Name,
+            "Kind" => _Kind,
+            "Value" => _Value,
+            "Timestamp" => _Timestamp,
+            "TraceId" => _TraceId,
+            "Creator" => _Creator,
+            "CreateTime" => _CreateTime,
+            "CreateIP" => _CreateIP,
+            _ => base[name]
+        };
+        set
+        {
+            switch (name)
+            {
+                case "Id": _Id = value.ToLong(); break;
+                case "DeviceId": _DeviceId = value.ToInt(); break;
+                case "Name": _Name = Convert.ToString(value); break;
+                case "Kind": _Kind = Convert.ToString(value); break;
+                case "Value": _Value = Convert.ToString(value); break;
+                case "Timestamp": _Timestamp = value.ToLong(); break;
+                case "TraceId": _TraceId = Convert.ToString(value); break;
+                case "Creator": _Creator = Convert.ToString(value); break;
+                case "CreateTime": _CreateTime = value.ToDateTime(); break;
+                case "CreateIP": _CreateIP = Convert.ToString(value); break;
+                default: base[name] = value; break;
+            }
+        }
+    }
+    #endregion
+
+    #region 关联映射
+    #endregion
+
+    #region 字段名
+    /// <summary>取得设备数据字段信息的快捷方式</summary>
+    public partial class _
+    {
+        /// <summary>编号</summary>
+        public static readonly Field Id = FindByName("Id");
+
+        /// <summary>设备</summary>
+        public static readonly Field DeviceId = FindByName("DeviceId");
+
+        /// <summary>名称。MQTT的Topic,或者属性名</summary>
+        public static readonly Field Name = FindByName("Name");
+
+        /// <summary>类型。数据来源,如PostProperty/PostData/MqttPostData</summary>
+        public static readonly Field Kind = FindByName("Kind");
+
+        /// <summary>数值</summary>
+        public static readonly Field Value = FindByName("Value");
+
+        /// <summary>时间戳。设备生成数据时的UTC毫秒</summary>
+        public static readonly Field Timestamp = FindByName("Timestamp");
+
+        /// <summary>追踪标识。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public static readonly Field TraceId = FindByName("TraceId");
+
+        /// <summary>创建者。服务端设备</summary>
+        public static readonly Field Creator = FindByName("Creator");
+
+        /// <summary>创建时间</summary>
+        public static readonly Field CreateTime = FindByName("CreateTime");
+
+        /// <summary>创建地址</summary>
+        public static readonly Field CreateIP = FindByName("CreateIP");
+
+        static Field FindByName(String name) => Meta.Table.FindByName(name);
+    }
+
+    /// <summary>取得设备数据字段名称的快捷方式</summary>
+    public partial class __
+    {
+        /// <summary>编号</summary>
+        public const String Id = "Id";
+
+        /// <summary>设备</summary>
+        public const String DeviceId = "DeviceId";
+
+        /// <summary>名称。MQTT的Topic,或者属性名</summary>
+        public const String Name = "Name";
+
+        /// <summary>类型。数据来源,如PostProperty/PostData/MqttPostData</summary>
+        public const String Kind = "Kind";
+
+        /// <summary>数值</summary>
+        public const String Value = "Value";
+
+        /// <summary>时间戳。设备生成数据时的UTC毫秒</summary>
+        public const String Timestamp = "Timestamp";
+
+        /// <summary>追踪标识。用于记录调用链追踪标识,在APM查找调用链</summary>
+        public const String TraceId = "TraceId";
+
+        /// <summary>创建者。服务端设备</summary>
+        public const String Creator = "Creator";
+
+        /// <summary>创建时间</summary>
+        public const String CreateTime = "CreateTime";
+
+        /// <summary>创建地址</summary>
+        public const String CreateIP = "CreateIP";
+    }
+    #endregion
+}
Added +69 -0
diff --git a/Samples/IoTZero/IoTSetting.cs b/Samples/IoTZero/IoTSetting.cs
new file mode 100644
index 0000000..988b3d7
--- /dev/null
+++ b/Samples/IoTZero/IoTSetting.cs
@@ -0,0 +1,69 @@
+using System.ComponentModel;
+using NewLife;
+using NewLife.Configuration;
+using NewLife.Security;
+using XCode.Configuration;
+
+namespace IoTZero;
+
+/// <summary>配置</summary>
+[Config("IoTZero")]
+public class IoTSetting : Config<IoTSetting>
+{
+    #region 静态
+    static IoTSetting() => Provider = new DbConfigProvider { UserId = 0, Category = "IoTServer" };
+    #endregion
+
+    #region 属性
+    ///// <summary>MQTT服务端口。默认1883</summary>
+    //[Description("MQTT服务端口。默认1883")]
+    //public Int32 MqttPort { get; set; } = 1883;
+
+    ///// <summary>MQTT证书地址。设置了才启用安全连接,默认为空</summary>
+    //[Description("MQTT证书地址。设置了才启用安全连接,默认为空")]
+    //public String MqttCertPath { get; set; }
+
+    ///// <summary>MMQTT证书密码</summary>
+    //[Description("MQTT证书密码")]
+    //public String MqttCertPassword { get; set; }
+    #endregion
+
+    #region 设备管理
+    /// <summary>令牌密钥。用于生成JWT令牌的算法和密钥,如HS256:ABCD1234</summary>
+    [Description("令牌密钥。用于生成JWT令牌的算法和密钥,如HS256:ABCD1234")]
+    [Category("设备管理")]
+    public String TokenSecret { get; set; }
+
+    /// <summary>令牌有效期。默认2*3600秒</summary>
+    [Description("令牌有效期。默认2*3600秒")]
+    [Category("设备管理")]
+    public Int32 TokenExpire { get; set; } = 2 * 3600;
+
+    /// <summary>会话超时。默认600秒</summary>
+    [Description("会话超时。默认600秒")]
+    [Category("设备管理")]
+    public Int32 SessionTimeout { get; set; } = 600;
+
+    /// <summary>自动注册。允许客户端自动注册,默认true</summary>
+    [Description("自动注册。允许客户端自动注册,默认true")]
+    [Category("设备管理")]
+    public Boolean AutoRegister { get; set; } = true;
+    #endregion
+
+    #region 数据存储
+    /// <summary>历史数据保留时间。默认30天</summary>
+    [Description("历史数据保留时间。默认30天")]
+    [Category("数据存储")]
+    public Int32 DataRetention { get; set; } = 30;
+    #endregion
+
+    #region 方法
+    /// <summary>加载时触发</summary>
+    protected override void OnLoaded()
+    {
+        if (TokenSecret.IsNullOrEmpty() || TokenSecret.Split(':').Length != 2) TokenSecret = $"HS256:{Rand.NextString(16)}";
+
+        base.OnLoaded();
+    }
+    #endregion
+}
\ No newline at end of file
Modified +0 -76
diff --git a/Samples/IoTZero/IoTZero.csproj b/Samples/IoTZero/IoTZero.csproj
index aa8edb3..6b553c9 100644
--- a/Samples/IoTZero/IoTZero.csproj
+++ b/Samples/IoTZero/IoTZero.csproj
@@ -19,59 +19,6 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\产品.Biz.cs" Link="Entity\产品.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\产品.cs" Link="Entity\产品.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备.Biz.cs" Link="Entity\设备.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备.cs" Link="Entity\设备.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备分组.Biz.cs" Link="Entity\设备分组.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备分组.cs" Link="Entity\设备分组.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备历史.Biz.cs" Link="Entity\设备历史.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备历史.cs" Link="Entity\设备历史.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备在线.Biz.cs" Link="Entity\设备在线.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备在线.cs" Link="Entity\设备在线.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备属性.Biz.cs" Link="Entity\设备属性.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备属性.cs" Link="Entity\设备属性.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备数据.Biz.cs" Link="Entity\设备数据.Biz.cs" />
-    <Compile Include="..\ZeroIoT\IoT.Data\Entity\设备数据.cs" Link="Entity\设备数据.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\LoginInfo.cs" Link="Models\LoginInfo.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\LoginResponse.cs" Link="Models\LoginResponse.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\LogoutResponse.cs" Link="Models\LogoutResponse.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\PingInfo.cs" Link="Models\PingInfo.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\PingResponse.cs" Link="Models\PingResponse.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\ThingSpecModel.cs" Link="Models\ThingSpecModel.cs" />
-    <Compile Include="..\ZeroIoT\IoTCore\Models\UpgradeInfo.cs" Link="Models\UpgradeInfo.cs" />
-    <Compile Include="..\ZeroIoT\IoTEdge\ClientSetting.cs" Link="Clients\ClientSetting.cs" />
-    <Compile Include="..\ZeroIoT\IoTEdge\HttpDevice.cs" Link="Clients\HttpDevice.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DeviceController.cs" Link="Areas\IoT\Controllers\DeviceController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DeviceDataController.cs" Link="Areas\IoT\Controllers\DeviceDataController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DeviceGroupController.cs" Link="Areas\IoT\Controllers\DeviceGroupController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DeviceHistoryController.cs" Link="Areas\IoT\Controllers\DeviceHistoryController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DeviceOnlineController.cs" Link="Areas\IoT\Controllers\DeviceOnlineController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\DevicePropertyController.cs" Link="Areas\IoT\Controllers\DevicePropertyController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\Controllers\ProductController.cs" Link="Areas\IoT\Controllers\ProductController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Areas\IoT\IoTArea.cs" Link="Areas\IoT\IoTArea.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Controllers\AppController.cs" Link="Controllers\AppController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Controllers\DeviceController.cs" Link="Controllers\DeviceController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Controllers\ThingController.cs" Link="Controllers\ThingController.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\IoTSetting.cs" Link="IoTSetting.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Program.cs" Link="Program.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\DataService.cs" Link="Services\DataService.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\DeviceOnlineService.cs" Link="Services\DeviceOnlineService.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\MyDeviceService.cs" Link="Services\MyDeviceService.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\QueueService.cs" Link="Services\QueueService.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\ShardTableService.cs" Link="Services\ShardTableService.cs" />
-    <Compile Include="..\ZeroIoT\IoTZero\Services\ThingService.cs" Link="Services\ThingService.cs" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Content Include="..\ZeroIoT\IoT.Data\Entity\IoT.htm" Link="Entity\IoT.htm" />
-    <Content Include="..\ZeroIoT\IoT.Data\Entity\Model.xml" Link="Entity\Model.xml" />
-    <Content Include="..\ZeroIoT\IoTZero\appsettings.json" Link="appsettings.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-  </ItemGroup>
-
-  <ItemGroup>
     <PackageReference Include="NewLife.Cube.Core" Version="5.5.2023.625-beta1355" />
     <PackageReference Include="NewLife.IoT" Version="1.8.2023.611-beta1629" />
     <PackageReference Include="NewLife.MQTT" Version="1.4.2023.620-beta1039" />
@@ -83,27 +30,4 @@
     <ProjectReference Include="..\..\NewLife.Remoting.Extensions\NewLife.Remoting.Extensions.csproj" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Areas\IoT\Controllers\" />
-    <Folder Include="Areas\IoT\Views\Device\" />
-    <Folder Include="Areas\IoT\Views\DeviceData\" />
-    <Folder Include="Areas\IoT\Views\DeviceGroup\" />
-    <Folder Include="Areas\IoT\Views\DeviceHistory\" />
-    <Folder Include="Controllers\" />
-    <Folder Include="Services\" />
-    <Folder Include="Common\" />
-    <Folder Include="Models\" />
-    <Folder Include="Entity\" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\DeviceData\_List_Search.cshtml" Link="Areas\IoT\Views\DeviceData\_List_Search.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\DeviceData\_List_Toolbar_Custom.cshtml" Link="Areas\IoT\Views\DeviceData\_List_Toolbar_Custom.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\DeviceGroup\_List_Toolbar_Batch.cshtml" Link="Areas\IoT\Views\DeviceGroup\_List_Toolbar_Batch.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\DeviceHistory\_List_Search.cshtml" Link="Areas\IoT\Views\DeviceHistory\_List_Search.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\Device\_List_Search.cshtml" Link="Areas\IoT\Views\Device\_List_Search.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\_ViewImports.cshtml" Link="Areas\IoT\Views\_ViewImports.cshtml" />
-    <None Include="..\ZeroIoT\IoTZero\Areas\IoT\Views\_ViewStart.cshtml" Link="Areas\IoT\Views\_ViewStart.cshtml" />
-  </ItemGroup>
-
 </Project>
Added +37 -0
diff --git a/Samples/IoTZero/Models/LoginInfo.cs b/Samples/IoTZero/Models/LoginInfo.cs
new file mode 100644
index 0000000..9b98f11
--- /dev/null
+++ b/Samples/IoTZero/Models/LoginInfo.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace NewLife.IoT.Models
+{
+    /// <summary>节点登录信息</summary>
+    public class LoginInfo
+    {
+        #region 属性
+        /// <summary>设备编码</summary>
+        public String Code { get; set; }
+
+        /// <summary>设备密钥</summary>
+        public String Secret { get; set; }
+
+        /// <summary>产品证书</summary>
+        public String ProductKey { get; set; }
+
+        /// <summary>产品密钥</summary>
+        public String ProductSecret { get; set; }
+
+        /// <summary>名称。可用于标识设备的名称</summary>
+        public String Name { get; set; }
+
+        /// <summary>版本</summary>
+        public String Version { get; set; }
+
+        /// <summary>本地IP地址</summary>
+        public String IP { get; set; }
+
+        /// <summary>唯一标识</summary>
+        public String UUID { get; set; }
+
+        /// <summary>本地UTC时间</summary>
+        public Int64 Time { get; set; }
+        #endregion
+    }
+}
\ No newline at end of file
Added +34 -0
diff --git a/Samples/IoTZero/Models/LoginResponse.cs b/Samples/IoTZero/Models/LoginResponse.cs
new file mode 100644
index 0000000..4778d71
--- /dev/null
+++ b/Samples/IoTZero/Models/LoginResponse.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace NewLife.IoT.Models
+{
+    /// <summary>设备登录响应</summary>
+    public class LoginResponse
+    {
+        #region 属性
+        /// <summary>产品</summary>
+        public String ProductKey { get; set; }
+
+        /// <summary>节点编码</summary>
+        public String Code { get; set; }
+
+        /// <summary>节点密钥</summary>
+        public String Secret { get; set; }
+
+        /// <summary>名称</summary>
+        public String Name { get; set; }
+
+        /// <summary>令牌</summary>
+        public String Token { get; set; }
+
+        /// <summary>服务器时间</summary>
+        public Int64 Time { get; set; }
+
+        ///// <summary>设备通道</summary>
+        //public String Channels { get; set; }
+
+        /// <summary>客户端唯一标识</summary>
+        public String ClientId { get; set; }
+        #endregion
+    }
+}
\ No newline at end of file
Added +22 -0
diff --git a/Samples/IoTZero/Models/LogoutResponse.cs b/Samples/IoTZero/Models/LogoutResponse.cs
new file mode 100644
index 0000000..b5aaca3
--- /dev/null
+++ b/Samples/IoTZero/Models/LogoutResponse.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace NewLife.IoT.Models
+{
+    /// <summary>设备注销响应</summary>
+    public class LogoutResponse
+    {
+        #region 属性
+        /// <summary>节点编码</summary>
+        public String Code { get; set; }
+
+        /// <summary>节点密钥</summary>
+        public String Secret { get; set; }
+
+        /// <summary>名称</summary>
+        public String Name { get; set; }
+
+        /// <summary>令牌</summary>
+        public String Token { get; set; }
+        #endregion
+    }
+}
\ No newline at end of file
Added +43 -0
diff --git a/Samples/IoTZero/Models/PingInfo.cs b/Samples/IoTZero/Models/PingInfo.cs
new file mode 100644
index 0000000..d1b6c6b
--- /dev/null
+++ b/Samples/IoTZero/Models/PingInfo.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace NewLife.IoT.Models
+{
+    /// <summary>心跳信息</summary>
+    public class PingInfo
+    {
+        #region 属性
+        /// <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 AvailableFreeSpace { get; set; }
+
+        /// <summary>CPU使用率</summary>
+        public Single CpuRate { get; set; }
+
+        /// <summary>温度</summary>
+        public Double Temperature { get; set; }
+
+        /// <summary>电量</summary>
+        public Double Battery { get; set; }
+
+        /// <summary>本地IP</summary>
+        public String IP { get; set; }
+
+        /// <summary>开机时间,单位s</summary>
+        public Int32 Uptime { get; set; }
+
+        /// <summary>本地UTC时间。ms毫秒</summary>
+        public Int64 Time { get; set; }
+
+        /// <summary>延迟。ms毫秒</summary>
+        public Int32 Delay { get; set; }
+        #endregion
+    }
+}
\ No newline at end of file
Added +21 -0
diff --git a/Samples/IoTZero/Models/PingResponse.cs b/Samples/IoTZero/Models/PingResponse.cs
new file mode 100644
index 0000000..ec717a5
--- /dev/null
+++ b/Samples/IoTZero/Models/PingResponse.cs
@@ -0,0 +1,21 @@
+using System;
+using NewLife.IoT.ThingModels;
+
+namespace NewLife.IoT.Models
+{
+    /// <summary>心跳响应</summary>
+    public class PingResponse
+    {
+        /// <summary>本地时间。ms毫秒</summary>
+        public Int64 Time { get; set; }
+
+        /// <summary>服务器时间</summary>
+        public Int64 ServerTime { get; set; }
+
+        /// <summary>心跳周期。单位秒</summary>
+        public Int32 Period { get; set; }
+
+        /// <summary>令牌。现有令牌即将过期时,颁发新的令牌</summary>
+        public String Token { get; set; }
+    }
+}
\ No newline at end of file
Added +13 -0
diff --git a/Samples/IoTZero/Models/ThingSpecModel.cs b/Samples/IoTZero/Models/ThingSpecModel.cs
new file mode 100644
index 0000000..8c354f0
--- /dev/null
+++ b/Samples/IoTZero/Models/ThingSpecModel.cs
@@ -0,0 +1,13 @@
+using NewLife.IoT.ThingSpecification;
+
+namespace NewLife.IoT.Models;
+
+/// <summary>物模型上报</summary>
+public class ThingSpecModel
+{
+    /// <summary>设备编码</summary>
+    public String DeviceCode { get; set; }
+
+    /// <summary>物模型</summary>
+    public ThingSpec Spec { get; set; }
+}
Added +26 -0
diff --git a/Samples/IoTZero/Models/UpgradeInfo.cs b/Samples/IoTZero/Models/UpgradeInfo.cs
new file mode 100644
index 0000000..29a56d6
--- /dev/null
+++ b/Samples/IoTZero/Models/UpgradeInfo.cs
@@ -0,0 +1,26 @@
+namespace NewLife.IoT.Models;
+
+/// <summary>更新响应</summary>
+public class UpgradeInfo
+{
+    /// <summary>版本号</summary>
+    public String Version { get; set; }
+
+    /// <summary>更新源,Url地址</summary>
+    public String Source { get; set; }
+
+    /// <summary>文件哈希</summary>
+    public String FileHash { get; set; }
+
+    /// <summary>文件大小</summary>
+    public Int64 FileSize { get; set; }
+
+    /// <summary>更新后要执行的命令</summary>
+    public String Executor { get; set; }
+
+    /// <summary>是否强制更新,不需要用户同意</summary>
+    public Boolean Force { get; set; }
+
+    /// <summary>描述</summary>
+    public String Description { get; set; }
+}
\ No newline at end of file
Added +108 -0
diff --git a/Samples/IoTZero/Program.cs b/Samples/IoTZero/Program.cs
new file mode 100644
index 0000000..1b6d71b
--- /dev/null
+++ b/Samples/IoTZero/Program.cs
@@ -0,0 +1,108 @@
+using IoTZero;
+using IoTZero.Services;
+using NewLife.Caching;
+using NewLife.Cube;
+using NewLife.Log;
+using NewLife.Reflection;
+using NewLife.Security;
+using XCode;
+
+// 日志输出到控制台,并拦截全局异常
+XTrace.UseConsole();
+
+var builder = WebApplication.CreateBuilder(args);
+var services = builder.Services;
+
+// 配置星尘。借助StarAgent,或者读取配置文件 config/star.config 中的服务器地址、应用标识、密钥
+var star = services.AddStardust(null);
+
+// 把数据目录指向上层,例如部署到 /root/iot/edge/,这些目录放在 /root/iot/
+var set = NewLife.Setting.Current;
+if (set.IsNew)
+{
+    set.LogPath = "../Log";
+    set.DataPath = "../Data";
+    set.BackupPath = "../Backup";
+    set.Save();
+}
+var set2 = CubeSetting.Current;
+if (set2.IsNew)
+{
+    set2.AvatarPath = "../Avatars";
+    set2.UploadPath = "../Uploads";
+    set2.Save();
+}
+var set3 = XCodeSetting.Current;
+if (set3.IsNew)
+{
+    set3.ShowSQL = false;
+    set3.EntityCacheExpire = 60;
+    set3.SingleCacheExpire = 60;
+    set3.Save();
+}
+
+// 系统设置
+services.AddSingleton(IoTSetting.Current);
+
+// 逐个注册每一个用到的服务,必须做到清晰明了
+services.AddSingleton<IPasswordProvider>(new SaltPasswordProvider { Algorithm = "md5" });
+
+services.AddSingleton<ThingService>();
+services.AddSingleton<DataService>();
+services.AddSingleton<QueueService>();
+services.AddSingleton<MyDeviceService>();
+
+services.AddHttpClient("hc", e => e.Timeout = TimeSpan.FromSeconds(5));
+
+services.AddSingleton<ICache, MemoryCache>();
+
+// 后台服务
+services.AddHostedService<ShardTableService>();
+services.AddHostedService<DeviceOnlineService>();
+
+// 启用接口响应压缩
+services.AddResponseCompression();
+
+services.AddControllersWithViews();
+
+// 引入魔方
+services.AddCube();
+
+var app = builder.Build();
+
+// 预热数据层,执行反向工程建表等操作
+EntityFactory.InitConnection("Membership");
+EntityFactory.InitConnection("Log");
+EntityFactory.InitConnection("Cube");
+EntityFactory.InitConnection("IoT");
+
+// 使用Cube前添加自己的管道
+if (app.Environment.IsDevelopment())
+    app.UseDeveloperExceptionPage();
+else
+    app.UseExceptionHandler("/CubeHome/Error");
+
+app.UseResponseCompression();
+
+app.UseWebSockets(new WebSocketOptions()
+{
+    KeepAliveInterval = TimeSpan.FromSeconds(60),
+});
+
+// 使用魔方
+app.UseCube(app.Environment);
+
+app.UseAuthorization();
+
+app.MapControllerRoute(
+    name: "default",
+    pattern: "{controller=CubeHome}/{action=Index}/{id?}");
+
+app.RegisterService("AlarmServer", null, app.Environment.EnvironmentName);
+
+// 反射查找并调用客户端测试,该代码仅用于测试,实际项目中不要这样做
+var clientType = "IoTZero.Clients.ClientTest".GetTypeEx();
+var test = clientType?.GetMethodEx("Main").As<Func<IServiceProvider, Task>>();
+if (test != null) _ = Task.Run(() => test(app.Services));
+
+app.Run();
Added +77 -0
diff --git a/Samples/IoTZero/Services/DataService.cs b/Samples/IoTZero/Services/DataService.cs
new file mode 100644
index 0000000..db92b39
--- /dev/null
+++ b/Samples/IoTZero/Services/DataService.cs
@@ -0,0 +1,77 @@
+using IoT.Data;
+using NewLife;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+
+namespace IoTZero.Services;
+
+/// <summary>数据服务</summary>
+public class DataService
+{
+    private readonly ITracer _tracer;
+
+    /// <summary>实例化数据服务</summary>
+    /// <param name="tracer"></param>
+    public DataService(ITracer tracer) => _tracer = tracer;
+
+    #region 方法
+    /// <summary>
+    /// 插入设备原始数据,异步批量操作
+    /// </summary>
+    /// <param name="deviceId">设备</param>
+    /// <param name="sensorId">传感器</param>
+    /// <param name="time"></param>
+    /// <param name="name"></param>
+    /// <param name="value"></param>
+    /// <param name="kind"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public DeviceData AddData(Int32 deviceId, Int32 sensorId, Int64 time, String name, String value, String kind, String ip)
+    {
+        if (value.IsNullOrEmpty()) return null;
+
+        using var span = _tracer?.NewSpan("thing:AddData", new { deviceId, time, name, value });
+
+        /*
+         * 使用采集时间来生成雪花Id,数据存储序列即业务时间顺序。
+         * 在历史数据查询和统计分析时,一马平川,再也不必考虑边界溢出问题。
+         * 数据延迟上传可能会导致插入历史数据,从而影响蚂蚁实时计算,可通过补偿定时批计算修正。
+         * 实际应用中,更多通过消息队列来驱动实时计算。
+         */
+
+        // 取客户端采集时间,较大时间差时取本地时间
+        var t = time.ToDateTime().ToLocalTime();
+        if (t.Year < 2000 || t.AddDays(1) < DateTime.Now) t = DateTime.Now;
+
+        var snow = DeviceData.Meta.Factory.Snow;
+
+        var traceId = DefaultSpan.Current?.TraceId;
+
+        var entity = new DeviceData
+        {
+            Id = snow.NewId(t, sensorId),
+            DeviceId = deviceId,
+            Name = name,
+            Value = value,
+            Kind = kind,
+
+            Timestamp = time,
+            TraceId = traceId,
+            Creator = Environment.MachineName,
+            CreateTime = DateTime.Now,
+            CreateIP = ip,
+        };
+
+        var rs = entity.SaveAsync() ? 1 : 0;
+
+        return entity;
+    }
+
+    /// <summary>添加事件</summary>
+    /// <param name="deviceId"></param>
+    /// <param name="model"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public void AddEvent(Int32 deviceId, EventModel model, String ip) => throw new NotImplementedException();
+    #endregion
+}
\ No newline at end of file
Added +102 -0
diff --git a/Samples/IoTZero/Services/DeviceOnlineService.cs b/Samples/IoTZero/Services/DeviceOnlineService.cs
new file mode 100644
index 0000000..f69a407
--- /dev/null
+++ b/Samples/IoTZero/Services/DeviceOnlineService.cs
@@ -0,0 +1,102 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Log;
+using NewLife.Threading;
+
+namespace IoTZero.Services;
+
+/// <summary>节点在线服务</summary>
+public class DeviceOnlineService : IHostedService
+{
+    #region 属性
+    private TimerX _timer;
+    private readonly MyDeviceService _deviceService;
+    private readonly IoTSetting _setting;
+    private readonly ITracer _tracer;
+    #endregion
+
+    #region 构造
+    /// <summary>
+    /// 实例化节点在线服务
+    /// </summary>
+    /// <param name="deviceService"></param>
+    /// <param name="setting"></param>
+    /// <param name="tracer"></param>
+    public DeviceOnlineService(MyDeviceService deviceService, IoTSetting setting, ITracer tracer)
+    {
+        _deviceService = deviceService;
+        _setting = setting;
+        _tracer = tracer;
+    }
+    #endregion
+
+    #region 方法
+    /// <summary>
+    /// 开始服务
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task StartAsync(CancellationToken cancellationToken)
+    {
+        _timer = new TimerX(CheckOnline, null, 5_000, 30_000) { Async = true };
+
+        return Task.CompletedTask;
+    }
+
+    /// <summary>
+    /// 停止服务
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        _timer.TryDispose();
+
+        return Task.CompletedTask;
+    }
+
+    private void CheckOnline(Object state)
+    {
+        // 节点超时
+        if (_setting.SessionTimeout > 0)
+        {
+            using var span = _tracer?.NewSpan(nameof(CheckOnline));
+
+            var rs = DeviceOnline.ClearExpire(TimeSpan.FromSeconds(_setting.SessionTimeout));
+            if (rs != null)
+            {
+                foreach (var olt in rs)
+                {
+                    var device = olt?.Device;
+                    var msg = $"[{device}]登录于{olt.CreateTime.ToFullString()},最后活跃于{olt.UpdateTime.ToFullString()}";
+                    _deviceService.WriteHistory(device, "超时下线", true, msg, olt.CreateIP);
+
+                    _deviceService.RemoveOnline(olt.DeviceId, olt.CreateIP);
+
+                    if (device != null)
+                    {
+                        // 计算在线时长
+                        if (olt.CreateTime.Year > 2000 && olt.UpdateTime.Year > 2000)
+                        {
+                            device.OnlineTime += (Int32)(olt.UpdateTime - olt.CreateTime).TotalSeconds;
+                            device.Logout();
+                        }
+
+                        CheckOffline(device, "超时下线");
+                    }
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// 检查离线
+    /// </summary>
+    /// <param name="node"></param>
+    /// <param name="reason"></param>
+    public static void CheckOffline(Device node, String reason)
+    {
+        //todo 下线告警
+    }
+    #endregion
+}
\ No newline at end of file
Added +404 -0
diff --git a/Samples/IoTZero/Services/MyDeviceService.cs b/Samples/IoTZero/Services/MyDeviceService.cs
new file mode 100644
index 0000000..f6ca961
--- /dev/null
+++ b/Samples/IoTZero/Services/MyDeviceService.cs
@@ -0,0 +1,404 @@
+using System.Reflection;
+using IoT.Data;
+using NewLife;
+using NewLife.Caching;
+using NewLife.IoT.Models;
+using NewLife.Log;
+using NewLife.Remoting;
+using NewLife.Security;
+using NewLife.Serialization;
+using NewLife.Web;
+
+namespace IoTZero.Services;
+
+/// <summary>设备服务</summary>
+public class MyDeviceService
+{
+    /// <summary>节点引用,令牌无效时使用</summary>
+    public Device Current { get; set; }
+
+    private readonly ICache _cache;
+    private readonly IPasswordProvider _passwordProvider;
+    private readonly DataService _dataService;
+    private readonly IoTSetting _setting;
+    private readonly ITracer _tracer;
+
+    /// <summary>
+    /// 实例化设备服务
+    /// </summary>
+    /// <param name="passwordProvider"></param>
+    /// <param name="dataService"></param>
+    /// <param name="cacheProvider"></param>
+    /// <param name="setting"></param>
+    /// <param name="tracer"></param>
+    public MyDeviceService(IPasswordProvider passwordProvider, DataService dataService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
+    {
+        _passwordProvider = passwordProvider;
+        _dataService = dataService;
+        _cache = cacheProvider.InnerCache;
+        _setting = setting;
+        _tracer = tracer;
+    }
+
+    #region 登录
+    /// <summary>
+    /// 设备登录验证,内部支持动态注册
+    /// </summary>
+    /// <param name="inf">登录信息</param>
+    /// <param name="source">登录来源</param>
+    /// <param name="ip">远程IP</param>
+    /// <returns></returns>
+    /// <exception cref="ApiException"></exception>
+    public LoginResponse Login(LoginInfo inf, String source, String ip)
+    {
+        var code = inf.Code;
+        var secret = inf.Secret;
+
+        var dv = Device.FindByCode(code);
+        Current = dv;
+
+        var autoReg = false;
+        if (dv == null)
+        {
+            if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(98, "找不到设备,且产品证书为空,无法登录");
+
+            dv = AutoRegister(null, inf, ip);
+            autoReg = true;
+        }
+        else
+        {
+            if (!dv.Enable) throw new ApiException(99, "禁止登录");
+
+            // 校验唯一编码,防止客户端拷贝配置
+            var uuid = inf.UUID;
+            if (!uuid.IsNullOrEmpty() && !dv.Uuid.IsNullOrEmpty() && uuid != dv.Uuid)
+                WriteHistory(dv, source + "登录校验", false, $"新旧唯一标识不一致!(新){uuid}!={dv.Uuid}(旧)", ip);
+
+            // 登录密码未设置或者未提交,则执行动态注册
+            if (dv == null || !dv.Secret.IsNullOrEmpty()
+                && (secret.IsNullOrEmpty() || !_passwordProvider.Verify(dv.Secret, secret)))
+            {
+                if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(98, "设备验证失败,且产品证书为空,无法登录");
+
+                dv = AutoRegister(dv, inf, ip);
+                autoReg = true;
+            }
+        }
+
+        //if (dv != null && !dv.Enable) throw new ApiException(99, "禁止登录");
+
+        Current = dv ?? throw new ApiException(12, "节点鉴权失败");
+
+        dv.Login(inf, ip);
+
+        // 设置令牌
+        var tm = IssueToken(dv.Code, _setting);
+
+        // 在线记录
+        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
+        olt.Save(inf, null, tm.AccessToken);
+
+        //SetChildOnline(dv, ip);
+
+        // 登录历史
+        WriteHistory(dv, source + "设备鉴权", true, $"[{dv.Name}/{dv.Code}]鉴权成功 " + inf.ToJson(false, false, false), ip);
+
+        var rs = new LoginResponse
+        {
+            Name = dv.Name,
+            Token = tm.AccessToken,
+            Time = DateTime.UtcNow.ToLong(),
+        };
+
+        // 动态注册的设备不可用时,不要发令牌,只发证书
+        if (!dv.Enable) rs.Token = null;
+
+        // 动态注册,下发节点证书
+        if (autoReg) rs.Secret = dv.Secret;
+
+        rs.Code = dv.Code;
+
+        return rs;
+    }
+
+    /// <summary>设置设备在线,同时检查在线表</summary>
+    /// <param name="dv"></param>
+    /// <param name="ip"></param>
+    /// <param name="reason"></param>
+    public void SetDeviceOnline(Device 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 });
+
+        var olt = GetOnline(dv, ip) ?? CreateOnline(dv, ip);
+
+        dv.SetOnline(ip, reason);
+
+        // 避免频繁更新心跳数
+        if (olt.UpdateTime.AddSeconds(60) < DateTime.Now)
+            olt.Save(null, null, null);
+    }
+
+    /// <summary>自动注册</summary>
+    /// <param name="device"></param>
+    /// <param name="inf"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    /// <exception cref="ApiException"></exception>
+    public Device AutoRegister(Device device, 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 = Rand.NextString(8);
+
+        device ??= new Device
+        {
+            Code = code,
+            CreateIP = ip,
+            CreateTime = DateTime.Now,
+            Secret = Rand.NextString(8),
+        };
+
+        // 如果未打开动态注册,则把节点修改为禁用
+        device.Enable = true;
+
+        if (device.Name.IsNullOrEmpty()) device.Name = inf.Name;
+
+        device.ProductId = product.Id;
+        //device.Secret = Rand.NextString(16);
+        device.UpdateIP = ip;
+        device.UpdateTime = DateTime.Now;
+
+        device.Save();
+
+        // 更新产品设备总量避免界面无法及时获取设备数量信息
+        device.Product.Fix();
+
+        WriteHistory(device, "动态注册", true, inf.ToJson(false, false, false), ip);
+
+        return device;
+    }
+
+    /// <summary>注销</summary>
+    /// <param name="device">设备</param>
+    /// <param name="reason">注销原因</param>
+    /// <param name="source">登录来源</param>
+    /// <param name="ip">远程IP</param>
+    /// <returns></returns>
+    public Device Logout(Device device, String reason, String source, String ip)
+    {
+        var olt = GetOnline(device, ip);
+        if (olt != null)
+        {
+            var msg = $"{reason} [{device}]]登录于{olt.CreateTime.ToFullString()},最后活跃于{olt.UpdateTime.ToFullString()}";
+            WriteHistory(device, source + "设备下线", true, msg, ip);
+            olt.Delete();
+
+            var sid = $"{device.Id}@{ip}";
+            _cache.Remove($"DeviceOnline:{sid}");
+
+            // 计算在线时长
+            if (olt.CreateTime.Year > 2000)
+            {
+                device.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
+                device.Logout();
+            }
+
+            //DeviceOnlineService.CheckOffline(device, "注销");
+        }
+
+        return device;
+    }
+    #endregion
+
+    #region 心跳
+    /// <summary>
+    /// 心跳
+    /// </summary>
+    /// <param name="device"></param>
+    /// <param name="inf"></param>
+    /// <param name="token"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public DeviceOnline Ping(Device device, PingInfo inf, String token, String ip)
+    {
+        if (inf != null && !inf.IP.IsNullOrEmpty()) device.IP = inf.IP;
+
+        // 自动上线
+        if (device != null && !device.Online) device.SetOnline(ip, "心跳");
+
+        device.UpdateIP = ip;
+        device.SaveAsync();
+
+        var olt = GetOnline(device, ip) ?? CreateOnline(device, ip);
+        olt.Name = device.Name;
+        olt.GroupPath = device.GroupPath;
+        olt.ProductId = device.ProductId;
+        olt.Save(null, inf, token);
+
+        return olt;
+    }
+
+    /// <summary></summary>
+    /// <param name="device"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    protected virtual DeviceOnline GetOnline(Device device, String ip)
+    {
+        var sid = $"{device.Id}@{ip}";
+        var olt = _cache.Get<DeviceOnline>($"DeviceOnline:{sid}");
+        if (olt != null)
+        {
+            _cache.SetExpire($"DeviceOnline:{sid}", TimeSpan.FromSeconds(600));
+            return olt;
+        }
+
+        return DeviceOnline.FindBySessionId(sid);
+    }
+
+    /// <summary>检查在线</summary>
+    /// <param name="device"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    protected virtual DeviceOnline CreateOnline(Device device, String ip)
+    {
+        var sid = $"{device.Id}@{ip}";
+        var olt = DeviceOnline.GetOrAdd(sid);
+        olt.ProductId = device.ProductId;
+        olt.DeviceId = device.Id;
+        olt.Name = device.Name;
+        olt.IP = device.IP;
+        olt.CreateIP = ip;
+
+        olt.Creator = Environment.MachineName;
+
+        _cache.Set($"DeviceOnline:{sid}", olt, 600);
+
+        return olt;
+    }
+
+    /// <summary>删除在线</summary>
+    /// <param name="deviceId"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public Int32 RemoveOnline(Int32 deviceId, String ip)
+    {
+        var sid = $"{deviceId}@{ip}";
+
+        return _cache.Remove($"DeviceOnline:{sid}");
+    }
+    #endregion
+
+    #region 辅助
+    /// <summary>
+    /// 颁发令牌
+    /// </summary>
+    /// <param name="name"></param>
+    /// <param name="set"></param>
+    /// <returns></returns>
+    public TokenModel IssueToken(String name, IoTSetting set)
+    {
+        // 颁发令牌
+        var ss = set.TokenSecret.Split(':');
+        var jwt = new JwtBuilder
+        {
+            Issuer = Assembly.GetEntryAssembly().GetName().Name,
+            Subject = name,
+            Id = Rand.NextString(8),
+            Expire = DateTime.Now.AddSeconds(set.TokenExpire),
+
+            Algorithm = ss[0],
+            Secret = ss[1],
+        };
+
+        return new TokenModel
+        {
+            AccessToken = jwt.Encode(null),
+            TokenType = jwt.Type ?? "JWT",
+            ExpireIn = set.TokenExpire,
+            RefreshToken = jwt.Encode(null),
+        };
+    }
+
+    /// <summary>
+    /// 解码令牌,并验证有效性
+    /// </summary>
+    /// <param name="token"></param>
+    /// <param name="tokenSecret"></param>
+    /// <returns></returns>
+    /// <exception cref="ApiException"></exception>
+    public Device DecodeToken(String token, String tokenSecret)
+    {
+        //if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
+        if (token.IsNullOrEmpty()) throw new ApiException(402, "节点未登录");
+
+        // 解码令牌
+        var ss = tokenSecret.Split(':');
+        var jwt = new JwtBuilder
+        {
+            Algorithm = ss[0],
+            Secret = ss[1],
+        };
+
+        var rs = jwt.TryDecode(token, out var message);
+        var node = Device.FindByCode(jwt.Subject);
+        Current = node;
+        if (!rs) throw new ApiException(403, $"非法访问 {message}");
+
+        return node;
+    }
+
+    /// <summary>
+    /// 验证并颁发令牌
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <param name="token"></param>
+    /// <returns></returns>
+    public TokenModel ValidAndIssueToken(String deviceCode, String token)
+    {
+        if (token.IsNullOrEmpty()) return null;
+
+        // 令牌有效期检查,10分钟内过期者,重新颁发令牌
+        var ss = _setting.TokenSecret.Split(':');
+        var jwt = new JwtBuilder
+        {
+            Algorithm = ss[0],
+            Secret = ss[1],
+        };
+        var rs = jwt.TryDecode(token, out var message);
+        if (!rs || jwt == null) return null;
+
+        if (DateTime.Now.AddMinutes(10) > jwt.Expire) return IssueToken(deviceCode, _setting);
+
+        return null;
+    }
+
+    /// <summary>
+    /// 写设备历史
+    /// </summary>
+    /// <param name="device"></param>
+    /// <param name="action"></param>
+    /// <param name="success"></param>
+    /// <param name="remark"></param>
+    /// <param name="ip"></param>
+    public void WriteHistory(Device device, String action, Boolean success, String remark, String ip)
+    {
+        var traceId = DefaultSpan.Current?.TraceId;
+        var hi = DeviceHistory.Create(device ?? Current, action, success, remark, Environment.MachineName, ip, traceId);
+    }
+    #endregion
+}
\ No newline at end of file
Added +69 -0
diff --git a/Samples/IoTZero/Services/QueueService.cs b/Samples/IoTZero/Services/QueueService.cs
new file mode 100644
index 0000000..b6acae7
--- /dev/null
+++ b/Samples/IoTZero/Services/QueueService.cs
@@ -0,0 +1,69 @@
+using NewLife.Caching;
+using NewLife.Caching.Queues;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Serialization;
+
+namespace IoTZero.Services;
+
+/// <summary>队列服务</summary>
+public class QueueService
+{
+    #region 属性
+    private readonly ICacheProvider _cacheProvider;
+    private readonly ITracer _tracer;
+    #endregion
+
+    #region 构造
+    /// <summary>
+    /// 实例化队列服务
+    /// </summary>
+    public QueueService(ICacheProvider cacheProvider, ITracer tracer)
+    {
+        _cacheProvider = cacheProvider;
+        _tracer = tracer;
+    }
+    #endregion
+
+    #region 命令队列
+    /// <summary>
+    /// 获取指定设备的命令队列
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <returns></returns>
+    public IProducerConsumer<String> GetQueue(String deviceCode)
+    {
+        var q = _cacheProvider.GetQueue<String>($"cmd:{deviceCode}");
+        if (q is QueueBase qb) qb.TraceName = "ServiceQueue";
+
+        return q;
+    }
+
+    /// <summary>
+    /// 向指定设备发送命令
+    /// </summary>
+    /// <param name="deviceCode"></param>
+    /// <param name="model"></param>
+    /// <returns></returns>
+    public Int32 Publish(String deviceCode, ServiceModel model)
+    {
+        using var span = _tracer?.NewSpan(nameof(Publish), $"{deviceCode} {model.ToJson()}");
+
+        var q = GetQueue(deviceCode);
+        return q.Add(model.ToJson());
+    }
+
+    /// <summary>
+    /// 获取指定设备的服务响应队列
+    /// </summary>
+    /// <param name="serviceLogId"></param>
+    /// <returns></returns>
+    public IProducerConsumer<String> GetReplyQueue(Int64 serviceLogId) => throw new NotImplementedException();
+
+    /// <summary>
+    /// 发送消息到服务响应队列
+    /// </summary>
+    /// <param name="model"></param>
+    public void PublishReply(ServiceReplyModel model) => throw new NotImplementedException();
+    #endregion
+}
\ No newline at end of file
Added +115 -0
diff --git a/Samples/IoTZero/Services/ShardTableService.cs b/Samples/IoTZero/Services/ShardTableService.cs
new file mode 100644
index 0000000..af8bc41
--- /dev/null
+++ b/Samples/IoTZero/Services/ShardTableService.cs
@@ -0,0 +1,115 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Log;
+using NewLife.Threading;
+using XCode.DataAccessLayer;
+using XCode.Shards;
+
+namespace IoTZero.Services;
+
+/// <summary>分表管理</summary>
+public class ShardTableService : IHostedService
+{
+    private readonly IoTSetting _setting;
+    private readonly ITracer _tracer;
+    private TimerX _timer;
+
+    /// <summary>
+    /// 实例化分表管理服务
+    /// </summary>
+    /// <param name="setting"></param>
+    /// <param name="tracer"></param>
+    public ShardTableService(IoTSetting setting, ITracer tracer)
+    {
+        _setting = setting;
+        _tracer = tracer;
+    }
+
+    /// <summary>
+    /// 开始服务
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task StartAsync(CancellationToken cancellationToken)
+    {
+        // 每小时执行
+        _timer = new TimerX(DoShardTable, null, 5_000, 3600 * 1000) { Async = true };
+
+        return Task.CompletedTask;
+    }
+
+    /// <summary>
+    /// 停止服务
+    /// </summary>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task StopAsync(CancellationToken cancellationToken)
+    {
+        _timer.TryDispose();
+
+        return Task.CompletedTask;
+    }
+
+    private void DoShardTable(Object state)
+    {
+        var set = _setting;
+        if (set.DataRetention <= 0) return;
+
+        // 保留数据的起点
+        var today = DateTime.Today;
+        var endday = today.AddDays(-set.DataRetention);
+
+        XTrace.WriteLine("检查数据分表,保留数据起始日期:{0:yyyy-MM-dd}", endday);
+
+        using var span = _tracer?.NewSpan("ShardTable", $"{endday.ToFullString()}");
+        try
+        {
+            // 所有表
+            var dal = DeviceData.Meta.Session.Dal;
+            var tnames = dal.Tables.Select(e => e.TableName).ToArray();
+            var policy = DeviceData.Meta.ShardPolicy as TimeShardPolicy;
+
+            // 删除旧数据
+            for (var dt = today.AddYears(-1); dt < endday; dt = dt.AddDays(1))
+            {
+                var name = policy.Shard(dt).TableName;
+                if (name.EqualIgnoreCase(tnames))
+                {
+                    try
+                    {
+                        dal.Execute($"Drop Table {name}");
+                    }
+                    catch { }
+                }
+            }
+
+            // 新建今天明天的表
+            var ts = new List<IDataTable>();
+            {
+                var table = DeviceData.Meta.Table.DataTable.Clone() as IDataTable;
+                table.TableName = policy.Shard(today).TableName;
+                ts.Add(table);
+            }
+            {
+                var table = DeviceData.Meta.Table.DataTable.Clone() as IDataTable;
+                table.TableName = policy.Shard(today.AddDays(1)).TableName;
+                ts.Add(table);
+            }
+
+            if (ts.Count > 0)
+            {
+                XTrace.WriteLine("创建或更新数据表[{0}]:{1}", ts.Count, ts.Join(",", e => e.TableName));
+
+                //dal.SetTables(ts.ToArray());
+                dal.Db.CreateMetaData().SetTables(Migration.On, ts.ToArray());
+            }
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, null);
+            throw;
+        }
+
+        XTrace.WriteLine("检查数据表完成");
+    }
+}
\ No newline at end of file
Added +302 -0
diff --git a/Samples/IoTZero/Services/ThingService.cs b/Samples/IoTZero/Services/ThingService.cs
new file mode 100644
index 0000000..48ba707
--- /dev/null
+++ b/Samples/IoTZero/Services/ThingService.cs
@@ -0,0 +1,302 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Caching;
+using NewLife.Data;
+using NewLife.IoT.ThingModels;
+using NewLife.Log;
+using NewLife.Security;
+
+namespace IoTZero.Services;
+
+/// <summary>物模型服务</summary>
+public class ThingService
+{
+    private readonly DataService _dataService;
+    private readonly QueueService _queueService;
+    private readonly MyDeviceService _deviceService;
+    private readonly ICacheProvider _cacheProvider;
+    private readonly IoTSetting _setting;
+    private readonly ITracer _tracer;
+    static Snowflake _snowflake = new();
+
+    /// <summary>
+    /// 实例化物模型服务
+    /// </summary>
+    /// <param name="dataService"></param>
+    /// <param name="queueService"></param>
+    /// <param name="ruleService"></param>
+    /// <param name="segmentService"></param>
+    /// <param name="deviceService"></param>
+    /// <param name="cacheProvider"></param>
+    /// <param name="setting"></param>
+    /// <param name="tracer"></param>
+    public ThingService(DataService dataService, QueueService queueService, MyDeviceService deviceService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer)
+    {
+        _dataService = dataService;
+        _queueService = queueService;
+        _deviceService = deviceService;
+        _cacheProvider = cacheProvider;
+        _setting = setting;
+        _tracer = tracer;
+    }
+
+    #region 数据存储
+    /// <summary>上报数据</summary>
+    /// <param name="device"></param>
+    /// <param name="model"></param>
+    /// <param name="kind"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public Int32 PostData(Device device, DataModels model, String kind, String ip)
+    {
+        var rs = 0;
+        foreach (var item in model.Items)
+        {
+            var property = BuildDataPoint(device, item.Name, item.Value, item.Time, ip);
+            if (property != null)
+            {
+                UpdateProperty(property);
+
+                SaveHistory(device, property, item.Time, kind, ip);
+
+                rs++;
+            }
+        }
+
+        // 自动上线
+        if (device != null) _deviceService.SetDeviceOnline(device, ip, kind);
+
+        //todo 触发指定设备的联动策略
+
+        return rs;
+    }
+
+    /// <summary>设备属性上报</summary>
+    /// <param name="device">设备</param>
+    /// <param name="name">属性名</param>
+    /// <param name="value">数值</param>
+    /// <param name="timestamp">时间戳</param>
+    /// <param name="ip">IP地址</param>
+    /// <returns></returns>
+    public DeviceProperty BuildDataPoint(Device device, String name, Object value, Int64 timestamp, String ip)
+    {
+        using var span = _tracer?.NewSpan(nameof(BuildDataPoint), $"{device.Id}-{name}-{value}");
+
+        var entity = GetProperty(device, name);
+        if (entity == null)
+        {
+            var key = $"{device.Id}###{name}";
+            entity = DeviceProperty.GetOrAdd(key,
+                k => DeviceProperty.FindByDeviceIdAndName(device.Id, name),
+                k => new DeviceProperty
+                {
+                    DeviceId = device.Id,
+                    Name = name,
+                    NickName = name,
+                    Enable = true,
+
+                    CreateTime = DateTime.Now,
+                    CreateIP = ip
+                });
+        }
+
+        // 检查是否锁定
+        if (!entity.Enable)
+        {
+            _tracer?.NewError($"{nameof(BuildDataPoint)}-NotEnable", new { name, entity.Enable });
+            return null;
+        }
+
+        //todo 检查数据是否越界
+
+        //todo 修正数字精度,小数点位数
+
+        entity.Name = name;
+        entity.Value = value?.ToString();
+
+        var now = DateTime.Now;
+        entity.TraceId = DefaultSpan.Current?.TraceId;
+        entity.UpdateTime = now;
+        entity.UpdateIP = ip;
+
+        return entity;
+    }
+
+    /// <summary>更新属性</summary>
+    /// <param name="property"></param>
+    /// <returns></returns>
+    public Boolean UpdateProperty(DeviceProperty property)
+    {
+        if (property == null) return false;
+
+        //todo 如果短时间内数据没有变化(无脏数据),则不需要保存属性
+        //var hasDirty = (property as IEntity).Dirtys[nameof(property.Value)];
+
+        // 新属性直接更新,其它异步更新
+        if (property.Id == 0)
+            property.Insert();
+        else
+            property.SaveAsync();
+
+        return true;
+    }
+
+    /// <summary>保存历史数据,写入属性表、数据表、分段数据表</summary>
+    /// <param name="device"></param>
+    /// <param name="property"></param>
+    /// <param name="timestamp"></param>
+    /// <param name="kind"></param>
+    /// <param name="ip"></param>
+    public void SaveHistory(Device device, DeviceProperty property, Int64 timestamp, String kind, String ip)
+    {
+        using var span = _tracer?.NewSpan("thing:SaveHistory", new { deviceName = device.Name, property.Name, property.Value, property.Type });
+        try
+        {
+            // 记录数据流水,使用经过处理的属性数值字段
+            var id = 0L;
+            var data = _dataService.AddData(property.DeviceId, property.Id, timestamp, property.Name, property.Value, kind, ip);
+            if (data != null) id = data.Id;
+
+            //todo 存储分段数据
+
+            //todo 推送队列
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, property);
+
+            throw;
+        }
+    }
+
+    /// <summary>获取设备属性对象,长时间缓存,便于加速属性保存</summary>
+    /// <param name="device"></param>
+    /// <param name="name"></param>
+    /// <returns></returns>
+    private DeviceProperty GetProperty(Device device, String name)
+    {
+        var key = $"DeviceProperty:{device.Id}:{name}";
+        if (_cacheProvider.InnerCache.TryGetValue<DeviceProperty>(key, out var property)) return property;
+
+        using var span = _tracer?.NewSpan(nameof(GetProperty), $"{device.Id}-{name}");
+
+        //var entity = device.Properties.FirstOrDefault(e => e.Name.EqualIgnoreCase(name));
+        var entity = DeviceProperty.FindByDeviceIdAndName(device.Id, name);
+        if (entity != null)
+            _cacheProvider.InnerCache.Set(key, entity, 600);
+
+        return entity;
+    }
+    #endregion
+
+    #region 属性功能
+    /// <summary>获取设备属性</summary>
+    /// <param name="device">设备</param>
+    /// <param name="names">属性名集合</param>
+    /// <returns></returns>
+    public PropertyModel[] GetProperty(Device device, String[] names)
+    {
+        var list = new List<PropertyModel>();
+        foreach (var item in device.Properties)
+        {
+            // 转换得到的属性是只读,不会返回到设备端,可以人为取消只读,此时返回设备端。
+            if (item.Enable && (names == null || names.Length == 0 || item.Name.EqualIgnoreCase(names)))
+            {
+                list.Add(new PropertyModel { Name = item.Name, Value = item.Value });
+
+                item.SaveAsync();
+            }
+        }
+
+        return list.ToArray();
+    }
+
+    /// <summary>查询设备属性。应用端调用</summary>
+    /// <param name="device">设备编码</param>
+    /// <param name="names">属性名集合</param>
+    /// <returns></returns>
+    public PropertyModel[] QueryProperty(Device device, String[] names)
+    {
+        var list = new List<PropertyModel>();
+        foreach (var item in device.Properties)
+        {
+            // 如果未指定属性名,则返回全部
+            if (item.Enable && (names == null || names.Length == 0 || item.Name.EqualIgnoreCase(names)))
+                list.Add(new PropertyModel { Name = item.Name, Value = item.Value });
+        }
+
+        return list.ToArray();
+    }
+    #endregion
+
+    #region 事件
+    /// <summary>设备事件上报</summary>
+    /// <param name="device"></param>
+    /// <param name="events"></param>
+    /// <param name="ip"></param>
+    /// <returns></returns>
+    public Int32 PostEvent(Device device, EventModel[] events, String ip) => throw new NotImplementedException();
+
+    /// <summary>设备事件上报</summary>
+    /// <param name="device"></param>
+    /// <param name="event"></param>
+    /// <param name="ip"></param>
+    public void PostEvent(Device device, EventModel @event, String ip) => throw new NotImplementedException();
+    #endregion
+
+    #region 服务调用
+    /// <summary>调用服务</summary>
+    /// <param name="device"></param>
+    /// <param name="command"></param>
+    /// <param name="argument"></param>
+    /// <param name="expire"></param>
+    /// <returns></returns>
+    /// <exception cref="InvalidOperationException"></exception>
+    public ServiceModel InvokeService(Device device, String command, String argument, DateTime expire)
+    {
+        var traceId = DefaultSpan.Current?.TraceId;
+
+        var log = new ServiceModel
+        {
+            Id = Rand.Next(),
+            Name = command,
+            InputData = argument,
+            Expire = expire,
+            TraceId = traceId,
+        };
+
+        return log;
+    }
+
+    ///// <summary>服务响应</summary>
+    ///// <param name="device"></param>
+    ///// <param name="model"></param>
+    ///// <returns></returns>
+    ///// <exception cref="InvalidOperationException"></exception>
+    //public DeviceServiceLog ServiceReply(Device device, ServiceReplyModel model) => throw new NotImplementedException();
+
+    /// <summary>异步调用服务,并等待响应</summary>
+    /// <param name="device"></param>
+    /// <param name="command"></param>
+    /// <param name="argument"></param>
+    /// <param name="expire"></param>
+    /// <param name="timeout"></param>
+    /// <returns></returns>
+    public async Task<ServiceReplyModel> InvokeServiceAsync(Device device, String command, String argument, DateTime expire, Int32 timeout)
+    {
+        var model = InvokeService(device, command, argument, expire);
+
+        _queueService.Publish(device.Code, model);
+
+        var reply = new ServiceReplyModel { Id = model.Id };
+
+        // 挂起等待。借助redis队列,等待响应
+        if (timeout > 1000)
+        {
+            throw new NotImplementedException();
+        }
+
+        return reply;
+    }
+    #endregion
+}
\ No newline at end of file
Deleted +0 -1
Samples/ZeroIoT