using Dlhis.Basic.Device.Request; using Dlhis.Basic.Device.Response; using Dlhis.Entity.Basic.Entity; using Dlhis.Entity.Device.Entities; using Dlhis.Entity.Device.Enums; using Dlhis.Sys.Api; using Kurisu.AspNetCore.Authentication.Abstractions; using Kurisu.AspNetCore.CustomClass; using Kurisu.AspNetCore.DataAccess.Dto; using Kurisu.AspNetCore.DataAccess.Entity; using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; using Kurisu.AspNetCore.DataAccess.SqlSugar.Services; using Kurisu.AspNetCore.Utils.Extensions; using Mapster; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Dlhis.Basic.Device.Services.Implements; public class DeviceService : IDeviceService { private const string DefaultPassword = "********"; private const string DefaultPasswordKey = "password"; private readonly IDbContext _dbContext; private readonly ICurrentUser _currentUser; private readonly ITenantApi _tenantApi; private readonly IDeviceTimePeriodService _deviceTimePeriodService; public DeviceService(IDbContext dbContext, ICurrentUser currentUser, ITenantApi tenantApi, IDeviceTimePeriodService deviceTimePeriodService) { _dbContext = dbContext; _currentUser = currentUser; _tenantApi = tenantApi; _deviceTimePeriodService = deviceTimePeriodService; } public async Task> GetPageAsync(DevicePageRequest request) { var page = await _dbContext.Queryable() .LeftJoin((d, dt) => d.TemplateCode == dt.Code) .LeftJoin((d, dt, dr) => d.UniqueNo == dr.DeviceUniqueNo) .LeftJoin((d, dt, dr, bp) => dr.BusinessPlaceCode == bp.Code) .Where((d, dt) => d.Type == request.Type) .WhereIF(request.Enable.HasValue, (d, dt) => d.Enable == request.Enable) .WhereIF(request.Search.IsPresent(), d => d.UniqueNo == request.Search || d.DeployPlace.Contains(request.Search)) .WhereIF(request.Supplier.IsPresent(), d => d.Supplier.Contains(request.Supplier)) .Select((d, dt, dr, bp) => new DevicePageResponse { FuncsStr = d.Funcs.ToString(), Brand = dt.Brand, Type = d.Type, BusinessPlaceCode = bp.Code, BusinessPlaceName = bp.Name, LastHeartbeatTime = dr.LastHeartbeatTime, Factory = d.TenantId }, true) .ToPageListAsync(request); //获取当前用户角色 var role = _currentUser.GetRole(); //如果当前用户不是管理员或租户管理员,则对设备列表中的连接信息进行脱敏处理,将密码字段替换为默认密码,防止敏感信息泄露。 if (role is not ("admin" or "tenant-admin")) { foreach (var item in page.Data) { var jObj = JObject.Parse(item.ConnectionInfo); if (jObj.TryGetValue(DefaultPasswordKey, out var password)) { if (password.Value().IsPresent()) { jObj[DefaultPasswordKey] = DefaultPassword; item.ConnectionInfo = jObj.ToString(); } } } } var deviceUniqueNos = page.Data.Select(x => x.UniqueNo).ToList(); var deviceUsers = await _dbContext.Queryable() .Where(x => deviceUniqueNos.Contains(x.DeviceUniqueNo)) .Select(x => x.DeviceUniqueNo) .Distinct() .ToListAsync(); foreach (var devicePageResponse in page.Data) { devicePageResponse.Sync = deviceUsers.Contains(devicePageResponse.UniqueNo); } return page; } public async Task SaveAsync(DeviceSaveRequest request) { await CheckUniqueNoExistsAsync(request); await CheckTemplateExistsAsync(request.TemplateCode); await CheckTemplateFieldAsync(request.TemplateCode, request.ConnectionInfo); await _deviceTimePeriodService.GenerateDeviceTimePeriodTasksAsync(async () => { DeviceEntity entity; if (!request.Id.HasValue) { var password = GetJsonValue(request.ConnectionInfo, DefaultPasswordKey); if (password.IsPresent()) { (password == DefaultPassword).ThrowIfTrue("不支持错误的密码类型"); } entity = request.Adapt(); entity.UniqueNo = request.UniqueNo; entity.Code = Guid.NewGuid(); await _dbContext.InsertAsync(entity); //同时保存注册信息 var registry = await _dbContext.Queryable().Where(x => x.DeviceUniqueNo == request.UniqueNo).SingleAsync(); if (registry == null) { registry = new DeviceRegistryEntity { DeviceUniqueNo = request.UniqueNo, LastHeartbeatTime = DateTime.Now, RegisterTime = DateTime.Now, BusinessPlaceCode = null, }; await _dbContext.InsertAsync(registry); } } else { entity = await _dbContext.Queryable().Where(x => x.Id == request.Id).FirstAsync(); entity.ThrowIfNull("设备不存在,无法更新"); //分类发生变化,检查是否存在分组 if (request.Type != entity.Type) { var existsG = await _dbContext.Queryable() .LeftJoin((dgr, dg) => dgr.GroupCode == dg.Code) .Where((dgr, dg) => dgr.DeviceCode == entity.Code) .AnyAsync(); existsG.ThrowIfTrue("存在设备分组,不可修改设备类型"); } //检查更新的密码字段是否为默认密码,如果是,则将原有密码从数据库实体中取出并替换到请求的连接信息中,防止默认密码被保存。 // 这样可以避免用户在未修改密码时,前端传递默认密码导致密码被覆盖为不可用的默认值。 var updatePassword = GetJsonValue(request.ConnectionInfo, DefaultPasswordKey); if (updatePassword.IsPresent()) { if (updatePassword == DefaultPassword) { var jObj = JObject.Parse(request.ConnectionInfo); var password = GetJsonValue(entity.ConnectionInfo, DefaultPasswordKey); jObj[DefaultPasswordKey] = password; request.ConnectionInfo = jObj.ToString(); } } request.Adapt(entity); await _dbContext.UpdateAsync(entity); var registry = await _dbContext.Queryable().Where(x => x.DeviceUniqueNo == request.UniqueNo).SingleAsync(); if (registry == null) { registry = new DeviceRegistryEntity { DeviceUniqueNo = request.UniqueNo, LastHeartbeatTime = DateTime.Now, RegisterTime = DateTime.Now, BusinessPlaceCode = null, }; await _dbContext.InsertAsync(registry); } } await GenerateUploadRecordLogForBeginTimeAsync(entity); }, "设备启用/关闭时段码功能"); } /// /// 获取json的字段值 /// /// /// /// /// private static string GetJsonValue(string json, string key) { try { var jObj = JObject.Parse(json); return jObj.TryGetValue(key, out var value) ? value.Value() : string.Empty; } catch (JsonReaderException) { throw new UserFriendlyException("连接信息参数格式不正确"); } } /// /// 批量导入 /// /// /// public async Task ImportAsync(List request) { var businessPlaces = await _dbContext.Queryable().ToListAsync(); var message = new List(); await _dbContext.Client.Ado.BeginTranAsync(); try { for (var i = 0; i < request.Count; i++) { var row = i + 1; try { var item = request[i]; await SaveAsync(item); if (item.Type == DeviceTypeEnumType.Meal) { var registry = await _dbContext.Queryable().Where(x => x.DeviceUniqueNo == item.UniqueNo).FirstAsync(); businessPlaces.Any(x => x.Code == item.BusinessPlaceCode).ThrowIfFalse("营业点不存在"); registry.BusinessPlaceCode = item.BusinessPlaceCode; await _dbContext.UpdateAsync(registry); } } catch (Exception ex) { message.Add($"第{row}列:{ex.Message}"); } } message.ThrowIfExists(message.Join("\r\n")); await _dbContext.Client.Ado.CommitTranAsync(); } catch { await _dbContext.Client.Ado.RollbackTranAsync(); throw; } } //******************************************************** help methods ********************************************************** private async Task GenerateUploadRecordLogForBeginTimeAsync(DeviceEntity entity) { if (entity.Type is DeviceTypeEnumType.Door or DeviceTypeEnumType.Attendance) { var exists = await _dbContext.Queryable() .Where(x => x.DeviceUniqueNo == entity.UniqueNo) .Where(x => x.Type == DeviceUploadLogTypeEnumType.Raise) .AnyAsync(); if (!exists) { await _dbContext.InsertAsync(new DeviceUploadLogEntity { DeviceUniqueNo = entity.UniqueNo, Type = DeviceUploadLogTypeEnumType.Raise, UserUniqueNo = string.Empty, SerialNo = string.Empty, RaiseDateTime = entity.CreateTime, }); } } } /// /// 检查设备模板是否存在 /// /// /// private async Task CheckTemplateExistsAsync(Guid templateCode) { var entity = await _dbContext.Queryable().Where(x => x.Code == templateCode).FirstAsync(); entity.ThrowIfNull("设备模板不存在"); } /// /// 检查设备连接参数是否与模板设定一致 /// /// /// /// private async Task CheckTemplateFieldAsync(Guid templateCode, string jsonContent) { var template = await _dbContext.Queryable().Where(x => x.Code == templateCode).SingleAsync(); template.ThrowIfNull("设备模板不存在"); try { var templateJObj = JObject.Parse(template.Template); var jObj = JObject.Parse(jsonContent); (templateJObj.Count == jObj.Count).ThrowIfFalse("连接参数字段数不一致"); foreach (var item in templateJObj) { jObj.ContainsKey(item.Key).ThrowIfFalse($"设备连接参数需要和模板一致.缺失{item.Key}"); } } catch (Exception ex) when (ex is JsonReaderException) { throw new UserFriendlyException("连接信息参数格式不正确"); } } /// /// 检查唯一号是否存在 /// /// /// private async Task CheckUniqueNoExistsAsync(DeviceSaveRequest request) { await _dbContext.IgnoreAsync(async () => { var currentTenantId = _currentUser.GetStringTenantId(); //查询设备是否存在 var device = await _dbContext.Queryable() .Where(x => x.UniqueNo == request.UniqueNo) .WhereIF(request.Id.HasValue, x => x.Id != request.Id) .SingleAsync(); if (device != null) { (device.TenantId == currentTenantId).ThrowIfTrue("设备已经存在,请检查"); var factory = device.TenantId; try { var tenant = await _tenantApi.GetTenantByCodeAsync(device.TenantId); factory = tenant?.Name ?? string.Empty; } catch (Exception) { //ignore } true.ThrowIfTrue($"设备已添加{factory}厂区"); } }); } /// /// 获取离线设备 /// /// private async Task> GetOfflineDevices(List devices) { var now = DateTime.Now; var deviceUniqueNos = devices.Select(x => x.UniqueNo).ToList(); var deviceRegistries = await _dbContext.Queryable() .Where(x => deviceUniqueNos.Contains(x.DeviceUniqueNo)) .Select(x => new { x.DeviceUniqueNo, x.LastHeartbeatTime }) .ToListAsync(); var offlineUniqueNos = devices.Where(x => { var item = deviceRegistries.FirstOrDefault(p => p.DeviceUniqueNo == x.UniqueNo); if (item == null) { return true; } return x.HeartbeatInterval > 0 && item.LastHeartbeatTime.AddSeconds(x.HeartbeatInterval) < now; }).Select(x => x.UniqueNo); return devices.Where(x => offlineUniqueNos.Contains(x.UniqueNo)).Select(x => x.Id).ToList(); } private async Task> GetOnlineDevices(List devices) { var now = DateTime.Now; var deviceUniqueNos = devices.Select(x => x.UniqueNo).ToList(); var deviceRegistries = await _dbContext.Queryable() .LeftJoin((x, d) => x.DeviceUniqueNo == d.UniqueNo) .Where(x => deviceUniqueNos.Contains(x.DeviceUniqueNo)) .Select((x, d) => new { d.UniqueNo, d.HeartbeatInterval, x.LastHeartbeatTime }) .ToListAsync(); var offlineUniqueNos = deviceRegistries .Where(x => x.HeartbeatInterval <= 0 || x.LastHeartbeatTime.AddSeconds(x.HeartbeatInterval) >= now) .Select(x => x.UniqueNo); return devices.Where(x => offlineUniqueNos.Contains(x.UniqueNo)).Select(x => x.Id).ToList(); } public async Task> GetOnlineStatusPageAsync(DeviceOnlineAndOfflineRequest request) { return await GetDeviceStatusPageAsync(request.IsOnline, request, request.Type); } //************************************************************* help methods ************************************************************************ private async Task> GetDeviceStatusPageAsync(bool isOnline, PageDto pageDto, DeviceTypeEnumType type) { var devices = type == DeviceTypeEnumType.Attendance ? await _dbContext.Queryable().Where(x => x.Type == DeviceTypeEnumType.Attendance).ToListAsync() : await _dbContext.Queryable().Where(x => x.Type == DeviceTypeEnumType.Door).ToListAsync(); var ids = isOnline ? await GetOnlineDevices(devices) : await GetOfflineDevices(devices); return await _dbContext.Queryable() .LeftJoin((d, dt) => d.TemplateCode == dt.Code) .Where(d => ids.Contains(d.Id)) .WhereIF(pageDto.Search.IsPresent(), d => d.UniqueNo == pageDto.Search || d.DeployPlace.Contains(pageDto.Search)) .Select((d, dt) => new DevicePageResponse { FuncsStr = d.Funcs.ToString(), Brand = dt.Brand, Type = d.Type }, true) .ToPageListAsync(pageDto); } }