467 lines
21 KiB
C#
467 lines
21 KiB
C#
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<AppointmentService> _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<AppointmentService> logger)
|
||
{
|
||
_dbContext = dbContext;
|
||
_dataProtectionProvider = dataProtectionProvider;
|
||
_currentUser = currentUser;
|
||
_dhApi = dhApi;
|
||
_factorySettingService = factorySettingService;
|
||
_logger = logger;
|
||
_gainfoFaceService = gainfoFaceService;
|
||
_serviceProvider = serviceProvider;
|
||
}
|
||
|
||
public async Task<Pagination<AppointmentPageResponse>> GetPageAsync(AppointmentPageRequest request)
|
||
{
|
||
var factory = _currentUser.GetStringTenantId();
|
||
|
||
return await _dbContext.Queryable<VisitorAppointmentEntity>()
|
||
.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<VisitorAppointmentDetailEntity>()
|
||
.Where(p => p.No == x.No)
|
||
.Where(p => p.IdNo == request.Search || p.Name == request.Search).Any()
|
||
)
|
||
.Where(x => x.Factory == factory)
|
||
.Select<AppointmentPageResponse>()
|
||
.ToPageListAsync(request);
|
||
}
|
||
|
||
public async Task<List<AppointmentDetailResponse>> GetAppointmentDetailsByIdAsync(string no)
|
||
{
|
||
return await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.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<AppointmentAreaEntity>().Where(p => p.CustomCode == x.ApplyDoorArea).Select(p => p.Name),
|
||
DoorStart = x.DoorStart,
|
||
DoorEnd = x.DoorEnd,
|
||
IsEnterWorkplace = x.IsEnterWorkplace,
|
||
ApplyWorkplaceAreas = SqlFunc.Subqueryable<AppointmentAreaEntity>().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<VisitorAppointmentDetailEntity>().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<AppointmentDetailResponse> 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<VisitorAppointmentDetailEntity>()
|
||
.LeftJoin<GainfoEntity>((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<VisitorAppointmentEntity>().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any())
|
||
.Select(x => new AppointmentDetailResponse
|
||
{
|
||
No = x.No,
|
||
Mst = SqlFunc.Subqueryable<VisitorAppointmentEntity>()
|
||
.LeftJoin<GainfoEntity>((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<VisitorAppointmentDetailEntity>().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<Pagination<AppointmentDetailResponse>> 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<VisitorAppointmentDetailEntity>()
|
||
.LeftJoin<GainfoEntity>((x, ga) => x.UserNo == ga.No)
|
||
.Where((x, ga) => ga.IdNo == request.IdNo)
|
||
.OrderBy(x => x.CreateTime)
|
||
.Select<AppointmentDetailResponse>()
|
||
.ToPageListAsync(request);
|
||
|
||
return page;
|
||
}
|
||
|
||
|
||
public async Task<List<AppointmentDetailResponse>> 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<VisitorAppointmentDetailEntity>()
|
||
.LeftJoin<GainfoEntity>((x, ga) => ga.IdNo == x.IdNo)
|
||
.Where(x => SqlFunc.Subqueryable<VisitorAppointmentEntity>()
|
||
.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<VisitorAppointmentEntity>()
|
||
.LeftJoin<GainfoEntity>((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<AppointmentSummaryResponse> GetSummaryAsync(string factory, DateTime start, DateTime end)
|
||
{
|
||
start = start.Date;
|
||
|
||
var total = await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.Where(x => x.IsEnterDoorArea && x.DoorStart >= start && x.DoorStart <= end)
|
||
.Where(x => SqlFunc.Subqueryable<VisitorAppointmentEntity>().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any())
|
||
.CountAsync();
|
||
|
||
var @in = await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.Where(x => x.IsEnterDoorArea && x.ArrivedTime >= start && x.ArrivedTime <= end && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Arrived)
|
||
.Where(x => SqlFunc.Subqueryable<VisitorAppointmentEntity>().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any())
|
||
.CountAsync();
|
||
|
||
var notIn = await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.Where(x => x.IsEnterDoorArea && x.DoorStart >= start && x.DoorStart <= end)
|
||
.Where(x => x.IsEnterDoorArea && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Wait)
|
||
.Where(x => SqlFunc.Subqueryable<VisitorAppointmentEntity>().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any())
|
||
.CountAsync();
|
||
|
||
var leave = await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.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<VisitorAppointmentEntity>().Where(p => p.No == x.No).Where(p => p.Factory == factory).Any())
|
||
.CountAsync();
|
||
|
||
var notLeave = await _dbContext.Queryable<VisitorAppointmentDetailEntity>()
|
||
.Where(x => x.IsEnterDoorArea && x.DoorEnd >= start && x.DoorEnd <= end && x.DoorStatus == VisitorAppointmentDetailStatusEnumType.Arrived)
|
||
.Where(x => SqlFunc.Subqueryable<VisitorAppointmentEntity>().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<VisitorAppointmentDetailEntity>().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<List<string>> GetVisitorDevicesAsync(long detailId)
|
||
{
|
||
var detail = await _dbContext.Queryable<VisitorAppointmentDetailEntity>().Where(x => x.Id == detailId).SingleAsync();
|
||
detail.ThrowIfNull("数据不存在");
|
||
|
||
var areaCodes = GetValidAreaCodes(detail, true).ToList();
|
||
if (areaCodes.IsEmpty()) return new List<string>();
|
||
|
||
var existsAreaCodes = await _dbContext.Queryable<AppointmentAreaEntity>().Where(x => areaCodes.Contains(x.CustomCode)).Select(x => x.CustomCode).ToListAsync();
|
||
|
||
//当前分配的设备组code
|
||
var deviceGroupCodes = await _dbContext.Queryable<AppointmentAreaAssignmentEntity>()
|
||
.Where(x => existsAreaCodes.Contains(x.CustomCode))
|
||
.Select(x => x.DeviceGroupCode)
|
||
.ToListAsync();
|
||
|
||
var deviceCodes = await _dbContext.Queryable<DeviceGroupRelationshipEntity>().Where(x => deviceGroupCodes.Contains(x.GroupCode)).Select(x => x.DeviceCode).ToListAsync();
|
||
var deviceUniqueNos = await _dbContext.Queryable<DeviceEntity>().Where(x => deviceCodes.Contains(x.Code)).Select(x => x.UniqueNo).ToListAsync();
|
||
|
||
return deviceUniqueNos;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task AutoResignAsync()
|
||
{
|
||
//获取外部人员
|
||
var externals = await _dbContext.Queryable<GainfoEntity>()
|
||
.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<IGainfoService>();
|
||
|
||
//人员分组,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<DeviceUploadLogEntity>()
|
||
.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<GainfoEntity>().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<AppointmentAreaEntity>().Where(x => areaCodes.Contains(x.CustomCode)).Select(x => x.CustomCode).ToListAsync();
|
||
|
||
//当前分配的设备组code
|
||
var deviceGroupCodes = await _dbContext.Queryable<AppointmentAreaAssignmentEntity>()
|
||
.Where(x => existsAreaCodes.Contains(x.CustomCode))
|
||
.Select(x => x.DeviceGroupCode)
|
||
.ToListAsync();
|
||
|
||
var deviceCodes = await _dbContext.Queryable<DeviceGroupRelationshipEntity>().Where(x => deviceGroupCodes.Contains(x.GroupCode)).Select(x => x.DeviceCode).ToListAsync();
|
||
var deviceUniqueNos = await _dbContext.Queryable<DeviceEntity>().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());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取有效的区域编码
|
||
/// </summary>
|
||
/// <param name="detail"></param>
|
||
/// <param name="withWorkplace"></param>
|
||
/// <returns></returns>
|
||
private static IEnumerable<string> 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;
|
||
}
|
||
}
|
||
}
|
||
} |