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>
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 扩展属性
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 业务操作
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
}
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>();
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
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)
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;
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
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
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}";
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
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();
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