diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 7d8b7f5..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "Samples/ZeroIoT"] - path = Samples/ZeroIoT - url = https://github.com/NewLifeX/ZeroIoT diff --git a/NewLife.Remoting.Extensions/Common/BaseController.cs b/NewLife.Remoting.Extensions/Common/BaseController.cs index ad5eb27..30c0a37 100644 --- a/NewLife.Remoting.Extensions/Common/BaseController.cs +++ b/NewLife.Remoting.Extensions/Common/BaseController.cs @@ -8,7 +8,6 @@ using NewLife.Remoting.Extensions.Services; using NewLife.Serialization; using NewLife.Web; -using static NewLife.Remoting.ApiHttpClient; using IWebFilter = Microsoft.AspNetCore.Mvc.Filters.IActionFilter; namespace NewLife.Remoting.Extensions; @@ -25,16 +24,13 @@ public abstract class BaseController : ControllerBase, IWebFilter /// 令牌 public String? Token { get; private set; } - /// 应用信息 - public IAppInfo App { get; set; } = null!; - /// 令牌对象 public JwtBuilder Jwt { get; set; } = null!; /// 用户主机 public String UserHost => HttpContext.GetUserHost(); - private IDictionary? _args; + private IDictionary? _args; private readonly TokenService _tokenService; private readonly ITokenSetting _setting; #endregion @@ -81,11 +77,10 @@ void IWebFilter.OnActionExecuting(ActionExecutingContext context) /// protected virtual Boolean OnAuthorize(String token) { - var (jwt, app) = _tokenService.DecodeToken(token, _setting.TokenSecret); - App = app; + var jwt = _tokenService.DecodeToken(token, _setting.TokenSecret); Jwt = jwt; - return app != null; + return jwt != null; } void IWebFilter.OnActionExecuted(ActionExecutedContext context) @@ -113,6 +108,6 @@ private void WriteError(Exception ex, ActionContext context) /// /// /// - protected virtual void WriteLog(String action, Boolean success, String message) => App.WriteLog(action, success, message, UserHost, Jwt?.Id); + protected virtual void WriteLog(String action, Boolean success, String message) => XTrace.WriteLine($"[{action}]{message}"); #endregion } \ No newline at end of file diff --git a/NewLife.Remoting.Extensions/Common/OAuthController.cs b/NewLife.Remoting.Extensions/Common/OAuthController.cs index ce9f5ad..fad21af 100644 --- a/NewLife.Remoting.Extensions/Common/OAuthController.cs +++ b/NewLife.Remoting.Extensions/Common/OAuthController.cs @@ -8,13 +8,14 @@ namespace NewLife.Remoting.Extensions; /// OAuth服务。向应用提供验证服务 [Route("[controller]/[action]")] -public class OAuthController : ControllerBase +public abstract class OAuthController : ControllerBase where TApp : IAppInfo { private readonly TokenService _tokenService; private readonly ITokenSetting _setting; /// 实例化 /// + /// public OAuthController(TokenService tokenService, ITokenSetting setting) { _tokenService = tokenService; @@ -40,7 +41,9 @@ public TokenModel Token([FromBody] TokenInModel model) // 密码模式 if (model.grant_type == "password") { - var app = _tokenService.Authorize(model.UserName, model.Password, set.AutoRegister, ip); + if (model.UserName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.UserName)); + + var app = Authorize(model.UserName, model.Password, set.AutoRegister, ip); var tokenModel = _tokenService.IssueToken(app.Name, set.TokenSecret, set.TokenExpire, clientId); @@ -51,12 +54,14 @@ public TokenModel Token([FromBody] TokenInModel model) // 刷新令牌 else if (model.grant_type == "refresh_token") { + if (model.refresh_token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.refresh_token)); + var (jwt, ex) = _tokenService.DecodeTokenWithError(model.refresh_token, set.TokenSecret); // 验证应用 - var app = _tokenService.Provider.FindByName(jwt?.Subject); + var app = FindByName(jwt?.Subject); if (app == null || !app.Enable) - ex ??= new ApiException(403, $"无效应用[{jwt.Subject}]"); + ex ??= new ApiException(ApiCode.Forbidden, $"无效应用[{jwt.Subject}]"); if (clientId.IsNullOrEmpty()) clientId = jwt.Id; @@ -77,13 +82,48 @@ public TokenModel Token([FromBody] TokenInModel model) } catch (Exception ex) { - var app = _tokenService.Provider.FindByName(model.UserName); + var app = FindByName(model.UserName!); app?.WriteLog("Authorize", false, ex.ToString(), ip, clientId); throw; } } + /// 验证应用密码,不存在时新增 + /// + /// + /// + /// + /// + protected TApp Authorize(String username, String? password, Boolean autoRegister, String? ip = null) + { + if (username.IsNullOrEmpty()) throw new ArgumentNullException(nameof(username)); + //if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password)); + + // 查找应用 + var app = FindByName(username); + // 查找或创建应用,避免多线程创建冲突 + app ??= Register(username, password, autoRegister, ip); + if (app == null) throw new ApiException(ApiCode.NotFound, $"[{username}]无效!"); + + //// 检查黑白名单 + //if (!app.ValidSource(ip)) + // throw new ApiException(ApiCode.Forbidden, $"应用[{username}]禁止{ip}访问!"); + + // 检查应用有效性 + if (!app.Enable) throw new ApiException(ApiCode.Forbidden, $"[{username}]已禁用!"); + //if (!app.Secret.IsNullOrEmpty() && password != app.Secret) throw new ApiException(401, $"非法访问应用[{username}]!"); + if (!OnAuthorize(app, password, ip)) throw new ApiException(ApiCode.Unauthorized, $"非法访问[{username}]!"); + + return app; + } + + protected abstract TApp FindByName(String username); + + protected abstract TApp Register(String username, String? password, Boolean autoRegister, String? ip = null); + + protected abstract Boolean OnAuthorize(TApp app, String? password, String? ip = null); + /// 根据令牌获取应用信息,同时也是验证令牌是否有效 /// /// @@ -91,19 +131,21 @@ public TokenModel Token([FromBody] TokenInModel model) public Object Info(String token) { var set = _setting; - var (_, app) = _tokenService.DecodeToken(token, set.TokenSecret); + var jwt = _tokenService.DecodeToken(token, set.TokenSecret); + var name = jwt?.Subject; + var app = name.IsNullOrEmpty() ? default : FindByName(name); if (app is IModel model) return new { Id = model["Id"], - app.Name, + Name = name, DisplayName = model["DisplayName"], Category = model["Category"], }; else return new { - app.Name, + Name = name, }; } } \ No newline at end of file diff --git a/NewLife.Remoting.Extensions/Models/IAppInfo.cs b/NewLife.Remoting.Extensions/Models/IAppInfo.cs new file mode 100644 index 0000000..89320f0 --- /dev/null +++ b/NewLife.Remoting.Extensions/Models/IAppInfo.cs @@ -0,0 +1,25 @@ +namespace NewLife.Remoting.Extensions.Models; + +/// 应用信息接口 +public interface IAppInfo +{ + /// 名称 + String Name { get; } + + /// 启用 + Boolean Enable { get; } + + /// 验证授权 + /// + /// + /// + Boolean Authorize(String? password, String? ip = null); + + /// 写日志 + /// + /// + /// + /// + /// + void WriteLog(String action, Boolean success, String message, String? ip, String? clientId); +} \ No newline at end of file diff --git a/NewLife.Remoting.Extensions/Models/IAppProvider.cs b/NewLife.Remoting.Extensions/Models/IAppProvider.cs deleted file mode 100644 index 94ccf37..0000000 --- a/NewLife.Remoting.Extensions/Models/IAppProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace NewLife.Remoting.Extensions.Models; - -public interface IAppInfo -{ - String Name { get; } - - Boolean Enable { get; } - - Boolean Authorize(String password, String? ip = null); - - void WriteLog(String action, Boolean success, String message, String? ip, String? clientId); -} - -public interface IAppProvider -{ - IAppInfo? FindByName(String? name); - - IAppInfo? Register(String username, String password, Boolean autoRegister, String? ip = null); -} diff --git a/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj b/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj index e0cb246..1c49381 100644 --- a/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj +++ b/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj @@ -2,7 +2,7 @@ Library - net5.0;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 RPC服务扩展 RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架 新生命开发团队 diff --git a/NewLife.Remoting.Extensions/Services/TokenService.cs b/NewLife.Remoting.Extensions/Services/TokenService.cs index 102da2d..8fa6795 100644 --- a/NewLife.Remoting.Extensions/Services/TokenService.cs +++ b/NewLife.Remoting.Extensions/Services/TokenService.cs @@ -1,5 +1,4 @@ using System.Reflection; -using NewLife.Remoting.Extensions.Models; using NewLife.Security; using NewLife.Web; @@ -8,44 +7,11 @@ namespace NewLife.Remoting.Extensions.Services; /// 应用服务 public class TokenService { - #region 公共 - /// 应用信息提供者 - public IAppProvider? Provider { get; set; } - #endregion - - /// 验证应用密码,不存在时新增 - /// - /// - /// - /// - /// - public IAppInfo Authorize(String username, String password, Boolean autoRegister, String? ip = null) - { - if (username.IsNullOrEmpty()) throw new ArgumentNullException(nameof(username)); - //if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password)); - if (Provider == null) throw new ArgumentNullException(nameof(Provider)); - - // 查找应用 - var app = Provider.FindByName(username); - // 查找或创建应用,避免多线程创建冲突 - app ??= Provider.Register(username, password, autoRegister, ip); - - //// 检查黑白名单 - //if (!app.ValidSource(ip)) - // throw new ApiException(403, $"应用[{username}]禁止{ip}访问!"); - - // 检查应用有效性 - if (!app.Enable) throw new ApiException(403, $"应用[{username}]已禁用!"); - //if (!app.Secret.IsNullOrEmpty() && password != app.Secret) throw new ApiException(401, $"非法访问应用[{username}]!"); - if (!app.Authorize(password, ip)) throw new ApiException(401, $"非法访问应用[{username}]!"); - - return app; - } - /// 颁发令牌 /// /// /// + /// /// public TokenModel IssueToken(String name, String secret, Int32 expire, String? id = null) { @@ -55,7 +21,7 @@ public TokenModel IssueToken(String name, String secret, Int32 expire, String? i var ss = secret.Split(':'); var jwt = new JwtBuilder { - Issuer = Assembly.GetEntryAssembly().GetName().Name, + Issuer = Assembly.GetEntryAssembly()?.GetName().Name, Subject = name, Id = id, Expire = DateTime.Now.AddSeconds(expire), @@ -66,10 +32,10 @@ public TokenModel IssueToken(String name, String secret, Int32 expire, String? i return new TokenModel { - AccessToken = jwt.Encode(null), + AccessToken = jwt.Encode(null!), TokenType = jwt.Type ?? "JWT", ExpireIn = expire, - RefreshToken = jwt.Encode(null), + RefreshToken = jwt.Encode(null!), }; } @@ -112,7 +78,7 @@ public TokenModel IssueToken(String name, String secret, Int32 expire, String? i }; Exception? ex = null; - if (!jwt.TryDecode(token, out var message)) ex = new ApiException(403, $"[{jwt.Subject}]非法访问 {message}"); + if (!jwt.TryDecode(token, out var message)) ex = new ApiException(ApiCode.Forbidden, $"[{jwt.Subject}]非法访问 {message}"); return (jwt, ex); } @@ -121,10 +87,9 @@ public TokenModel IssueToken(String name, String secret, Int32 expire, String? i /// /// /// - public (JwtBuilder, IAppInfo) DecodeToken(String token, String tokenSecret) + public JwtBuilder DecodeToken(String token, String tokenSecret) { if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token)); - if (Provider == null) throw new ArgumentNullException(nameof(Provider)); // 解码令牌 var ss = tokenSecret.Split(':'); @@ -134,40 +99,8 @@ public TokenModel IssueToken(String name, String secret, Int32 expire, String? i Secret = ss[1], }; if (!jwt.TryDecode(token, out var message) || jwt.Subject.IsNullOrEmpty()) - throw new ApiException(403, $"非法访问[{jwt.Subject}],{message}"); - - // 验证应用 - var app = Provider.FindByName(jwt.Subject) - ?? throw new ApiException(403, $"无效应用[{jwt.Subject}]"); - if (!app.Enable) throw new ApiException(403, $"已停用应用[{jwt.Subject}]"); - - return (jwt, app); - } - - /// 解码令牌 - /// - /// - /// - public (IAppInfo?, Exception?) TryDecodeToken(String token, String tokenSecret) - { - if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token)); - if (Provider == null) throw new ArgumentNullException(nameof(Provider)); - - // 解码令牌 - var ss = tokenSecret.Split(':'); - var jwt = new JwtBuilder - { - Algorithm = ss[0], - Secret = ss[1], - }; - - Exception? ex = null; - if (!jwt.TryDecode(token, out var message)) ex = new ApiException(403, $"非法访问 {message}"); - - // 验证应用 - var app = Provider.FindByName(jwt.Subject); - if ((app == null || !app.Enable) && ex == null) ex = new ApiException(401, $"无效应用[{jwt.Subject}]"); + throw new ApiException(ApiCode.Forbidden, $"非法访问[{jwt.Subject}],{message}"); - return (app, ex); + return jwt; } } \ No newline at end of file diff --git a/NewLife.Remoting/ApiMessage.cs b/NewLife.Remoting/ApiMessage.cs index 90cb6dd..bbe6986 100644 --- a/NewLife.Remoting/ApiMessage.cs +++ b/NewLife.Remoting/ApiMessage.cs @@ -13,4 +13,8 @@ public class ApiMessage /// 数据。请求参数或响应内容 public Packet? Data { get; set; } + + /// 已重载。友好表示该消息 + /// + public override String ToString() => Code > 0 ? $"{Action}[{Code}]" : Action; } \ No newline at end of file diff --git a/NewLife.Remoting/Clients/ClientBase.cs b/NewLife.Remoting/Clients/ClientBase.cs index aaf0ff2..bbfa6ac 100644 --- a/NewLife.Remoting/Clients/ClientBase.cs +++ b/NewLife.Remoting/Clients/ClientBase.cs @@ -289,7 +289,7 @@ public virtual LoginRequest BuildLoginRequest() { foreach (var model in rs.Commands) { - await ReceiveCommand(model); + await ReceiveCommand(model, "Pong"); } } } @@ -329,7 +329,7 @@ public virtual LoginRequest BuildLoginRequest() } /// 获取心跳信息 - public PingRequest BuildPingRequest() + public virtual PingRequest BuildPingRequest() { var request = new PingRequest { @@ -385,7 +385,11 @@ protected virtual void StopTimer() /// protected virtual Task OnPing(Object state) => Ping(); - async Task ReceiveCommand(CommandModel model) + /// 收到命令 + /// + /// + /// + protected async Task ReceiveCommand(CommandModel model, String source) { if (model == null) return; @@ -399,7 +403,7 @@ async Task ReceiveCommand(CommandModel model) { //todo 有效期判断可能有隐患,现在只是假设服务器和客户端在同一个时区,如果不同,可能会出现问题 var now = GetNow(); - XTrace.WriteLine("Got Command: {0}", model.ToJson()); + XTrace.WriteLine("[{0}] Got Command: {1}", source, model.ToJson()); if (model.Expire.Year < 2000 || model.Expire > now) { // 延迟执行 diff --git a/NewLife.Remoting/Clients/HttpClientBase.cs b/NewLife.Remoting/Clients/HttpClientBase.cs index 2a1736c..5bc82b4 100644 --- a/NewLife.Remoting/Clients/HttpClientBase.cs +++ b/NewLife.Remoting/Clients/HttpClientBase.cs @@ -1,10 +1,8 @@ -using NewLife.Caching; +using System.Diagnostics.CodeAnalysis; +using NewLife.Caching; using NewLife.Log; using NewLife.Remoting.Models; -using NewLife.Threading; using NewLife.Serialization; -using System.Net.Http; -using System.Diagnostics.CodeAnalysis; #if NETCOREAPP using System.Net.WebSockets; @@ -50,6 +48,11 @@ public HttpClientBase(String urls) : this() } } } + + /// 新增服务点 + /// + /// + public void AddService(String name, String url) => _client.Add(name, new Uri(url)); #endregion #region 方法 @@ -232,14 +235,7 @@ private async Task DoPull(WebSocket socket, CancellationToken cancellationToken) { var data = await socket.ReceiveAsync(new ArraySegment(buf), cancellationToken); var txt = buf.ToStr(null, 0, data.Count); - if (txt.StartsWithIgnoreCase("Pong")) - { - } - else - { - var model = txt.ToJsonEntity(); - if (model != null) await ReceiveCommand(model); - } + await OnReceive(txt); } } catch (Exception ex) @@ -251,52 +247,18 @@ private async Task DoPull(WebSocket socket, CancellationToken cancellationToken) } #endif - async Task ReceiveCommand(CommandModel model) + /// 收到服务端主动下发消息。默认转为CommandModel命令处理 + /// + /// + protected virtual async Task OnReceive(String message) { - if (model == null) return; - - // 去重,避免命令被重复执行 - if (!_cache.Add($"cmd:{model.Id}", model, 3600)) return; - - // 建立追踪链路 - using var span = Tracer?.NewSpan("cmd:" + model.Command, model); - if (model.TraceId != null) span?.Detach(model.TraceId); - try + if (message.StartsWithIgnoreCase("Pong")) { - //todo 有效期判断可能有隐患,现在只是假设服务器和客户端在同一个时区,如果不同,可能会出现问题 - //WriteLog("Got Service: {0}", model.ToJson()); - var now = GetNow(); - if (model.Expire.Year < 2000 || model.Expire > now) - { - // 延迟执行 - var ts = model.StartTime - now; - if (ts.TotalMilliseconds > 0) - { - TimerX.Delay(s => - { - _ = OnReceiveCommand(model); - }, (Int32)ts.TotalMilliseconds); - - var reply = new CommandReplyModel - { - Id = model.Id, - Status = CommandStatus.处理中, - Data = $"已安排计划执行 {model.StartTime.ToFullString()}" - }; - await CommandReply(reply); - } - else - await OnReceiveCommand(model); - } - else - { - var rs = new CommandReplyModel { Id = model.Id, Status = CommandStatus.取消 }; - await CommandReply(rs); - } } - catch (Exception ex) + else { - span?.SetError(ex, null); + var model = message.ToJsonEntity(); + if (model != null) await ReceiveCommand(model, "WebSocket"); } } #endregion diff --git a/NewLife.Remoting/Http/WebSocketServerCodec.cs b/NewLife.Remoting/Http/WebSocketServerCodec.cs index ee0b247..375520d 100644 --- a/NewLife.Remoting/Http/WebSocketServerCodec.cs +++ b/NewLife.Remoting/Http/WebSocketServerCodec.cs @@ -71,8 +71,10 @@ public override Boolean Close(IHandlerContext context, String reason) var msg = new WebSocketMessage(); if (msg.Read(pk)) message = msg.Payload; } - - ss["isWs"] = false; + else + { + ss["isWs"] = false; + } return base.Read(context, message); } diff --git a/NewLife.Remoting/IApiHandler.cs b/NewLife.Remoting/IApiHandler.cs index e20fd8c..233fd77 100644 --- a/NewLife.Remoting/IApiHandler.cs +++ b/NewLife.Remoting/IApiHandler.cs @@ -46,10 +46,10 @@ public class ApiHandler : IApiHandler if (action.IsNullOrEmpty()) action = "Api/Info"; var manager = (Host as ApiServer)?.Manager; - var api = manager?.Find(action) ?? throw new ApiException(404, $"无法找到名为[{action}]的服务!"); + var api = manager?.Find(action) ?? throw new ApiException(ApiCode.NotFound, $"无法找到名为[{action}]的服务!"); // 全局共用控制器,或者每次创建对象实例 - var controller = manager.CreateController(api) ?? throw new ApiException(403, $"无法创建名为[{api.Name}]的服务!"); + var controller = manager.CreateController(api) ?? throw new ApiException(ApiCode.Forbidden, $"无法创建名为[{api.Name}]的服务!"); if (controller is IApi capi) capi.Session = session; if (session is INetSession ss) api.LastSession = ss.Remote + ""; diff --git a/NewLife.Remoting/Services/ICommandClient.cs b/NewLife.Remoting/Services/ICommandClient.cs index 222db0e..1fed0f4 100644 --- a/NewLife.Remoting/Services/ICommandClient.cs +++ b/NewLife.Remoting/Services/ICommandClient.cs @@ -133,7 +133,8 @@ public static async Task ExecuteCommand(this ICommandClient c { //WriteLog("OnCommand {0}", model.ToJson()); - if (!client.Commands.TryGetValue(model.Command, out var d)) throw new ApiException(400, $"找不到服务[{model.Command}]"); + if (!client.Commands.TryGetValue(model.Command, out var d)) + throw new ApiException(ApiCode.NotFound, $"找不到服务[{model.Command}]"); if (d is Func> func1) return await func1(model.Argument); //if (d is Func> func2) return await func2(model.Argument); 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 +{ + 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 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 +{ + 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 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(); + 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(); + 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(); + 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; + +/// 设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。 +[Menu(50, true, Icon = "fa-table")] +[IoTArea] +public class DeviceGroupController : EntityController +{ + static DeviceGroupController() + { + LogOnChange = true; + + ListFields.RemoveField("UpdateUserId", "UpdateIP"); + ListFields.RemoveCreateField().RemoveRemarkField(); + } + + /// 高级搜索。列表页查询、导出Excel、导出Json、分享页等使用 + /// 分页器。包含分页排序参数,以及Http请求参数 + /// + protected override IEnumerable 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 +{ + protected override IEnumerable 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; + +/// 设备在线 +[Menu(70, true, Icon = "fa-table")] +[IoTArea] +public class DeviceOnlineController : EntityController +{ + 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}"; + } + } + + /// 高级搜索。列表页查询、导出Excel、导出Json、分享页等使用 + /// 分页器。包含分页排序参数,以及Http请求参数 + /// + protected override IEnumerable 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 +{ + 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(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 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 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 +{ + 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 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(); +} \ 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; +} +
+ + @Html.ForDropDownList("productId", Product.FindAllWithCache(), page["productId"], "全部", true) +
+@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" }); +} +
+ 原始数据   + 每分钟   + 每15钟   + 每小时   +
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; +} + \ 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/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/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; + +/// 配置 +[Config("IoTClient")] +public class ClientSetting : Config +{ + #region 属性 + /// 服务端地址。IoT服务平台地址 + [Description("服务端地址。IoT服务平台地址")] + public String Server { get; set; } = "http://localhost:1880"; + + /// 设备证书。在一机一密时手工填写,一型一密时自动下发 + [Description("设备证书。在一机一密时手工填写,一型一密时自动下发")] + public String DeviceCode { get; set; } + + /// 设备密钥。在一机一密时手工填写,一型一密时自动下发 + [Description("设备密钥。在一机一密时手工填写,一型一密时自动下发")] + public String DeviceSecret { get; set; } + + /// 产品证书。用于一型一密验证,对一机一密无效 + [Description("产品证书。用于一型一密验证,对一机一密无效")] + public String ProductKey { get; set; } = "EdgeGateway"; + #endregion +} \ No newline at end of file diff --git a/Samples/IoTZero/Clients/ClientTest.cs b/Samples/IoTZero/Clients/ClientTest.cs index 5143895..e94d63b 100644 --- a/Samples/IoTZero/Clients/ClientTest.cs +++ b/Samples/IoTZero/Clients/ClientTest.cs @@ -26,7 +26,7 @@ public static async Task Main(IServiceProvider serviceProvider) Log = XTrace.Log, }; - await device.LoginAsync(); + await device.Login(); _device = device; } diff --git a/Samples/IoTZero/Clients/HttpDevice.cs b/Samples/IoTZero/Clients/HttpDevice.cs new file mode 100644 index 0000000..bdb25f9 --- /dev/null +++ b/Samples/IoTZero/Clients/HttpDevice.cs @@ -0,0 +1,116 @@ +using NewLife; +using NewLife.IoT.Models; +using NewLife.IoT.ThingModels; +using NewLife.Log; +using NewLife.Remoting.Clients; +using NewLife.Remoting.Models; +using NewLife.Security; +using LoginResponse = NewLife.Remoting.Models.LoginResponse; + +namespace IoTEdge; + +/// Http协议设备 +public class HttpDevice : HttpClientBase +{ + #region 属性 + /// 产品编码。从IoT管理平台获取 + public String ProductKey { get; set; } + + private readonly ClientSetting _setting; + #endregion + + #region 构造 + public HttpDevice() => Prefix = "Device/"; + + public HttpDevice(ClientSetting setting) : base(setting.Server) + { + Prefix = "Device/"; + + _setting = setting; + + ProductKey = setting.ProductKey; + } + #endregion + + #region 登录注销 + public override LoginRequest BuildLoginRequest() + { + var request = base.BuildLoginRequest(); + + return new LoginInfo + { + Code = request.Code, + Secret = request.Secret, + Version = request.Version, + ClientId = request.ClientId, + + ProductKey = ProductKey, + //ProductSecret = _setting.DeviceSecret, + }; + } + + public override async Task Login() + { + var rs = await base.Login(); + + if (Logined && !rs.Secret.IsNullOrEmpty()) + { + _setting.DeviceCode = rs.Code; + _setting.DeviceSecret = rs.Secret; + _setting.Save(); + } + + return rs; + } + #endregion + + #region 心跳 + public override PingRequest BuildPingRequest() + { + var request = base.BuildPingRequest(); + + return new PingInfo + { + }; + } + + public override Task CommandReply(CommandReplyModel model) => InvokeAsync("Thing/ServiceReply", new ServiceReplyModel + { + Id = model.Id, + Status = (ServiceStatus)model.Status, + Data = model.Data, + }); + #endregion + + #region 数据 + /// 上传数据 + /// + public async Task PostDataAsync() + { + if (Tracer != null) DefaultSpan.Current = null; + + using var span = Tracer?.NewSpan("PostData"); + try + { + var items = new List + { + new() { + Time = DateTime.UtcNow.ToLong(), + Name = "TestValue", + Value = Rand.Next(0, 100) + "" + } + }; + + var data = new DataModels { DeviceCode = Code, Items = items.ToArray() }; + + await InvokeAsync("Thing/PostData", data); + } + catch (Exception ex) + { + span?.SetError(ex, null); + + throw; + } + } + #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; + +/// 统一Api过滤处理 +public sealed class ApiFilterAttribute : ActionFilterAttribute +{ + /// 执行前,验证模型 + /// + 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); + } + + /// 执行后,包装结果和异常 + /// + 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; + +/// 物模型Api控制器。用于应用系统调用 +[ApiFilter] +[ApiController] +[Route("[controller]")] +public class AppController : BaseController +{ + private readonly MyDeviceService _deviceService; + private readonly ThingService _thingService; + private readonly ITracer _tracer; + private IDictionary _args; + + #region 构造 + /// + /// 实例化应用管理服务 + /// + /// + /// + /// + /// + public AppController(IServiceProvider serviceProvider, MyDeviceService deviceService, ThingService thingService, ITracer tracer) : base(serviceProvider) + { + _deviceService = deviceService; + _thingService = thingService; + _tracer = tracer; + } + #endregion + + #region 物模型 + /// 获取设备属性 + /// 设备编号 + /// 设备编码 + /// + [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); + } + + /// 设置设备属性 + /// 数据 + /// + [HttpPost(nameof(SetProperty))] + public Task SetProperty(DevicePropertyModel model) + { + var dv = Device.FindByCode(model.DeviceCode); + if (dv == null) return null; + + throw new NotImplementedException(); + } + + /// 调用设备服务 + /// 服务 + /// + [HttpPost(nameof(InvokeService))] + public async Task 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..df04991 --- /dev/null +++ b/Samples/IoTZero/Controllers/DeviceController.cs @@ -0,0 +1,202 @@ +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; + +/// 设备控制器 +[ApiFilter] +[ApiController] +[Route("[controller]")] +public class DeviceController : BaseController +{ + /// 当前设备 + public Device Device { get; set; } + + private readonly QueueService _queue; + private readonly MyDeviceService _deviceService; + private readonly ThingService _thingService; + private readonly ITracer _tracer; + + #region 构造 + /// 实例化设备控制器 + /// + /// + /// + /// + /// + public DeviceController(IServiceProvider serviceProvider, QueueService queue, MyDeviceService deviceService, ThingService thingService, ITracer tracer) : base(serviceProvider) + { + _queue = queue; + _deviceService = deviceService; + _thingService = thingService; + _tracer = tracer; + } + + protected override Boolean OnAuthorize(String token) + { + if (!base.OnAuthorize(token) || Jwt == null) return false; + + var dv = Device.FindByCode(Jwt.Subject); + if (dv == null || !dv.Enable) throw new ApiException(ApiCode.Forbidden, "无效设备!"); + + Device = dv; + + return true; + } + #endregion + + #region 登录 + /// 设备登录 + /// + /// + [AllowAnonymous] + [HttpPost(nameof(Login))] + public LoginResponse Login(LoginInfo model) => _deviceService.Login(model, "Http", UserHost); + + /// 设备注销 + /// 注销原因 + /// + [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 心跳 + /// 设备心跳 + /// + /// + [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 升级 + /// 升级检查 + /// + [HttpGet(nameof(Upgrade))] + public UpgradeInfo Upgrade() + { + var device = Device ?? throw new ApiException(ApiCode.Unauthorized, "节点未登录"); + + throw new NotImplementedException(); + } + #endregion + + #region 设备通道 + /// 获取设备信息,包括主设备和子设备 + /// + [HttpGet(nameof(GetDevices))] + public DeviceModel[] GetDevices() => throw new NotImplementedException(); + + /// 设备上线。驱动打开后调用,子设备发现,或者上报主设备/子设备的默认参数模版 + /// + /// 有些设备驱动具备扫描发现子设备能力,通过该方法上报设备。 + /// 主设备或子设备,也可通过该方法上报驱动的默认参数模版。 + /// 根据需要,驱动内可能多次调用该方法。 + /// + /// 设备信息集合。可传递参数模版 + /// 返回上报信息对应的反馈,如果新增子设备,则返回子设备信息 + [HttpPost(nameof(SetOnline))] + public IDeviceInfo[] SetOnline(DeviceModel[] devices) => throw new NotImplementedException(); + + /// 设备下线。驱动内子设备变化后调用 + /// + /// 根据需要,驱动内可能多次调用该方法。 + /// + /// 设备编码集合。用于子设备离线 + /// 返回上报信息对应的反馈,如果新增子设备,则返回子设备信息 + [HttpPost(nameof(SetOffline))] + public IDeviceInfo[] SetOffline(String[] devices) => throw new NotImplementedException(); + + /// 获取设备点位表 + /// 设备编码 + /// + [HttpGet(nameof(GetPoints))] + public PointModel[] GetPoints(String deviceCode) => throw new NotImplementedException(); + + /// 提交驱动信息。客户端把自己的驱动信息提交到平台 + /// + /// + [HttpPost(nameof(PostDriver))] + public Int32 PostDriver(DriverInfo[] drivers) => throw new NotImplementedException(); + #endregion + + #region 下行通知 + /// 下行通知 + /// + [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..77011a1 --- /dev/null +++ b/Samples/IoTZero/Controllers/ThingController.cs @@ -0,0 +1,171 @@ +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; +using NewLife.Remoting.Extensions; + +namespace IoTZero.Controllers; + +/// 物模型控制器 +[ApiFilter] +[ApiController] +[Route("[controller]")] +public class ThingController : BaseController +{ + /// 当前设备 + public Device Device { get; set; } + + private readonly QueueService _queue; + private readonly ThingService _thingService; + + #region 构造 + /// 实例化物模型控制器 + /// + /// + /// + public ThingController(IServiceProvider serviceProvider, QueueService queue, ThingService thingService) : base(serviceProvider) + { + _queue = queue; + _thingService = thingService; + } + + protected override Boolean OnAuthorize(String token) + { + if (!base.OnAuthorize(token) || Jwt == null) return false; + + var dv = Device.FindByCode(Jwt.Subject); + if (dv == null || !dv.Enable) throw new ApiException(ApiCode.Forbidden, "无效设备!"); + + Device = dv; + + return true; + } + #endregion + + #region 设备属性 + /// 上报设备属性 + /// 属性集合 + /// + [HttpPost(nameof(PostProperty))] + public Int32 PostProperty(PropertyModels model) => throw new NotImplementedException(); + + /// 批量上报设备属性,融合多个子设备数据批量上传 + /// 属性集合 + /// + [HttpPost(nameof(PostProperties))] + public Int32 PostProperties(PropertyModels[] models) => throw new NotImplementedException(); + + /// 获取设备属性 + /// 设备编码 + /// + [HttpGet(nameof(GetProperty))] + public PropertyModel[] GetProperty(String deviceCode) => throw new NotImplementedException(); + + /// 设备数据上报 + /// 模型 + /// + [HttpPost(nameof(PostData))] + public Int32 PostData(DataModels model) + { + var device = GetDevice(model.DeviceCode); + + return _thingService.PostData(device, model, "PostData", UserHost); + } + + /// 批量设备数据上报,融合多个子设备数据批量上传 + /// 模型 + /// + [HttpPost(nameof(PostDatas))] + public Int32 PostDatas(DataModels[] models) => throw new NotImplementedException(); + #endregion + + #region 设备事件 + /// 设备事件上报 + /// 模型 + /// + [HttpPost(nameof(PostEvent))] + public Int32 PostEvent(EventModels model) => throw new NotImplementedException(); + + /// 批量设备事件上报,融合多个子设备数据批量上传 + /// 模型 + /// + [HttpPost(nameof(PostEvents))] + public Int32 PostEvents(EventModels[] models) => throw new NotImplementedException(); + #endregion + + #region 设备服务 + /// 设备端响应服务调用 + /// 服务 + /// + [HttpPost(nameof(ServiceReply))] + public Int32 ServiceReply(ServiceReplyModel model) => throw new NotImplementedException(); + #endregion + + #region 物模型 + /// 获取设备所属产品的物模型 + /// 设备编码 + /// + [HttpGet(nameof(GetSpecification))] + public ThingSpec GetSpecification(String deviceCode) => throw new NotImplementedException(); + + /// 上报物模型 + /// + /// + [HttpPost(nameof(PostSpecification))] + public IPoint[] PostSpecification(ThingSpecModel model) => throw new NotImplementedException(); + #endregion + + #region 设备影子 + /// 上报设备影子 + /// + /// 设备影子是一个JSON文档,用于存储设备上报状态、应用程序期望状态信息。 + /// 每个设备有且只有一个设备影子,设备可以通过MQTT获取和设置设备影子来同步状态,该同步可以是影子同步给设备,也可以是设备同步给影子。 + /// 使用设备影子机制,设备状态变更,只需同步状态给设备影子一次,应用程序请求获取设备状态,不论应用程序请求数量,和设备是否联网在线,都可从设备影子中获取设备当前状态,实现应用程序与设备解耦。 + /// + /// 数据 + /// + [HttpPost(nameof(PostShadow))] + public Int32 PostShadow(ShadowModel model) => throw new NotImplementedException(); + + /// 获取设备影子 + /// 设备编码 + /// + [HttpGet(nameof(GetShadow))] + public String GetShadow(String deviceCode) => throw new NotImplementedException(); + #endregion + + #region 配置 + /// 设备端查询配置信息 + /// 设备编码 + /// + [HttpGet(nameof(GetConfig))] + public IDictionary GetConfig(String deviceCode) => throw new NotImplementedException(); + #endregion + + #region 辅助 + + /// + /// 查找子设备 + /// + /// + /// + 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 @@ + +

产品(Product)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int32AIN
Name名称String50
Code编码String50UQProductKey
Enable启用BooleanN开发中/已发布
DeviceCount设备数量Int32N
CreateUser创建人String50
CreateUserId创建者Int32N
CreateTime创建时间DateTime
CreateIP创建地址String50
UpdateUser更新人String50
UpdateUserId更新者Int32N
UpdateTime更新时间DateTime
UpdateIP更新地址String50
Remark描述String500
+

+

设备(Device)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int32AIN
Name名称String50
Code编码String50UQ设备唯一证书DeviceName,用于设备认证,在注册时由系统生成
Secret密钥String50设备密钥DeviceSecret,用于设备认证,注册时由系统生成
ProductId产品Int32N
GroupId分组Int32N
Enable启用BooleanN
Online在线BooleanN
Version版本String50
IP本地IPString200
Uuid唯一标识String200硬件标识,或其它能够唯一区分设备的标记
Location位置String50场地安装位置,或者经纬度
Period心跳周期Int32N默认60秒
PollingTime采集间隔Int32N默认1000ms
Logins登录次数Int32N
LastLogin最后登录DateTime
LastLoginIP最后IPString50最后的公网IP地址
OnlineTime在线时长Int32N总时长,每次下线后累加,单位,秒
RegisterTime激活时间DateTime
CreateUserId创建者Int32N
CreateTime创建时间DateTime
CreateIP创建地址String50
UpdateUserId更新者Int32N
UpdateTime更新时间DateTime
UpdateIP更新地址String50
Remark描述String500
+

+

设备分组(DeviceGroup)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int32AIN
Name名称String50
ParentId父级Int32N
Sort排序Int32N
Devices设备总数Int32N
Activations激活设备Int32N
Onlines当前在线Int32N
CreateUserId创建者Int32N
CreateTime创建时间DateTime
CreateIP创建地址String50
UpdateUserId更新者Int32N
UpdateTime更新时间DateTime
UpdateIP更新地址String50
Remark描述String500
+

+

设备在线(DeviceOnline)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int32AIN
SessionId会话String50UQ
ProductId产品Int32N
DeviceId设备Int32N
Name名称String50
IP本地IPString200
GroupPath分组String50
Pings心跳Int32N
Delay延迟Int32N网络延迟,单位ms
Offset偏移Int32N客户端时间减服务端时间,单位s
LocalTime本地时间DateTime
Token令牌String200
Creator创建者String50服务端设备
CreateTime创建时间DateTime
CreateIP创建地址String50
UpdateTime更新时间DateTime
Remark备注String500
+

+

设备历史(DeviceHistory)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int64PKN
DeviceId设备Int32N
Name名称String50
Action操作String50
Success成功BooleanN
TraceId追踪String50用于记录调用链追踪标识,在APM查找调用链
Creator创建者String50服务端设备
CreateTime创建时间DateTime
CreateIP创建地址String50
Remark内容String2000
+

+

设备属性(DeviceProperty)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int32AIN
DeviceId设备Int32N
Name名称String50
NickName昵称String50
Type类型String50
Value数值String设备上报数值
Unit单位String50
Enable启用BooleanN
TraceId追踪String50用于记录调用链追踪标识,在APM查找调用链
CreateTime创建时间DateTime
CreateIP创建地址String50
UpdateTime更新时间DateTime
UpdateIP更新地址String50
+

+

设备数据(DeviceData)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称显示名类型长度精度主键允许空备注
Id编号Int64PKN
DeviceId设备Int32N
Name名称String50MQTT的Topic,或者属性名
Kind类型String50数据来源,如PostProperty/PostData/MqttPostData
Value数值String2000
Timestamp时间戳Int64N设备生成数据时的UTC毫秒
TraceId追踪标识String50用于记录调用链追踪标识,在APM查找调用链
Creator创建者String50服务端设备
CreateTime创建时间DateTime
CreateIP创建地址String50
+

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 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+
\ 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + public override void Valid(Boolean isNew) + { + // 如果没有脏数据,则不需要进行任何处理 + if (!HasDirty) return; + + // 建议先调用基类方法,基类方法会做一些统一处理 + base.Valid(isNew); + + // 自动编码 + if (Code.IsNullOrEmpty()) Code = PinYin.GetFirst(Name); + CheckExist(nameof(Code)); + } + + /// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + [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 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据编码查找 + /// 编码 + /// 实体对象 + 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 高级查询 + /// 高级查询 + /// 编码。ProductKey + /// 更新时间开始 + /// 更新时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary GetCategoryList() => _CategoryCache.FindAllName(); + #endregion + + #region 业务操作 + /// 更新设备所属产品下辖设备数量 + 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; + +/// 产品。设备的集合,通常指一组具有相同功能的设备。物联网平台为每个产品颁发全局唯一的ProductKey。 +[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; + /// 编号 + [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; + /// 名称 + [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; + /// 编码。ProductKey + [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; + /// 启用。开发中/已发布 + [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; + /// 设备数量 + [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; + /// 创建人 + [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; + /// 创建者 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 更新人 + [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; + /// 更新者 + [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; + /// 更新时间 + [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; + /// 更新地址 + [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; + /// 描述 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得产品字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 编码。ProductKey + public static readonly Field Code = FindByName("Code"); + + /// 启用。开发中/已发布 + public static readonly Field Enable = FindByName("Enable"); + + /// 设备数量 + public static readonly Field DeviceCount = FindByName("DeviceCount"); + + /// 创建人 + public static readonly Field CreateUser = FindByName("CreateUser"); + + /// 创建者 + public static readonly Field CreateUserId = FindByName("CreateUserId"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 更新人 + public static readonly Field UpdateUser = FindByName("UpdateUser"); + + /// 更新者 + public static readonly Field UpdateUserId = FindByName("UpdateUserId"); + + /// 更新时间 + public static readonly Field UpdateTime = FindByName("UpdateTime"); + + /// 更新地址 + public static readonly Field UpdateIP = FindByName("UpdateIP"); + + /// 描述 + public static readonly Field Remark = FindByName("Remark"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得产品字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 名称 + public const String Name = "Name"; + + /// 编码。ProductKey + public const String Code = "Code"; + + /// 启用。开发中/已发布 + public const String Enable = "Enable"; + + /// 设备数量 + public const String DeviceCount = "DeviceCount"; + + /// 创建人 + public const String CreateUser = "CreateUser"; + + /// 创建者 + public const String CreateUserId = "CreateUserId"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 更新人 + public const String UpdateUser = "UpdateUser"; + + /// 更新者 + public const String UpdateUserId = "UpdateUserId"; + + /// 更新时间 + public const String UpdateTime = "UpdateTime"; + + /// 更新地址 + public const String UpdateIP = "UpdateIP"; + + /// 描述 + 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + + var sc = Meta.SingleCache; + sc.Expire = 20 * 60; + sc.MaxEntity = 200_000; + sc.FindSlaveKeyMethod = k => Find(_.Code == k); + sc.GetSlaveKeyMethod = e => e.Code; + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + 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; + } + + /// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + [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 扩展属性 + /// 产品 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Product Product => Extends.Get(nameof(Product), k => Product.FindById(ProductId)); + + /// 产品 + [Map(nameof(ProductId), typeof(Product), "Id")] + public String ProductName => Product?.Name; + + /// 设备属性。借助扩展属性缓存 + [XmlIgnore, IgnoreDataMember] + public IList Properties => Extends.Get(nameof(Properties), k => DeviceProperty.FindAllByDeviceId(Id)); + #endregion + + #region 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据编码查找 + /// 编码 + /// 实体对象 + 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; + } + + /// 根据产品查找 + /// 产品 + /// 实体列表 + public static IList FindAllByProductId(Int32 productId) + { + if (productId <= 0) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.ProductId == productId); + + return FindAll(_.ProductId == productId); + } + + /// 根据唯一标识查找 + /// 唯一标识 + /// 实体列表 + public static IList FindAllByUuid(String uuid) + { + if (uuid.IsNullOrEmpty()) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.Uuid.EqualIgnoreCase(uuid)); + + return FindAll(_.Uuid == uuid); + } + #endregion + + #region 高级查询 + /// 高级查询 + /// 产品 + /// + /// + /// 更新时间开始 + /// 更新时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _UuidCache = new FieldCache(nameof(Uuid)) + { + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + }; + + /// 获取唯一标识列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + /// + public static IDictionary GetUuidList() => _UuidCache.FindAllName(); + + /// + /// 根据设备分组来分组 + /// + /// + public static IList 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 业务操作 + + /// 登录并保存信息 + /// + /// + 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(); + } + + /// 设备上线 + /// + /// + 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); + } + } + + /// + /// 注销 + /// + public void Logout() + { + Online = false; + + Update(); + } + + /// 填充 + /// + 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; + +/// 设备。归属于某个产品下的具体设备。物联网平台为设备颁发产品内唯一的证书DeviceName。设备可以直接连接物联网平台,也可以作为子设备通过网关连接物联网平台。 +[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; + /// 编号 + [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; + /// 名称 + [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; + /// 编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成 + [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; + /// 密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成 + [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; + /// 产品 + [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; + /// 分组 + [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; + /// 启用 + [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; + /// 在线 + [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; + /// 版本 + [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; + /// 本地IP + [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; + /// 唯一标识。硬件标识,或其它能够唯一区分设备的标记 + [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; + /// 位置。场地安装位置,或者经纬度 + [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; + /// 心跳周期。默认60秒 + [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; + /// 采集间隔。默认1000ms + [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; + /// 登录次数 + [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; + /// 最后登录 + [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; + /// 最后IP。最后的公网IP地址 + [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; + /// 在线时长。总时长,每次下线后累加,单位,秒 + [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; + /// 激活时间 + [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; + /// 创建者 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 更新者 + [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; + /// 更新时间 + [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; + /// 更新地址 + [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; + /// 描述 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 关联映射 + /// 分组 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public DeviceGroup Group => Extends.Get(nameof(Group), k => DeviceGroup.FindById(GroupId)); + + /// 分组 + [Map(nameof(GroupId), typeof(DeviceGroup), "Id")] + public String GroupPath => Group?.Name; + + #endregion + + #region 字段名 + /// 取得设备字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成 + public static readonly Field Code = FindByName("Code"); + + /// 密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成 + public static readonly Field Secret = FindByName("Secret"); + + /// 产品 + public static readonly Field ProductId = FindByName("ProductId"); + + /// 分组 + public static readonly Field GroupId = FindByName("GroupId"); + + /// 启用 + public static readonly Field Enable = FindByName("Enable"); + + /// 在线 + public static readonly Field Online = FindByName("Online"); + + /// 版本 + public static readonly Field Version = FindByName("Version"); + + /// 本地IP + public static readonly Field IP = FindByName("IP"); + + /// 唯一标识。硬件标识,或其它能够唯一区分设备的标记 + public static readonly Field Uuid = FindByName("Uuid"); + + /// 位置。场地安装位置,或者经纬度 + public static readonly Field Location = FindByName("Location"); + + /// 心跳周期。默认60秒 + public static readonly Field Period = FindByName("Period"); + + /// 采集间隔。默认1000ms + public static readonly Field PollingTime = FindByName("PollingTime"); + + /// 登录次数 + public static readonly Field Logins = FindByName("Logins"); + + /// 最后登录 + public static readonly Field LastLogin = FindByName("LastLogin"); + + /// 最后IP。最后的公网IP地址 + public static readonly Field LastLoginIP = FindByName("LastLoginIP"); + + /// 在线时长。总时长,每次下线后累加,单位,秒 + public static readonly Field OnlineTime = FindByName("OnlineTime"); + + /// 激活时间 + public static readonly Field RegisterTime = FindByName("RegisterTime"); + + /// 创建者 + public static readonly Field CreateUserId = FindByName("CreateUserId"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 更新者 + public static readonly Field UpdateUserId = FindByName("UpdateUserId"); + + /// 更新时间 + public static readonly Field UpdateTime = FindByName("UpdateTime"); + + /// 更新地址 + public static readonly Field UpdateIP = FindByName("UpdateIP"); + + /// 描述 + public static readonly Field Remark = FindByName("Remark"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 名称 + public const String Name = "Name"; + + /// 编码。设备唯一证书DeviceName,用于设备认证,在注册时由系统生成 + public const String Code = "Code"; + + /// 密钥。设备密钥DeviceSecret,用于设备认证,注册时由系统生成 + public const String Secret = "Secret"; + + /// 产品 + public const String ProductId = "ProductId"; + + /// 分组 + public const String GroupId = "GroupId"; + + /// 启用 + public const String Enable = "Enable"; + + /// 在线 + public const String Online = "Online"; + + /// 版本 + public const String Version = "Version"; + + /// 本地IP + public const String IP = "IP"; + + /// 唯一标识。硬件标识,或其它能够唯一区分设备的标记 + public const String Uuid = "Uuid"; + + /// 位置。场地安装位置,或者经纬度 + public const String Location = "Location"; + + /// 心跳周期。默认60秒 + public const String Period = "Period"; + + /// 采集间隔。默认1000ms + public const String PollingTime = "PollingTime"; + + /// 登录次数 + public const String Logins = "Logins"; + + /// 最后登录 + public const String LastLogin = "LastLogin"; + + /// 最后IP。最后的公网IP地址 + public const String LastLoginIP = "LastLoginIP"; + + /// 在线时长。总时长,每次下线后累加,单位,秒 + public const String OnlineTime = "OnlineTime"; + + /// 激活时间 + public const String RegisterTime = "RegisterTime"; + + /// 创建者 + public const String CreateUserId = "CreateUserId"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 更新者 + public const String UpdateUserId = "UpdateUserId"; + + /// 更新时间 + public const String UpdateTime = "UpdateTime"; + + /// 更新地址 + public const String UpdateIP = "UpdateIP"; + + /// 描述 + 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + public override void Valid(Boolean isNew) + { + // 如果没有脏数据,则不需要进行任何处理 + if (!HasDirty) return; + + // 建议先调用基类方法,基类方法会做一些统一处理 + base.Valid(isNew); + + if (Name.IsNullOrEmpty()) throw new ApiException(500, "名称不能为空"); + } + + /// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + [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 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据父级、名称查找 + /// 父级 + /// 名称 + /// 实体对象 + 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); + } + + /// 根据名称查找 + /// 名称 + /// 实体列表 + public static IList FindAllByName(String name) + { + if (name.IsNullOrEmpty()) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.Name.EqualIgnoreCase(name)); + + return FindAll(_.Name == name); + } + #endregion + + #region 高级查询 + /// 高级查询 + /// 名称 + /// 父级 + /// 更新时间开始 + /// 更新时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary 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; + +/// 设备分组。物联网平台支持建立设备分组,分组中可包含不同产品下的设备。通过设备组来进行跨产品管理设备。 +[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; + /// 编号 + [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; + /// 名称 + [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; + /// 父级 + [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; + /// 排序 + [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; + /// 设备总数 + [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; + /// 激活设备 + [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; + /// 当前在线 + [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; + /// 创建者 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 更新者 + [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; + /// 更新时间 + [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; + /// 更新地址 + [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; + /// 描述 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得设备分组字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 父级 + public static readonly Field ParentId = FindByName("ParentId"); + + /// 排序 + public static readonly Field Sort = FindByName("Sort"); + + /// 设备总数 + public static readonly Field Devices = FindByName("Devices"); + + /// 激活设备 + public static readonly Field Activations = FindByName("Activations"); + + /// 当前在线 + public static readonly Field Onlines = FindByName("Onlines"); + + /// 创建者 + public static readonly Field CreateUserId = FindByName("CreateUserId"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 更新者 + public static readonly Field UpdateUserId = FindByName("UpdateUserId"); + + /// 更新时间 + public static readonly Field UpdateTime = FindByName("UpdateTime"); + + /// 更新地址 + public static readonly Field UpdateIP = FindByName("UpdateIP"); + + /// 描述 + public static readonly Field Remark = FindByName("Remark"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备分组字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 名称 + public const String Name = "Name"; + + /// 父级 + public const String ParentId = "ParentId"; + + /// 排序 + public const String Sort = "Sort"; + + /// 设备总数 + public const String Devices = "Devices"; + + /// 激活设备 + public const String Activations = "Activations"; + + /// 当前在线 + public const String Onlines = "Onlines"; + + /// 创建者 + public const String CreateUserId = "CreateUserId"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 更新者 + public const String UpdateUserId = "UpdateUserId"; + + /// 更新时间 + public const String UpdateTime = "UpdateTime"; + + /// 更新地址 + public const String UpdateIP = "UpdateIP"; + + /// 描述 + 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + 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; + } + + ///// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + //[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[设备历史]数据!"); + //} + + ///// 已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert + ///// + //public override Int32 Insert() + //{ + // return base.Insert(); + //} + + ///// 已重载。在事务保护范围内处理业务,位于Valid之后 + ///// + //protected override Int32 OnDelete() + //{ + // return base.OnDelete(); + //} + #endregion + + #region 扩展属性 + /// 设备 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId)); + + /// 设备 + [Map(nameof(DeviceId), typeof(Device), "Id")] + public String DeviceName => Device?.Name; + #endregion + + #region 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据设备、编号查找 + /// 设备 + /// 编号 + /// 实体列表 + public static IList 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); + } + + /// 根据设备查找 + /// 设备 + /// 实体列表 + public static IList FindAllByDeviceId(Int32 deviceId) + { + if (deviceId <= 0) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId); + + return FindAll(_.DeviceId == deviceId); + } + + /// 根据设备、操作查找 + /// 设备 + /// 操作 + /// 实体列表 + public static IList 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 高级查询 + /// 高级查询 + /// 设备 + /// 操作 + /// 创建时间开始 + /// 创建时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary GetCategoryList() => _CategoryCache.FindAllName(); + #endregion + + #region 业务操作 + /// 删除指定日期之前的数据 + /// + /// + public static Int32 DeleteBefore(DateTime date) => Delete(_.Id < Meta.Factory.Snow.GetId(date)); + + /// 创建日志 + /// + /// + /// + /// + /// + /// + /// + /// + 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> NameCache = new(() => new FieldCache(__.Action)); + /// 获取所有分类名称 + /// + public static IDictionary 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; + +/// 设备历史。记录设备上线下线等操作 +[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; + /// 编号 + [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; + /// 设备 + [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; + /// 名称 + [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; + /// 操作 + [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; + /// 成功 + [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; + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + [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; + /// 创建者。服务端设备 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 内容 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得设备历史字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 设备 + public static readonly Field DeviceId = FindByName("DeviceId"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 操作 + public static readonly Field Action = FindByName("Action"); + + /// 成功 + public static readonly Field Success = FindByName("Success"); + + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + public static readonly Field TraceId = FindByName("TraceId"); + + /// 创建者。服务端设备 + public static readonly Field Creator = FindByName("Creator"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 内容 + public static readonly Field Remark = FindByName("Remark"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备历史字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 设备 + public const String DeviceId = "DeviceId"; + + /// 名称 + public const String Name = "Name"; + + /// 操作 + public const String Action = "Action"; + + /// 成功 + public const String Success = "Success"; + + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + public const String TraceId = "TraceId"; + + /// 创建者。服务端设备 + public const String Creator = "Creator"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 内容 + 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 +{ + #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(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + 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)); + } + + ///// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + //[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[设备在线]数据!"); + //} + + ///// 已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert + ///// + //public override Int32 Insert() + //{ + // return base.Insert(); + //} + + ///// 已重载。在事务保护范围内处理业务,位于Valid之后 + ///// + //protected override Int32 OnDelete() + //{ + // return base.OnDelete(); + //} + #endregion + + #region 扩展属性 + /// 产品 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Product Product => Extends.Get(nameof(Product), k => Product.FindById(ProductId)); + + /// 产品 + [Map(nameof(ProductId), typeof(Product), "Id")] + public String ProductName => Product?.Name; + /// 设备 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId)); + + /// 设备 + [Map(nameof(DeviceId), typeof(Device), "Id")] + public String DeviceName => Device?.Name; + #endregion + + #region 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据会话查找 + /// 会话 + /// 实体对象 + 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); + } + + /// 根据产品查找 + /// 产品 + /// 实体列表 + public static IList FindAllByProductId(Int32 productId) + { + if (productId <= 0) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.ProductId == productId); + + return FindAll(_.ProductId == productId); + } + #endregion + + #region 高级查询 + /// 高级查询 + /// 会话 + /// 产品 + /// 更新时间开始 + /// 更新时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary GetCategoryList() => _CategoryCache.FindAllName(); + #endregion + + #region 业务操作 + /// 根据编码查询或添加 + /// + /// + public static DeviceOnline GetOrAdd(String sessionid) => GetOrAdd(sessionid, FindBySessionId, k => new DeviceOnline { SessionId = k }); + + /// 删除过期,指定过期时间 + /// 超时时间,秒 + /// + public static IList 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; + } + + /// 更新并保存在线状态 + /// + /// + /// + 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(); + } + + /// 填充节点信息 + /// + public void Fill(LoginInfo di) + { + var online = this; + + online.LocalTime = di.Time.ToDateTime().ToLocalTime(); + online.IP = di.IP; + } + + /// 填充在线节点信息 + /// + 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; + +/// 设备在线 +[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; + /// 编号 + [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; + /// 会话 + [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; + /// 产品 + [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; + /// 设备 + [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; + /// 名称 + [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; + /// 本地IP + [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; + /// 分组 + [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; + /// 心跳 + [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; + /// 延迟。网络延迟,单位ms + [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; + /// 偏移。客户端时间减服务端时间,单位s + [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; + /// 本地时间 + [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; + /// 令牌 + [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; + /// 创建者。服务端设备 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 更新时间 + [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; + /// 备注 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得设备在线字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 会话 + public static readonly Field SessionId = FindByName("SessionId"); + + /// 产品 + public static readonly Field ProductId = FindByName("ProductId"); + + /// 设备 + public static readonly Field DeviceId = FindByName("DeviceId"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 本地IP + public static readonly Field IP = FindByName("IP"); + + /// 分组 + public static readonly Field GroupPath = FindByName("GroupPath"); + + /// 心跳 + public static readonly Field Pings = FindByName("Pings"); + + /// 延迟。网络延迟,单位ms + public static readonly Field Delay = FindByName("Delay"); + + /// 偏移。客户端时间减服务端时间,单位s + public static readonly Field Offset = FindByName("Offset"); + + /// 本地时间 + public static readonly Field LocalTime = FindByName("LocalTime"); + + /// 令牌 + public static readonly Field Token = FindByName("Token"); + + /// 创建者。服务端设备 + public static readonly Field Creator = FindByName("Creator"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 更新时间 + public static readonly Field UpdateTime = FindByName("UpdateTime"); + + /// 备注 + public static readonly Field Remark = FindByName("Remark"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备在线字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 会话 + public const String SessionId = "SessionId"; + + /// 产品 + public const String ProductId = "ProductId"; + + /// 设备 + public const String DeviceId = "DeviceId"; + + /// 名称 + public const String Name = "Name"; + + /// 本地IP + public const String IP = "IP"; + + /// 分组 + public const String GroupPath = "GroupPath"; + + /// 心跳 + public const String Pings = "Pings"; + + /// 延迟。网络延迟,单位ms + public const String Delay = "Delay"; + + /// 偏移。客户端时间减服务端时间,单位s + public const String Offset = "Offset"; + + /// 本地时间 + public const String LocalTime = "LocalTime"; + + /// 令牌 + public const String Token = "Token"; + + /// 创建者。服务端设备 + public const String Creator = "Creator"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 更新时间 + public const String UpdateTime = "UpdateTime"; + + /// 备注 + 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + 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)); + } + + ///// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + //[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[设备属性]数据!"); + //} + + ///// 已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert + ///// + //public override Int32 Insert() + //{ + // return base.Insert(); + //} + + ///// 已重载。在事务保护范围内处理业务,位于Valid之后 + ///// + //protected override Int32 OnDelete() + //{ + // return base.OnDelete(); + //} + #endregion + + #region 扩展属性 + /// 设备 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId)); + + /// 设备 + [Map(nameof(DeviceId), typeof(Device), "Id")] + public String DeviceName => Device?.Name; + #endregion + + #region 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据设备、名称查找 + /// 设备 + /// 名称 + /// 实体对象 + 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); + } + + /// 根据设备查找 + /// 设备 + /// 实体对象 + public static IList FindAllByDeviceId(Int32 deviceId) + { + var list = new List(); + if (deviceId <= 0) return list; + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId); + + return FindAll(_.DeviceId == deviceId); + } + #endregion + + #region 高级查询 + /// 高级查询 + /// 设备 + /// 名称 + /// 更新时间开始 + /// 更新时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary 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; + +/// 设备属性。设备的功能模型之一,一般用于描述设备运行时的状态,如环境监测设备所读取的当前环境温度等。一个设备有多个属性,名值表 +[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; + /// 编号 + [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; + /// 设备 + [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; + /// 名称 + [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; + /// 昵称 + [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; + /// 类型 + [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; + /// 数值。设备上报数值 + [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; + /// 单位 + [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; + /// 启用 + [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; + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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; + /// 更新时间 + [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; + /// 更新地址 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得设备属性字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 设备 + public static readonly Field DeviceId = FindByName("DeviceId"); + + /// 名称 + public static readonly Field Name = FindByName("Name"); + + /// 昵称 + public static readonly Field NickName = FindByName("NickName"); + + /// 类型 + public static readonly Field Type = FindByName("Type"); + + /// 数值。设备上报数值 + public static readonly Field Value = FindByName("Value"); + + /// 单位 + public static readonly Field Unit = FindByName("Unit"); + + /// 启用 + public static readonly Field Enable = FindByName("Enable"); + + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + public static readonly Field TraceId = FindByName("TraceId"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + /// 更新时间 + public static readonly Field UpdateTime = FindByName("UpdateTime"); + + /// 更新地址 + public static readonly Field UpdateIP = FindByName("UpdateIP"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备属性字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 设备 + public const String DeviceId = "DeviceId"; + + /// 名称 + public const String Name = "Name"; + + /// 昵称 + public const String NickName = "NickName"; + + /// 类型 + public const String Type = "Type"; + + /// 数值。设备上报数值 + public const String Value = "Value"; + + /// 单位 + public const String Unit = "Unit"; + + /// 启用 + public const String Enable = "Enable"; + + /// 追踪。用于记录调用链追踪标识,在APM查找调用链 + public const String TraceId = "TraceId"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + + /// 更新时间 + public const String UpdateTime = "UpdateTime"; + + /// 更新地址 + 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 +{ + #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(); + Meta.Modules.Add(); + Meta.Modules.Add(); + } + + /// 验证并修补数据,通过抛出异常的方式提示验证失败。 + /// 是否插入 + 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; + } + + ///// 首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法 + //[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[设备数据]数据!"); + //} + + ///// 已重载。基类先调用Valid(true)验证数据,然后在事务保护内调用OnInsert + ///// + //public override Int32 Insert() + //{ + // return base.Insert(); + //} + + ///// 已重载。在事务保护范围内处理业务,位于Valid之后 + ///// + //protected override Int32 OnDelete() + //{ + // return base.OnDelete(); + //} + #endregion + + #region 扩展属性 + /// 设备 + [XmlIgnore, IgnoreDataMember, ScriptIgnore] + public Device Device => Extends.Get(nameof(Device), k => Device.FindById(DeviceId)); + + /// 设备 + [Map(nameof(DeviceId), typeof(Device), "Id")] + public String DeviceName => Device?.Name; + #endregion + + #region 扩展查询 + /// 根据编号查找 + /// 编号 + /// 实体对象 + 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); + } + + /// 根据设备、编号查找 + /// 设备 + /// 编号 + /// 实体列表 + public static IList 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); + } + + /// 根据设备查找 + /// 设备 + /// 实体列表 + public static IList FindAllByDeviceId(Int32 deviceId) + { + if (deviceId <= 0) return new List(); + + // 实体缓存 + if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.DeviceId == deviceId); + + return FindAll(_.DeviceId == deviceId); + } + + /// 根据设备、名称查找 + /// 设备 + /// 名称 + /// 实体列表 + public static IList 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); + } + + /// 根据设备、类型查找 + /// 设备 + /// 类型 + /// 实体列表 + public static IList 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 高级查询 + /// 高级查询 + /// 设备 + /// 名称。MQTT的Topic,或者属性名 + /// 创建时间开始 + /// 创建时间结束 + /// 关键字 + /// 分页参数信息。可携带统计和数据权限扩展查询等信息 + /// 实体列表 + public static IList 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 _CategoryCache = new FieldCache(nameof(Category)) + //{ + //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty + //}; + + ///// 获取类别列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择 + ///// + //public static IDictionary 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; + +/// 设备数据。设备采集原始数据,按天分表存储 +[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; + /// 编号 + [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; + /// 设备 + [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; + /// 名称。MQTT的Topic,或者属性名 + [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; + /// 类型。数据来源,如PostProperty/PostData/MqttPostData + [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; + /// 数值 + [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; + /// 时间戳。设备生成数据时的UTC毫秒 + [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; + /// 追踪标识。用于记录调用链追踪标识,在APM查找调用链 + [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; + /// 创建者。服务端设备 + [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; + /// 创建时间 + [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; + /// 创建地址 + [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 获取/设置 字段值 + /// 获取/设置 字段值 + /// 字段名 + /// + 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 字段名 + /// 取得设备数据字段信息的快捷方式 + public partial class _ + { + /// 编号 + public static readonly Field Id = FindByName("Id"); + + /// 设备 + public static readonly Field DeviceId = FindByName("DeviceId"); + + /// 名称。MQTT的Topic,或者属性名 + public static readonly Field Name = FindByName("Name"); + + /// 类型。数据来源,如PostProperty/PostData/MqttPostData + public static readonly Field Kind = FindByName("Kind"); + + /// 数值 + public static readonly Field Value = FindByName("Value"); + + /// 时间戳。设备生成数据时的UTC毫秒 + public static readonly Field Timestamp = FindByName("Timestamp"); + + /// 追踪标识。用于记录调用链追踪标识,在APM查找调用链 + public static readonly Field TraceId = FindByName("TraceId"); + + /// 创建者。服务端设备 + public static readonly Field Creator = FindByName("Creator"); + + /// 创建时间 + public static readonly Field CreateTime = FindByName("CreateTime"); + + /// 创建地址 + public static readonly Field CreateIP = FindByName("CreateIP"); + + static Field FindByName(String name) => Meta.Table.FindByName(name); + } + + /// 取得设备数据字段名称的快捷方式 + public partial class __ + { + /// 编号 + public const String Id = "Id"; + + /// 设备 + public const String DeviceId = "DeviceId"; + + /// 名称。MQTT的Topic,或者属性名 + public const String Name = "Name"; + + /// 类型。数据来源,如PostProperty/PostData/MqttPostData + public const String Kind = "Kind"; + + /// 数值 + public const String Value = "Value"; + + /// 时间戳。设备生成数据时的UTC毫秒 + public const String Timestamp = "Timestamp"; + + /// 追踪标识。用于记录调用链追踪标识,在APM查找调用链 + public const String TraceId = "TraceId"; + + /// 创建者。服务端设备 + public const String Creator = "Creator"; + + /// 创建时间 + public const String CreateTime = "CreateTime"; + + /// 创建地址 + public const String CreateIP = "CreateIP"; + } + #endregion +} diff --git a/Samples/IoTZero/IoTSetting.cs b/Samples/IoTZero/IoTSetting.cs new file mode 100644 index 0000000..4d34d69 --- /dev/null +++ b/Samples/IoTZero/IoTSetting.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using NewLife; +using NewLife.Configuration; +using NewLife.Remoting.Extensions.Models; +using NewLife.Security; +using XCode.Configuration; + +namespace IoTZero; + +/// 配置 +[Config("IoTZero")] +public class IoTSetting : Config, ITokenSetting +{ + #region 静态 + static IoTSetting() => Provider = new DbConfigProvider { UserId = 0, Category = "IoTServer" }; + #endregion + + #region 属性 + ///// MQTT服务端口。默认1883 + //[Description("MQTT服务端口。默认1883")] + //public Int32 MqttPort { get; set; } = 1883; + + ///// MQTT证书地址。设置了才启用安全连接,默认为空 + //[Description("MQTT证书地址。设置了才启用安全连接,默认为空")] + //public String MqttCertPath { get; set; } + + ///// MMQTT证书密码 + //[Description("MQTT证书密码")] + //public String MqttCertPassword { get; set; } + #endregion + + #region 设备管理 + /// 令牌密钥。用于生成JWT令牌的算法和密钥,如HS256:ABCD1234 + [Description("令牌密钥。用于生成JWT令牌的算法和密钥,如HS256:ABCD1234")] + [Category("设备管理")] + public String TokenSecret { get; set; } + + /// 令牌有效期。默认2*3600秒 + [Description("令牌有效期。默认2*3600秒")] + [Category("设备管理")] + public Int32 TokenExpire { get; set; } = 2 * 3600; + + /// 会话超时。默认600秒 + [Description("会话超时。默认600秒")] + [Category("设备管理")] + public Int32 SessionTimeout { get; set; } = 600; + + /// 自动注册。允许客户端自动注册,默认true + [Description("自动注册。允许客户端自动注册,默认true")] + [Category("设备管理")] + public Boolean AutoRegister { get; set; } = true; + #endregion + + #region 数据存储 + /// 历史数据保留时间。默认30天 + [Description("历史数据保留时间。默认30天")] + [Category("数据存储")] + public Int32 DataRetention { get; set; } = 30; + #endregion + + #region 方法 + /// 加载时触发 + 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 1b845ca..8de791f 100644 --- a/Samples/IoTZero/IoTZero.csproj +++ b/Samples/IoTZero/IoTZero.csproj @@ -18,59 +18,6 @@ latest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - @@ -83,27 +30,4 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/IoTZero/Models/LoginInfo.cs b/Samples/IoTZero/Models/LoginInfo.cs new file mode 100644 index 0000000..41f2cf4 --- /dev/null +++ b/Samples/IoTZero/Models/LoginInfo.cs @@ -0,0 +1,36 @@ +using NewLife.Remoting.Models; + +namespace NewLife.IoT.Models; + +/// 节点登录信息 +public class LoginInfo : LoginRequest +{ + #region 属性 + ///// 设备编码 + //public String Code { get; set; } + + ///// 设备密钥 + //public String Secret { get; set; } + + /// 产品证书 + public String ProductKey { get; set; } + + /// 产品密钥 + public String ProductSecret { get; set; } + + /// 名称。可用于标识设备的名称 + public String Name { get; set; } + + ///// 版本 + //public String Version { get; set; } + + /// 本地IP地址 + public String IP { get; set; } + + /// 唯一标识 + public String UUID { get; set; } + + /// 本地UTC时间 + 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 +{ + /// 设备登录响应 + public class LoginResponse + { + #region 属性 + /// 产品 + public String ProductKey { get; set; } + + /// 节点编码 + public String Code { get; set; } + + /// 节点密钥 + public String Secret { get; set; } + + /// 名称 + public String Name { get; set; } + + /// 令牌 + public String Token { get; set; } + + /// 服务器时间 + public Int64 Time { get; set; } + + ///// 设备通道 + //public String Channels { get; set; } + + /// 客户端唯一标识 + 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 +{ + /// 设备注销响应 + public class LogoutResponse + { + #region 属性 + /// 节点编码 + public String Code { get; set; } + + /// 节点密钥 + public String Secret { get; set; } + + /// 名称 + public String Name { get; set; } + + /// 令牌 + 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..0b72a3d --- /dev/null +++ b/Samples/IoTZero/Models/PingInfo.cs @@ -0,0 +1,42 @@ +using NewLife.Remoting.Models; + +namespace NewLife.IoT.Models; + +/// 心跳信息 +public class PingInfo : PingRequest +{ + #region 属性 + /// 内存大小 + public UInt64 Memory { get; set; } + + /// 可用内存大小 + public UInt64 AvailableMemory { get; set; } + + /// 磁盘大小。应用所在盘 + public UInt64 TotalSize { get; set; } + + /// 磁盘可用空间。应用所在盘 + public UInt64 AvailableFreeSpace { get; set; } + + /// CPU使用率 + public Single CpuRate { get; set; } + + /// 温度 + public Double Temperature { get; set; } + + /// 电量 + public Double Battery { get; set; } + + /// 本地IP + public String IP { get; set; } + + /// 开机时间,单位s + public Int32 Uptime { get; set; } + + /// 本地UTC时间。ms毫秒 + public Int64 Time { get; set; } + + /// 延迟。ms毫秒 + 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 +{ + /// 心跳响应 + public class PingResponse + { + /// 本地时间。ms毫秒 + public Int64 Time { get; set; } + + /// 服务器时间 + public Int64 ServerTime { get; set; } + + /// 心跳周期。单位秒 + public Int32 Period { get; set; } + + /// 令牌。现有令牌即将过期时,颁发新的令牌 + 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; + +/// 物模型上报 +public class ThingSpecModel +{ + /// 设备编码 + public String DeviceCode { get; set; } + + /// 物模型 + 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; + +/// 更新响应 +public class UpgradeInfo +{ + /// 版本号 + public String Version { get; set; } + + /// 更新源,Url地址 + public String Source { get; set; } + + /// 文件哈希 + public String FileHash { get; set; } + + /// 文件大小 + public Int64 FileSize { get; set; } + + /// 更新后要执行的命令 + public String Executor { get; set; } + + /// 是否强制更新,不需要用户同意 + public Boolean Force { get; set; } + + /// 描述 + 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..3050729 --- /dev/null +++ b/Samples/IoTZero/Program.cs @@ -0,0 +1,116 @@ +using IoTZero; +using IoTZero.Services; +using NewLife.Caching; +using NewLife.Cube; +using NewLife.Log; +using NewLife.Reflection; +using NewLife.Remoting.Extensions.Models; +using NewLife.Remoting.Extensions.Services; +using NewLife.Security; +using XCode; + +// 日志输出到控制台,并拦截全局异常 +XTrace.UseConsole(); + +var builder = WebApplication.CreateBuilder(args); +var services = builder.Services; + +InitConfig(); + +// 配置星尘。借助StarAgent,或者读取配置文件 config/star.config 中的服务器地址、应用标识、密钥 +var star = services.AddStardust(null); + +// 系统设置 +var set = IoTSetting.Current; +services.AddSingleton(set); + +// 逐个注册每一个用到的服务,必须做到清晰明了 +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); + +// 注册Remoting所必须的服务 +services.AddSingleton(); +services.AddSingleton(set); + +// 注册密码提供者,用于通信过程中保护密钥,避免明文传输 +services.AddSingleton(new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 }); + +services.AddHttpClient("hc", e => e.Timeout = TimeSpan.FromSeconds(5)); + +services.AddSingleton(); + +// 后台服务 +services.AddHostedService(); +services.AddHostedService(); + +// 启用接口响应压缩 +services.AddResponseCompression(); + +services.AddControllersWithViews(); + +// 引入魔方 +services.AddCube(); + +var app = builder.Build(); + +// 使用Cube前添加自己的管道 +if (app.Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); +else + app.UseExceptionHandler("/CubeHome/Error"); + +if (Environment.GetEnvironmentVariable("__ASPNETCORE_BROWSER_TOOLS") is null) + 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>(); +if (test != null) _ = Task.Run(() => test(app.Services)); + +app.Run(); + +void InitConfig() +{ + // 把数据目录指向上层,例如部署到 /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(); + } +} \ No newline at end of file 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; + +/// 数据服务 +public class DataService +{ + private readonly ITracer _tracer; + + /// 实例化数据服务 + /// + public DataService(ITracer tracer) => _tracer = tracer; + + #region 方法 + /// + /// 插入设备原始数据,异步批量操作 + /// + /// 设备 + /// 传感器 + /// + /// + /// + /// + /// + /// + 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; + } + + /// 添加事件 + /// + /// + /// + /// + 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; + +/// 节点在线服务 +public class DeviceOnlineService : IHostedService +{ + #region 属性 + private TimerX _timer; + private readonly MyDeviceService _deviceService; + private readonly IoTSetting _setting; + private readonly ITracer _tracer; + #endregion + + #region 构造 + /// + /// 实例化节点在线服务 + /// + /// + /// + /// + public DeviceOnlineService(MyDeviceService deviceService, IoTSetting setting, ITracer tracer) + { + _deviceService = deviceService; + _setting = setting; + _tracer = tracer; + } + #endregion + + #region 方法 + /// + /// 开始服务 + /// + /// + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _timer = new TimerX(CheckOnline, null, 5_000, 30_000) { Async = true }; + + return Task.CompletedTask; + } + + /// + /// 停止服务 + /// + /// + /// + 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, "超时下线"); + } + } + } + } + } + + /// + /// 检查离线 + /// + /// + /// + 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..ddeca6b --- /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; + +/// 设备服务 +public class MyDeviceService +{ + /// 节点引用,令牌无效时使用 + 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; + + /// + /// 实例化设备服务 + /// + /// + /// + /// + /// + /// + public MyDeviceService(IPasswordProvider passwordProvider, DataService dataService, ICacheProvider cacheProvider, IoTSetting setting, ITracer tracer) + { + _passwordProvider = passwordProvider; + _dataService = dataService; + _cache = cacheProvider.InnerCache; + _setting = setting; + _tracer = tracer; + } + + #region 登录 + /// + /// 设备登录验证,内部支持动态注册 + /// + /// 登录信息 + /// 登录来源 + /// 远程IP + /// + /// + 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(ApiCode.NotFound, "找不到设备,且产品证书为空,无法登录"); + + dv = AutoRegister(null, inf, ip); + autoReg = true; + } + else + { + if (!dv.Enable) throw new ApiException(ApiCode.Forbidden, "禁止登录"); + + // 校验唯一编码,防止客户端拷贝配置 + var uuid = inf.UUID; + if (!uuid.IsNullOrEmpty() && !dv.Uuid.IsNullOrEmpty() && uuid != dv.Uuid) + WriteHistory(dv, source + "登录校验", false, $"新旧唯一标识不一致!(新){uuid}!={dv.Uuid}(旧)", ip); + + // 登录密码未设置或者未提交,则执行动态注册 + if (dv == null || !dv.Secret.IsNullOrEmpty() + && (secret.IsNullOrEmpty() || !_passwordProvider.Verify(dv.Secret, secret))) + { + if (inf.ProductKey.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "设备验证失败,且产品证书为空,无法登录"); + + 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; + } + + /// 设置设备在线,同时检查在线表 + /// + /// + /// + 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); + } + + /// 自动注册 + /// + /// + /// + /// + /// + 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; + } + + /// 注销 + /// 设备 + /// 注销原因 + /// 登录来源 + /// 远程IP + /// + 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 心跳 + /// + /// 心跳 + /// + /// + /// + /// + /// + /// + 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; + } + + /// + /// + /// + /// + protected virtual DeviceOnline GetOnline(Device device, String ip) + { + var sid = $"{device.Id}@{ip}"; + var olt = _cache.Get($"DeviceOnline:{sid}"); + if (olt != null) + { + _cache.SetExpire($"DeviceOnline:{sid}", TimeSpan.FromSeconds(600)); + return olt; + } + + return DeviceOnline.FindBySessionId(sid); + } + + /// 检查在线 + /// + /// + /// + 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; + } + + /// 删除在线 + /// + /// + /// + public Int32 RemoveOnline(Int32 deviceId, String ip) + { + var sid = $"{deviceId}@{ip}"; + + return _cache.Remove($"DeviceOnline:{sid}"); + } + #endregion + + #region 辅助 + /// + /// 颁发令牌 + /// + /// + /// + /// + 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), + }; + } + + /// + /// 解码令牌,并验证有效性 + /// + /// + /// + /// + /// + public Device DecodeToken(String token, String tokenSecret) + { + //if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token)); + if (token.IsNullOrEmpty()) throw new ApiException(ApiCode.Unauthorized, "节点未登录"); + + // 解码令牌 + var ss = tokenSecret.Split(':'); + var jwt = new JwtBuilder + { + Algorithm = ss[0], + Secret = ss[1], + }; + + var rs = jwt.TryDecode(token, out var message); + var node = Device.FindByCode(jwt.Subject); + Current = node; + if (!rs) throw new ApiException(ApiCode.Forbidden, $"非法访问 {message}"); + + return node; + } + + /// + /// 验证并颁发令牌 + /// + /// + /// + /// + 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; + } + + /// + /// 写设备历史 + /// + /// + /// + /// + /// + /// + 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; + +/// 队列服务 +public class QueueService +{ + #region 属性 + private readonly ICacheProvider _cacheProvider; + private readonly ITracer _tracer; + #endregion + + #region 构造 + /// + /// 实例化队列服务 + /// + public QueueService(ICacheProvider cacheProvider, ITracer tracer) + { + _cacheProvider = cacheProvider; + _tracer = tracer; + } + #endregion + + #region 命令队列 + /// + /// 获取指定设备的命令队列 + /// + /// + /// + public IProducerConsumer GetQueue(String deviceCode) + { + var q = _cacheProvider.GetQueue($"cmd:{deviceCode}"); + if (q is QueueBase qb) qb.TraceName = "ServiceQueue"; + + return q; + } + + /// + /// 向指定设备发送命令 + /// + /// + /// + /// + 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()); + } + + /// + /// 获取指定设备的服务响应队列 + /// + /// + /// + public IProducerConsumer GetReplyQueue(Int64 serviceLogId) => throw new NotImplementedException(); + + /// + /// 发送消息到服务响应队列 + /// + /// + 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; + +/// 分表管理 +public class ShardTableService : IHostedService +{ + private readonly IoTSetting _setting; + private readonly ITracer _tracer; + private TimerX _timer; + + /// + /// 实例化分表管理服务 + /// + /// + /// + public ShardTableService(IoTSetting setting, ITracer tracer) + { + _setting = setting; + _tracer = tracer; + } + + /// + /// 开始服务 + /// + /// + /// + public Task StartAsync(CancellationToken cancellationToken) + { + // 每小时执行 + _timer = new TimerX(DoShardTable, null, 5_000, 3600 * 1000) { Async = true }; + + return Task.CompletedTask; + } + + /// + /// 停止服务 + /// + /// + /// + 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(); + { + 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; + +/// 物模型服务 +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(); + + /// + /// 实例化物模型服务 + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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 数据存储 + /// 上报数据 + /// + /// + /// + /// + /// + 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; + } + + /// 设备属性上报 + /// 设备 + /// 属性名 + /// 数值 + /// 时间戳 + /// IP地址 + /// + 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; + } + + /// 更新属性 + /// + /// + 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; + } + + /// 保存历史数据,写入属性表、数据表、分段数据表 + /// + /// + /// + /// + /// + 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; + } + } + + /// 获取设备属性对象,长时间缓存,便于加速属性保存 + /// + /// + /// + private DeviceProperty GetProperty(Device device, String name) + { + var key = $"DeviceProperty:{device.Id}:{name}"; + if (_cacheProvider.InnerCache.TryGetValue(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 属性功能 + /// 获取设备属性 + /// 设备 + /// 属性名集合 + /// + public PropertyModel[] GetProperty(Device device, String[] names) + { + var list = new List(); + 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(); + } + + /// 查询设备属性。应用端调用 + /// 设备编码 + /// 属性名集合 + /// + public PropertyModel[] QueryProperty(Device device, String[] names) + { + var list = new List(); + 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 事件 + /// 设备事件上报 + /// + /// + /// + /// + public Int32 PostEvent(Device device, EventModel[] events, String ip) => throw new NotImplementedException(); + + /// 设备事件上报 + /// + /// + /// + public void PostEvent(Device device, EventModel @event, String ip) => throw new NotImplementedException(); + #endregion + + #region 服务调用 + /// 调用服务 + /// + /// + /// + /// + /// + /// + 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; + } + + ///// 服务响应 + ///// + ///// + ///// + ///// + //public DeviceServiceLog ServiceReply(Device device, ServiceReplyModel model) => throw new NotImplementedException(); + + /// 异步调用服务,并等待响应 + /// + /// + /// + /// + /// + /// + public async Task 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 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/Zero.RpcServer/Program.cs b/Samples/Zero.RpcServer/Program.cs index 36b2472..a6b5fe0 100644 --- a/Samples/Zero.RpcServer/Program.cs +++ b/Samples/Zero.RpcServer/Program.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; -using NewLife; -using NewLife.Caching; +using NewLife.Caching; using NewLife.Caching.Services; -using NewLife.Configuration; using NewLife.Log; using NewLife.Model; using NewLife.Remoting; @@ -23,7 +20,7 @@ // 引入Redis,用于消息队列和缓存,单例,带性能跟踪。一般使用上面的ICacheProvider替代 //services.AddRedis("127.0.0.1:6379", "123456", 3, 5000); -var port = 12345; +var port = 8080; // 实例化RPC服务端,指定端口,同时在Tcp/Udp/IPv4/IPv6上监听 var server = new ApiServer(port) diff --git a/Samples/ZeroIoT b/Samples/ZeroIoT deleted file mode 160000 index a00ae3a..0000000 --- a/Samples/ZeroIoT +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a00ae3aa4d90c20a4cea76e216aa1e91cb5aa46c