using Dlhis.Basic.Appointment.Request; using Dlhis.Basic.Appointment.Response; using Dlhis.Basic.Organization.Request; using Dlhis.Basic.Organization.Services; using Dlhis.Dh.Api; using Dlhis.Dh.Api.Requests; using Dlhis.Entity.Basic.Entity; using Dlhis.Entity.Basic.Enums; using Dlhis.Entity.Device.Entities; using Dlhis.Entity.Device.Enums; using Dlhis.Entity.Visitor.Entity; using Dlhis.Entity.Visitor.Enums; using Kurisu.AspNetCore.Authentication.Abstractions; using Kurisu.AspNetCore.CustomClass; using Kurisu.AspNetCore.DataAccess.Dto; using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; using Kurisu.AspNetCore.DataAccess.SqlSugar.Services; using Kurisu.AspNetCore.Utils.Extensions; using Microsoft.AspNetCore.DataProtection; using SqlSugar; namespace Dlhis.Basic.Appointment.Services.Implements; public class AppointmentService : IAppointmentService { private readonly IDbContext _dbContext; private readonly IDataProtectionProvider _dataProtectionProvider; private readonly ICurrentUser _currentUser; private readonly IDhApi _dhApi; private readonly IFactorySettingService _factorySettingService; private readonly ILogger _logger; private readonly IGainfoFaceService _gainfoFaceService; private readonly IServiceProvider _serviceProvider; public AppointmentService(IDbContext dbContext, IDataProtectionProvider dataProtectionProvider, ICurrentUser currentUser, IDhApi dhApi, IFactorySettingService factorySettingService, IGainfoFaceService gainfoFaceService, IServiceProvider serviceProvider, ILogger logger) { _dbContext = dbContext; _dataProtectionProvider = dataProtectionProvider; _currentUser = currentUser; _dhApi = dhApi; _factorySettingService = factorySettingService; _logger = logger; _gainfoFaceService = gainfoFaceService; _serviceProvider = serviceProvider; } public async Task> GetPageAsync(AppointmentPageRequest request) { var factory = _currentUser.GetStringTenantId(); return await _dbContext.Queryable() .WhereIF(request.SearchDateTimePeriod.HasValue(), x => x.Date >= request.SearchDateTimePeriod.Start && x.Date <= request.SearchDateTimePeriod.End) .WhereIF(request.Search.IsPresent(), x => x.Code == request.Search || SqlFunc.Subqueryable() .Where(p => p.No == x.No) .Where(p => p.IdNo == request.Search || p.Name == request.Search).Any() ) .Where(x => x.Factory == factory) .Select() .ToPageListAsync(request); } public async Task> GetAppointmentDetailsByIdAsync(string no) { return await _dbContext.Queryable() .Where(x => x.No == no) .Select(x => new AppointmentDetailResponse { Id = x.Id, No = x.No, UserNo = x.UserNo, IdType = x.IdType, IdNo = x.IdNo, Name = x.Name, Phone = x.Phone, LicensePlate = x.LicensePlate, IsEnterDoorArea = x.IsEnterDoorArea, ApplyDoorArea = SqlFunc.Subqueryable().Where(p => p.CustomCode == x.ApplyDoorArea).Select(p => p.Name), DoorStart = x.DoorStart, DoorEnd = x.DoorEnd, IsEnterWorkplace = x.IsEnterWorkplace, ApplyWorkplaceAreas = SqlFunc.Subqueryable().Where(p => x.ApplyWorkplaceAreas.ToString().Contains(p.CustomCode)).ToList(p => p.Name), WorkplaceStart = x.WorkplaceStart, WorkplaceEnd = x.WorkplaceEnd, ImageUrl = x.ImageUrl, Remark = x.Remark, DoorStatus = x.DoorStatus, WorkplaceStatus = x.WorkplaceStatus, }) .ToListAsync(); } public async Task ConfirmAsync(AppointmentConfirmRequest request) { var detail = await _dbContext.Queryable().Where(x => x.Id == request.Id).SingleAsync(); detail.ThrowIfNull("预约信息不存在"); if (detail.DoorStatus == VisitorAppointmentDetailStatusEnumType.Wait) { detail.DoorStatus = VisitorAppointmentDetailStatusEnumType.Arrived; } else { true.ThrowIfTrue($"数据有误:{detail.DoorStatus.GetDescription()}"); } detail.ArrivedTime = DateTime.Now; await _dbContext.UpdateAsync(detail); await _gainfoFaceService.UploadExternalFaceAsync(new GainfoUploadFaceRequest { UserNo = detail.IdNo, Face = request.Base64Image }); await GenerateDeviceTasksAsync(detail, false, DeviceTaskEnumType.Add, "访客门岗认证生成"); try { var r = await _dhApi.SendFaceAsync(new FaceRequest { IdNo = detail.IdNo, Face = request.Base64Image }); _logger.LogInformation("上传face数据到大华成功: {idNo} . {json}", detail.IdNo, r.ToJson()); } catch (Exception e) { _logger.LogError(e, "上传face数据到大华失败:{msg}", e.Message); } } public async Task GetTodayByIdNoAsync(string idNo, string factory) { var protector = _dataProtectionProvider.CreateProtector("gainfo:external:idno"); try { idNo = protector.Unprotect(idNo); } catch (Exception) { //ignore } var now = DateTime.Now; return await _dbContext.Queryable() .LeftJoin((x, ga) => ga.IdNo == x.IdNo) .Where((x, ga) => ga.IdNo == idNo) .Where(x => x.IsEnterDoorArea && now >= x.DoorStart && now <= x.DoorEnd && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Wait) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .Select(x => new AppointmentDetailResponse { No = x.No, Mst = SqlFunc.Subqueryable() .LeftJoin((p, ga) => p.Code == ga.Code) .Where((p, ga) => p.No == x.No) .First((p, ga) => new AppointmentPageResponse { No = p.No, Title = p.Title, Code = p.Code, Name = ga.Name, Source = p.Source, Date = p.Date, Remark = p.Remark, Factory = p.Factory, Reason = p.Reason }, false) }, true) .FirstAsync(); } public async Task FocusLeaveAsync(long id, string reason) { // 根据访客预约详情ID获取记录,若不存在则抛出异常 var detail = await _dbContext.Queryable().Where(x => x.Id == id).SingleAsync(); detail.ThrowIfNull("数据不存在"); // 如果已进入门区且门区状态为“已到达”,则将门区状态更新为“强制离场” if (detail.IsEnterDoorArea && detail.DoorStatus == VisitorAppointmentDetailStatusEnumType.Arrived) detail.DoorStatus = VisitorAppointmentDetailStatusEnumType.FocusLeave; // 如果已进入门区且门区状态为“待到达”,则将门区状态更新为“作废” if (detail.IsEnterDoorArea && detail.DoorStatus == VisitorAppointmentDetailStatusEnumType.Wait) detail.DoorStatus = VisitorAppointmentDetailStatusEnumType.Obsolete; // 如果已进入工作区且工作区状态为“已到达”,则将工作区状态更新为“强制离场” if (detail.IsEnterWorkplace && detail.WorkplaceStatus == VisitorAppointmentDetailStatusEnumType.Arrived) detail.WorkplaceStatus = VisitorAppointmentDetailStatusEnumType.FocusLeave; // 如果已进入工作区且工作区状态为“待到达”,则将工作区状态更新为“作废” if (detail.IsEnterWorkplace && detail.WorkplaceStatus == VisitorAppointmentDetailStatusEnumType.Wait) detail.WorkplaceStatus = VisitorAppointmentDetailStatusEnumType.Obsolete; // 记录强制离场时间和原因 detail.FocusLeaveDateTime = DateTime.Now; detail.Remark += "强制离场:" + reason; // 生成设备任务并更新数据库 await GenerateDeviceTasksAsync(detail, true, DeviceTaskEnumType.Delete, $"强制离场:{reason}"); await _dbContext.UpdateAsync(detail); } public async Task> GetRecordPageByIdNoAsync(AppointmentIdNoPageRequest request) { var protector = _dataProtectionProvider.CreateProtector("gainfo:external:idno"); try { request.IdNo = protector.Unprotect(request.IdNo); } catch (Exception) { //ignore } var page = await _dbContext.Queryable() .LeftJoin((x, ga) => x.UserNo == ga.No) .Where((x, ga) => ga.IdNo == request.IdNo) .OrderBy(x => x.CreateTime) .Select() .ToPageListAsync(request); return page; } public async Task> GetRecordListAsync(string factory, string search, VisitorAppointmentDetailStatusEnumType? status, DateTime start, DateTime end) { var now = DateTime.Now; var currentDateTime = start.Date.AddHours(now.Hour).AddMinutes(now.Minute); return await _dbContext.Queryable() .LeftJoin((x, ga) => ga.IdNo == x.IdNo) .Where(x => SqlFunc.Subqueryable() .Where(p => p.No == x.No) .Where(p => p.Factory == factory).Any() ) .Where(x => x.IsEnterDoorArea) .WhereIF(status.HasValue, x => x.DoorStatus == status) .WhereIF(status.HasValue && status == VisitorAppointmentDetailStatusEnumType.Wait, x => currentDateTime >= x.DoorStart && currentDateTime <= x.DoorEnd) .WhereIF(status.HasValue && status == VisitorAppointmentDetailStatusEnumType.Arrived, x => currentDateTime >= x.DoorStart && currentDateTime <= x.DoorEnd) .WhereIF(!status.HasValue, x => x.DoorStart >= start && x.DoorEnd <= end) .WhereIF(search.IsPresent(), x => x.Name.Contains(search) || x.No == search || x.IdNo == search) .Select(x => new AppointmentDetailResponse { No = x.No, Mst = SqlFunc.Subqueryable() .LeftJoin((p, ga) => p.Code == ga.Code) .Where((p, ga) => p.No == x.No) .First((p, ga) => new AppointmentPageResponse { No = p.No, Title = p.Title, Code = p.Code, Name = ga.Name, Source = p.Source, Date = p.Date, Remark = p.Remark, Factory = p.Factory, Reason = p.Reason }, false) }, true) .ToListAsync(); } public async Task GetSummaryAsync(string factory, DateTime start, DateTime end) { start = start.Date; var total = await _dbContext.Queryable() .Where(x => x.IsEnterDoorArea && x.DoorStart >= start && x.DoorStart <= end) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .CountAsync(); var @in = await _dbContext.Queryable() .Where(x => x.IsEnterDoorArea && x.ArrivedTime >= start && x.ArrivedTime <= end && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Arrived) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .CountAsync(); var notIn = await _dbContext.Queryable() .Where(x => x.IsEnterDoorArea && x.DoorStart >= start && x.DoorStart <= end) .Where(x => x.IsEnterDoorArea && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Wait) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .CountAsync(); var leave = await _dbContext.Queryable() .Where(x => x.IsEnterDoorArea && x.DoorEnd >= start && x.DoorEnd <= end) .Where(x => x.IsEnterDoorArea && (x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Leave || x.DoorStatus == VisitorAppointmentDetailStatusEnumType.FocusLeave)) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .CountAsync(); var notLeave = await _dbContext.Queryable() .Where(x => x.IsEnterDoorArea && x.DoorEnd >= start && x.DoorEnd <= end && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Arrived) .Where(x => SqlFunc.Subqueryable().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any()) .CountAsync(); return new AppointmentSummaryResponse { Total = total, In = @in, NotIn = notIn, Leave = leave, NotLeave = notLeave }; } public async Task UpdateFaceAsync(long detailId, string base64) { var detail = await _dbContext.Queryable().Where(x => x.Id == detailId).SingleAsync(); detail.ThrowIfNull("数据不存在"); await _gainfoFaceService.UploadExternalFaceAsync(new GainfoUploadFaceRequest { UserNo = detail.IdNo, Face = base64 }); await GenerateDeviceTasksAsync(detail, true, DeviceTaskEnumType.Update, "访客手动上传照片"); } public async Task> GetVisitorDevicesAsync(long detailId) { var detail = await _dbContext.Queryable().Where(x => x.Id == detailId).SingleAsync(); detail.ThrowIfNull("数据不存在"); var areaCodes = GetValidAreaCodes(detail, true).ToList(); if (areaCodes.IsEmpty()) return new List(); var existsAreaCodes = await _dbContext.Queryable().Where(x => areaCodes.Contains(x.CustomCode)).Select(x => x.CustomCode).ToListAsync(); //当前分配的设备组code var deviceGroupCodes = await _dbContext.Queryable() .Where(x => existsAreaCodes.Contains(x.CustomCode)) .Select(x => x.DeviceGroupCode) .ToListAsync(); var deviceCodes = await _dbContext.Queryable().Where(x => deviceGroupCodes.Contains(x.GroupCode)).Select(x => x.DeviceCode).ToListAsync(); var deviceUniqueNos = await _dbContext.Queryable().Where(x => deviceCodes.Contains(x.Code)).Select(x => x.UniqueNo).ToListAsync(); return deviceUniqueNos; } /// public async Task AutoResignAsync() { //获取外部人员 var externals = await _dbContext.Queryable() .Where(x => x.Type == GainfoUserTypeEnumType.External) .Select(x => new { x.Id, x.Code, x.Factory, x.CreateTime }) .ToListAsync(); var settings = await _factorySettingService.GetListAsync(false); //todo 临时处理循环依赖 var gainfoService = _serviceProvider.GetService(); //人员分组,key为factory foreach (var exts in externals.GroupBy(x => x.Factory)) { //factory的设置 var setting = settings.First(x => x.Factory == exts.Key); if (setting.NoRecordAutoResignExternalDays == 0) { continue; } foreach (var ext in exts) { // 查询该外部人员最近一次通行记录的时间 DateTime? raiseDateTime = await _dbContext.Queryable() .Where(x => x.UserUniqueNo == ext.Code) .GroupBy(x => x.UserUniqueNo) .Select(x => SqlFunc.IsNull(SqlFunc.AggregateMax(x.RaiseDateTime), default(DateTime?))) .SingleAsync(); // 如果最近通行时间存在且超出未通行天数阈值,则自动归档该外部人员 if (raiseDateTime.HasValue && raiseDateTime.Value.AddDays(setting.NoRecordAutoResignExternalDays) < DateTime.Now) { try { await gainfoService.ResignAsync(new GainfoResignRequest { Id = ext.Id, Reason = $"{setting.NoRecordAutoResignExternalDays}天无通行记录", Date = DateTime.Now }); } catch (Exception e) { // 记录自动归档异常日志 _logger.LogError(e, "自动归档外部人员异常:{msg}", e.Message); } } } } } //********************************************************************************************************************* private async Task GenerateDeviceTasksAsync(VisitorAppointmentDetailEntity detail, bool withWorkplace, DeviceTaskEnumType deviceTaskType, string source) { var gainfo = await _dbContext.Queryable().Where(x => x.IdNo == detail.IdNo || x.Code == detail.IdNo).SingleAsync(); gainfo.ThrowIfNull("人员信息不存在"); var areaCodes = GetValidAreaCodes(detail, withWorkplace).ToList(); if (areaCodes.IsEmpty()) return; var existsAreaCodes = await _dbContext.Queryable().Where(x => areaCodes.Contains(x.CustomCode)).Select(x => x.CustomCode).ToListAsync(); //当前分配的设备组code var deviceGroupCodes = await _dbContext.Queryable() .Where(x => existsAreaCodes.Contains(x.CustomCode)) .Select(x => x.DeviceGroupCode) .ToListAsync(); var deviceCodes = await _dbContext.Queryable().Where(x => deviceGroupCodes.Contains(x.GroupCode)).Select(x => x.DeviceCode).ToListAsync(); var deviceUniqueNos = await _dbContext.Queryable().Where(x => deviceCodes.Contains(x.Code)).Select(x => new { x.UniqueNo, x.TenantId }).ToListAsync(); await _dbContext.InsertAsync(deviceUniqueNos.Select(x => new DeviceTaskEntity { DeviceType = DeviceTypeEnumType.Door, DeviceUniqueNo = x.UniqueNo, Status = DeviceTaskStatusEnumType.Wait, UserNo = gainfo.No, Type = deviceTaskType, Priority = 100, TenantId = x.TenantId, Source = source, Version = 0 }).ToList()); } /// /// 获取有效的区域编码 /// /// /// /// private static IEnumerable GetValidAreaCodes(VisitorAppointmentDetailEntity detail, bool withWorkplace) { if (detail.IsEnterDoorArea) { yield return detail.ApplyDoorArea; } if (withWorkplace && detail.IsEnterWorkplace) { foreach (var item in detail.ApplyWorkplaceAreas) { yield return item; } } } }