NewLife/ZeroIoT

数据初始化
大石头 authored at 2023-05-23 18:01:26
6e4de91
Tree
1 Parent(s) f873d6d
Summary: 14 changed files with 403 additions and 247 deletions.
Modified +8 -6
Modified +25 -55
Modified +71 -62
Modified +42 -55
Modified +7 -23
Modified +0 -0
Modified +2 -1
Modified +8 -0
Added +43 -0
Added +49 -0
Modified +0 -37
Added +9 -0
Modified +17 -8
Added +122 -0
Modified +8 -6
diff --git a/IoT.Data/Entity/Model.xml b/IoT.Data/Entity/Model.xml
index 370fccc..289ee92 100644
--- a/IoT.Data/Entity/Model.xml
+++ b/IoT.Data/Entity/Model.xml
@@ -2,15 +2,13 @@
 <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>.\</Output>
     <!--是否使用中文文件名。默认false-->
     <ChineseFileName>False</ChineseFileName>
     <!--基类。可能包含基类和接口,其中{name}替换为Table.Name-->
     <BaseClass>Entity</BaseClass>
     <!--命名空间-->
     <Namespace>IoT.Data</Namespace>
-    <!--是否分部类-->
-    <Partial>False</Partial>
     <!--类名模板。其中{name}替换为Table.Name,如{name}Model/I{name}Dto等-->
     <ClassNameTemplate />
     <!--显示名模板。其中{displayName}替换为Table.DisplayName-->
@@ -25,18 +23,22 @@
     <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.0505</Version>
+    <Version>11.8.2023.0523</Version>
     <!--帮助文档-->
     <Document>https://newlifex.com/xcode/model</Document>
     <!--魔方区域显示名-->
-    <DisplayName />
+    <DisplayName>设备管理</DisplayName>
     <!--魔方控制器输出目录-->
-    <CubeOutput />
+    <CubeOutput>../../IoTZero/Areas/IoT/</CubeOutput>
   </Option>
   <Table Name="Product" Description="产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。">
     <Columns>
Modified +25 -55
diff --git "a/IoT.Data/Entity/\344\272\247\345\223\201.Biz.cs" "b/IoT.Data/Entity/\344\272\247\345\223\201.Biz.cs"
index e009222..8aeeb42 100644
--- "a/IoT.Data/Entity/\344\272\247\345\223\201.Biz.cs"
+++ "b/IoT.Data/Entity/\344\272\247\345\223\201.Biz.cs"
@@ -1,7 +1,11 @@
 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;
 
@@ -32,64 +36,30 @@ public partial class Product : Entity<Product>
         // 建议先调用基类方法,基类方法会做一些统一处理
         base.Valid(isNew);
 
-        // 在新插入数据或者修改了指定字段时进行修正
-        // 处理当前已登录用户信息,可以由UserModule过滤器代劳
-        /*var user = ManageProvider.User;
-        if (user != null)
-        {
-            if (isNew && !Dirtys[nameof(CreateUserId)]) CreateUserId = user.ID;
-            if (!Dirtys[nameof(UpdateUserId)]) UpdateUserId = user.ID;
-        }*/
-        //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(Code));
+        // 自动编码
+        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();
-    //    entity.Name = "abc";
-    //    entity.Code = "abc";
-    //    entity.Enable = true;
-    //    entity.DeviceCount = 0;
-    //    entity.CreateUser = "abc";
-    //    entity.CreateUserId = 0;
-    //    entity.CreateTime = DateTime.Now;
-    //    entity.CreateIP = "abc";
-    //    entity.UpdateUser = "abc";
-    //    entity.UpdateUserId = 0;
-    //    entity.UpdateTime = DateTime.Now;
-    //    entity.UpdateIP = "abc";
-    //    entity.Remark = "abc";
-    //    entity.Insert();
-
-    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化Product[产品]数据!");
-    //}
-
-    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
-    ///// <returns></returns>
-    //public override Int32 Insert()
-    //{
-    //    return base.Insert();
-    //}
+    /// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    protected override void InitData()
+    {
+        // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+        if (Meta.Session.Count > 0) return;
 
-    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
-    ///// <returns></returns>
-    //protected override Int32 OnDelete()
-    //{
-    //    return base.OnDelete();
-    //}
+        if (XTrace.Debug) XTrace.WriteLine("开始初始化Product[产品]数据……");
+
+        var entity = new Product
+        {
+            Name = "测试产品",
+            Code = "test",
+            Enable = true,
+        };
+        entity.Insert();
+
+        if (XTrace.Debug) XTrace.WriteLine("完成初始化Product[产品]数据!");
+    }
     #endregion
 
     #region 扩展属性
Modified +71 -62
diff --git "a/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs" "b/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
index 271b261..add4f96 100644
--- "a/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
+++ "b/IoT.Data/Entity/\350\256\276\345\244\207.Biz.cs"
@@ -1,11 +1,15 @@
 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;
@@ -18,13 +22,20 @@ public partial class Device : Entity<Device>
     static Device()
     {
         // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
-        //var df = Meta.Factory.AdditionalFields;
-        //df.Add(nameof(ProductId));
+        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>
@@ -34,69 +45,50 @@ public partial class Device : Entity<Device>
         // 如果没有脏数据,则不需要进行任何处理
         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);
 
-        // 在新插入数据或者修改了指定字段时进行修正
-        // 处理当前已登录用户信息,可以由UserModule过滤器代劳
-        /*var user = ManageProvider.User;
-        if (user != null)
-        {
-            if (isNew && !Dirtys[nameof(CreateUserId)]) CreateUserId = user.ID;
-            if (!Dirtys[nameof(UpdateUserId)]) UpdateUserId = user.ID;
-        }*/
-        //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(Code));
+        // 自动编码
+        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();
-    //    entity.Name = "abc";
-    //    entity.Code = "abc";
-    //    entity.ProductId = 0;
-    //    entity.Enable = true;
-    //    entity.Online = true;
-    //    entity.Uuid = "abc";
-    //    entity.Location = "abc";
-    //    entity.PollingTime = 0;
-    //    entity.CreateUserId = 0;
-    //    entity.CreateTime = DateTime.Now;
-    //    entity.CreateIP = "abc";
-    //    entity.UpdateUserId = 0;
-    //    entity.UpdateTime = DateTime.Now;
-    //    entity.UpdateIP = "abc";
-    //    entity.Remark = "abc";
-    //    entity.Insert();
-
-    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化Device[设备]数据!");
-    //}
-
-    ///// <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();
-    //}
+    /// <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 扩展属性
@@ -140,7 +132,8 @@ public partial class Device : Entity<Device>
         // 实体缓存
         if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Code.EqualIgnoreCase(code));
 
-        return Find(_.Code == code);
+        //return Find(_.Code == code);
+        return Meta.SingleCache.GetItemWithSlaveKey(code) as Device;
     }
 
     /// <summary>根据产品查找</summary>
@@ -173,17 +166,19 @@ public partial class Device : Entity<Device>
     #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, Boolean? enable, DateTime start, DateTime end, String key, PageParameter page)
+    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);
@@ -200,6 +195,20 @@ public partial class Device : Entity<Device>
     /// <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 业务操作
Modified +42 -55
diff --git "a/IoT.Data/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs" "b/IoT.Data/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs"
index 0defb03..ef5fe95 100644
--- "a/IoT.Data/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs"
+++ "b/IoT.Data/Entity/\350\256\276\345\244\207\345\210\206\347\273\204.Biz.cs"
@@ -15,6 +15,7 @@ using NewLife.Data;
 using NewLife.Log;
 using NewLife.Model;
 using NewLife.Reflection;
+using NewLife.Remoting;
 using NewLife.Threading;
 using NewLife.Web;
 using XCode;
@@ -51,64 +52,30 @@ public partial class DeviceGroup : Entity<DeviceGroup>
         // 建议先调用基类方法,基类方法会做一些统一处理
         base.Valid(isNew);
 
-        // 在新插入数据或者修改了指定字段时进行修正
-        // 处理当前已登录用户信息,可以由UserModule过滤器代劳
-        /*var user = ManageProvider.User;
-        if (user != null)
-        {
-            if (isNew && !Dirtys[nameof(CreateUserId)]) CreateUserId = user.ID;
-            if (!Dirtys[nameof(UpdateUserId)]) UpdateUserId = user.ID;
-        }*/
-        //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(ParentId), nameof(Name));
+        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();
-    //    entity.Name = "abc";
-    //    entity.ParentId = 0;
-    //    entity.Sort = 0;
-    //    entity.Devices = 0;
-    //    entity.Activations = 0;
-    //    entity.Onlines = 0;
-    //    entity.CreateUserId = 0;
-    //    entity.CreateTime = DateTime.Now;
-    //    entity.CreateIP = "abc";
-    //    entity.UpdateUserId = 0;
-    //    entity.UpdateTime = DateTime.Now;
-    //    entity.UpdateIP = "abc";
-    //    entity.Remark = "abc";
-    //    entity.Insert();
-
-    //    if (XTrace.Debug) XTrace.WriteLine("完成初始化DeviceGroup[设备分组]数据!");
-    //}
-
-    ///// <summary>已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert</summary>
-    ///// <returns></returns>
-    //public override Int32 Insert()
-    //{
-    //    return base.Insert();
-    //}
+    /// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    protected override void InitData()
+    {
+        // InitData一般用于当数据表没有数据时添加一些默认数据,该实体类的任何第一次数据库操作都会触发该方法,默认异步调用
+        if (Meta.Session.Count > 0) return;
 
-    ///// <summary>已重载。在事务保护范围内处理业务,位于Valid之后</summary>
-    ///// <returns></returns>
-    //protected override Int32 OnDelete()
-    //{
-    //    return base.OnDelete();
-    //}
+        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 扩展属性
@@ -190,5 +157,25 @@ public partial class DeviceGroup : Entity<DeviceGroup>
     #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
 }
Modified +7 -23
diff --git "a/IoT.Data/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs" "b/IoT.Data/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs"
index 1c2ec7c..5ec8767 100644
--- "a/IoT.Data/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs"
+++ "b/IoT.Data/Entity/\350\256\276\345\244\207\346\225\260\346\215\256.Biz.cs"
@@ -1,26 +1,11 @@
 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;
 
@@ -31,15 +16,14 @@ public partial class DeviceData : Entity<DeviceData>
     #region 对象操作
     static DeviceData()
     {
-        // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
-        //var df = Meta.Factory.AdditionalFields;
-        //df.Add(nameof(DeviceId));
+        Meta.Table.DataTable.InsertOnly = true;
+
         // 按天分表
-        //Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
-        //{
-        //    TablePolicy = "{{0}}_{{1:yyyyMMdd}}",
-        //    Step = TimeSpan.FromDays(1),
-        //};
+        Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
+        {
+            TablePolicy = "{0}_{1:yyyyMMdd}",
+            Step = TimeSpan.FromDays(1),
+        };
 
         // 过滤器 UserModule、TimeModule、IPModule
         Meta.Modules.Add<TimeModule>();
Modified +0 -0
diff --git a/IoT.Data/xcodetool.exe b/IoT.Data/xcodetool.exe
index e4b8ea3..51c2c0b 100644
Binary files a/IoT.Data/xcodetool.exe and b/IoT.Data/xcodetool.exe differ
Modified +2 -1
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceController.cs b/IoTZero/Areas/IoT/Controllers/DeviceController.cs
index 59dcd7d..3ae3f83 100644
--- a/IoTZero/Areas/IoT/Controllers/DeviceController.cs
+++ b/IoTZero/Areas/IoT/Controllers/DeviceController.cs
@@ -54,6 +54,7 @@ public class DeviceController : EntityController<Device>
         }
 
         var productId = p["productId"].ToInt(-1);
+        var groupId = p["groupId"].ToInt(-1);
         var enable = p["enable"]?.ToBoolean();
 
         var start = p["dtStart"].ToDateTime();
@@ -62,7 +63,7 @@ public class DeviceController : EntityController<Device>
         //// 如果没有指定产品和主设备,则过滤掉子设备
         //if (productId < 0 && parentId < 0) parentId = 0;
 
-        return Device.Search(productId, enable, start, end, p["Q"], p);
+        return Device.Search(productId, groupId, enable, start, end, p["Q"], p);
     }
 
     protected override Int32 OnInsert(Device entity)
Modified +8 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs b/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
index f9bfa9d..ee684cb 100644
--- a/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
+++ b/IoTZero/Areas/IoT/Controllers/DeviceDataController.cs
@@ -84,24 +84,32 @@ public class DeviceDataController : EntityController<DeviceData>
                     }
                     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;
Added +43 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs b/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs
new file mode 100644
index 0000000..ddb050d
--- /dev/null
+++ b/IoTZero/Areas/IoT/Controllers/DeviceGroupController.cs
@@ -0,0 +1,43 @@
+using IoT.Data;
+using Microsoft.AspNetCore.Mvc;
+using NewLife.Cube;
+using NewLife.Web;
+using XCode.Membership;
+
+namespace IoTZero.Areas.IoT.Controllers;
+
+/// <summary>设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。</summary>
+[Menu(50, true, Icon = "fa-table")]
+[IoTArea]
+public class DeviceGroupController : EntityController<DeviceGroup>
+{
+    static DeviceGroupController()
+    {
+        LogOnChange = true;
+
+        ListFields.RemoveField("UpdateUserId", "UpdateIP");
+        ListFields.RemoveCreateField().RemoveRemarkField();
+    }
+
+    /// <summary>高级搜索。列表页查询、导出Excel、导出Json、分享页等使用</summary>
+    /// <param name="p">分页器。包含分页排序参数,以及Http请求参数</param>
+    /// <returns></returns>
+    protected override IEnumerable<DeviceGroup> Search(Pager p)
+    {
+        var name = p["name"];
+        var parentid = p["parentid"].ToInt(-1);
+
+        var start = p["dtStart"].ToDateTime();
+        var end = p["dtEnd"].ToDateTime();
+
+        return DeviceGroup.Search(name, parentid, start, end, p["Q"], p);
+    }
+
+    [EntityAuthorize(PermissionFlags.Update)]
+    public ActionResult Refresh()
+    {
+        DeviceGroup.Refresh();
+
+        return JsonRefresh("成功!", 1);
+    }
+}
\ No newline at end of file
Added +49 -0
diff --git a/IoTZero/Areas/IoT/Controllers/DeviceOnlineController.cs b/IoTZero/Areas/IoT/Controllers/DeviceOnlineController.cs
new file mode 100644
index 0000000..d757c71
--- /dev/null
+++ b/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(40, 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
Modified +0 -37
diff --git a/IoTZero/Areas/IoT/Controllers/ProductController.cs b/IoTZero/Areas/IoT/Controllers/ProductController.cs
index 7637242..8b6939f 100644
--- a/IoTZero/Areas/IoT/Controllers/ProductController.cs
+++ b/IoTZero/Areas/IoT/Controllers/ProductController.cs
@@ -1,13 +1,11 @@
 using System.ComponentModel;
 using IoT.Data;
 using NewLife.Cube;
-using NewLife.Cube.ViewModels;
 using NewLife.Web;
 
 namespace IoTZero.Areas.IoT.Controllers;
 
 [IoTArea]
-[DisplayName("产品定义")]
 [Menu(90, true, Icon = "fa-product-hunt")]
 public class ProductController : EntityController<Product>
 {
@@ -19,41 +17,6 @@ public class ProductController : EntityController<Product>
         ListFields.RemoveCreateField();
 
         {
-            var df = ListFields.GetField("DeviceCount") as ListField;
-            df.DisplayName = "{DeviceCount}";
-            df.Url = "/IoT/Device?productId={Id}";
-            //df.DataVisible = (e, f) => (e as Product).DeviceCount > 0;
-        }
-
-        {
-            var df = ListFields.AddListField("function", "UpdateUser");
-            df.DisplayName = "功能定义";
-            df.Url = "/IoT/ProductFunction?productId={Id}";
-            df.Title = "产品的物模型属性";
-        }
-
-        {
-            var df = ListFields.AddListField("tsl", "UPdateUser");
-            df.DisplayName = "功能定义TSL";
-            df.Url = "/IoT/TSL/Edit?productId={Id}";
-            df.Title = "TSL模型";
-        }
-
-        {
-            var df = ListFields.AddListField("publish", "UPdateUser");
-            df.DisplayName = "功能发布";
-            df.Url = "/IoT/ProductFunction/PublishBatch?productId={Id}";
-            df.Title = "批量发布产品功能定义";
-            //df.DataVisible = e => (e as Product).DeviceCount > 0;
-        }
-
-        {
-            var df = ListFields.AddListField("rule", "UpdateUser");
-            df.DisplayName = "规则策略";
-            df.Url = "/IoT/RulePolicy?productId={Id}";
-        }
-
-        {
             var df = ListFields.AddListField("Log");
             df.DisplayName = "日志";
             df.Url = "/Admin/Log?category=产品&linkId={Id}";
Added +9 -0
diff --git a/IoTZero/Areas/IoT/Views/DeviceGroup/_List_Toolbar_Batch.cshtml b/IoTZero/Areas/IoT/Views/DeviceGroup/_List_Toolbar_Batch.cshtml
new file mode 100644
index 0000000..897a0ec
--- /dev/null
+++ b/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
Modified +17 -8
diff --git a/IoTZero/Program.cs b/IoTZero/Program.cs
index 6e67ba9..0c58475 100644
--- a/IoTZero/Program.cs
+++ b/IoTZero/Program.cs
@@ -1,7 +1,9 @@
-using IoTZero.Services;
+using IoTZero;
+using IoTZero.Services;
 using NewLife.Caching;
 using NewLife.Cube;
 using NewLife.Log;
+using NewLife.Security;
 using XCode;
 
 // 日志输出到控制台,并拦截全局异常
@@ -38,16 +40,24 @@ if (set3.IsNew)
     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.AddControllersWithViews();
 
@@ -73,11 +83,10 @@ app.UseCube(app.Environment);
 
 app.UseAuthorization();
 
-app.UseEndpoints(endpoints =>
-{
-    endpoints.MapControllerRoute(
-        name: "default",
-        pattern: "{controller=CubeHome}/{action=Index}/{id?}");
-});
+app.MapControllerRoute(
+    name: "default",
+    pattern: "{controller=CubeHome}/{action=Index}/{id?}");
+
+app.RegisterService("AlarmServer", null, app.Environment.EnvironmentName);
 
 app.Run();
Added +122 -0
diff --git a/IoTZero/Services/ShardTableService.cs b/IoTZero/Services/ShardTableService.cs
new file mode 100644
index 0000000..a0abb4d
--- /dev/null
+++ b/IoTZero/Services/ShardTableService.cs
@@ -0,0 +1,122 @@
+using IoT.Data;
+using NewLife;
+using NewLife.Log;
+using NewLife.Threading;
+using XCode;
+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 ss = EntitySession<DeviceData>.Create(dal.ConnName, table.TableName);
+                ss.Queue.MaxEntity = 10_000_000;
+            }
+            {
+                var table = DeviceData.Meta.Table.DataTable.Clone() as IDataTable;
+                table.TableName = policy.Shard(today.AddDays(1)).TableName;
+                ts.Add(table);
+
+                var ss = EntitySession<DeviceData>.Create(dal.ConnName, table.TableName);
+                ss.Queue.MaxEntity = 10_000_000;
+            }
+
+            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