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"
+ }
+}
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
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
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
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
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
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
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
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
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
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
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
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
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>
+ <a href="?@url&sample=60">每分钟</a>
+ <a href="?@url&sample=900">每15钟</a>
+ <a href="?@url&sample=3600">每小时</a>
+</div>
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
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
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
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
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
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
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
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
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>
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
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
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
+}
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
+}
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
+}
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
+}
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
+}
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
+}
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
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
+}
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
+}
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
+}
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
+}
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
+}
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
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
+}
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
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>
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
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
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
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
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
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; }
+}
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
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();
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
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
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
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
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
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