Files
jinko.yikatong/Src/Dlhis.Basic/Appointment/Services/Implements/AppointmentService.cs

547 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Dlhis.Basic.Appointment.Request;
using Dlhis.Basic.Appointment.Response;
using Dlhis.Basic.Common.Message.Requests;
using Dlhis.Basic.Common.Message.Services;
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 IJinkoApiService _jinkoApiService;
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,
IJinkoApiService jinkoApiService,
IFactorySettingService factorySettingService,
IGainfoFaceService gainfoFaceService,
IServiceProvider serviceProvider,
ILogger<AppointmentService> logger)
{
_dbContext = dbContext;
_dataProtectionProvider = dataProtectionProvider;
_currentUser = currentUser;
_dhApi = dhApi;
_jinkoApiService = jinkoApiService;
_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("预约信息不存在");
//提前传
await UploadFaceToDhAsync(detail.IdNo, request.Base64Image);
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, "访客门岗认证生成");
}
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);
var appointment = await _dbContext.Queryable<VisitorAppointmentEntity>().Where(x => x.No == detail.No).SingleAsync();
await _jinkoApiService.SendWorkMessageAsync(new DingTalkWorkMessageRequest
{
WorkCodes = new List<string> { appointment.Code },
Content = $"您的访客预约《{appointment.Title}》中的访客{detail.Name}已被强制离场,原因:{reason}。"
});
}
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 UploadFaceToDhAsync(detail.IdNo, base64);
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>();
var holidaies = await _dbContext
.Queryable<HolidayEntity>()
.Select(x => new
{
x.Type,
Period = new DatePeriod
{
Start = x.Start,
End = x.End
}
})
.ToListAsync();
var weekends = new List<DayOfWeek>() { DayOfWeek.Saturday, DayOfWeek.Sunday };
//人员分组,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)
{
var checkDates = GetDaysUntilToday(raiseDateTime.Value);
var days = checkDates.Count;
foreach (var date in checkDates)
{
//date.DayOfWeek
foreach (var holiday in holidaies)
{
var isPresent = date >= holiday.Period.Start && date <= holiday.Period.Start;
if (isPresent)
{
if (holiday.Type == HolidayEnumType.Rest)
{
days--;
}
}
else
{
if (weekends.Contains(date.DayOfWeek))
{
days--;
}
}
}
}
if (days >= setting.NoRecordAutoResignExternalDays)
{
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 List<DateOnly> GetDaysUntilToday(DateTime begin)
{
var start = DateOnly.FromDateTime(begin.Date);
var today = DateOnly.FromDateTime(DateTime.Today);
if (start > today) return new List<DateOnly>();
var days = new List<DateOnly>();
for (var d = start; d <= today; d = d.AddDays(1))
{
days.Add(d);
}
return days;
}
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;
}
}
}
private async Task UploadFaceToDhAsync(string idNo, string base64Image)
{
try
{
var r = await _dhApi.SendFaceAsync(new FaceRequest
{
IdNo = idNo,
Face = base64Image
});
_logger.LogInformation("上传face数据到大华成功: {idNo} . {json}", idNo, r.ToJson());
}
catch (Exception e)
{
_logger.LogError(e, "上传face数据到大华失败:{msg}", e.Message);
}
}
}