feat 人员考勤分析

This commit is contained in:
2025-06-17 16:02:23 +08:00
parent 72c4240764
commit 03f20bedc8
9 changed files with 490 additions and 2 deletions

View File

@@ -0,0 +1,54 @@
using Dlhis.ScheduledTask.DeviceUploadLog.Request;
using Dlhis.ScheduledTask.DeviceUploadLog.Response;
using Dlhis.ScheduledTask.DeviceUploadLog.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Controllers;
/// <summary>
/// 用户考勤数据分析
/// </summary>
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UserAttendanceDataAnalysisController : ControllerBase
{
private readonly IUserAttendanceDataAnalysisService _userAttendanceDataAnalysisService;
public UserAttendanceDataAnalysisController(IUserAttendanceDataAnalysisService userAttendanceDataAnalysisService)
{
_userAttendanceDataAnalysisService = userAttendanceDataAnalysisService;
}
/// <summary>
/// 保存用户(批量)
/// </summary>
/// <param name="request">用户考勤用户编码请求</param>
[HttpPost("save")]
public async Task SaveAsync(UserAttendanceUserCodesRequest request)
{
await _userAttendanceDataAnalysisService.SaveAsync(request);
}
/// <summary>
/// 删除用户(批量)
/// </summary>
/// <param name="request">用户考勤用户编码请求</param>
[HttpPost("delete")]
public async Task DeleteAsync(UserAttendanceUserCodesRequest request)
{
await _userAttendanceDataAnalysisService.DeleteAsync(request);
}
/// <summary>
/// 分页获取用户考勤数据分析结果
/// </summary>
/// <param name="request">分页请求参数</param>
/// <returns>用户编码列表</returns>
[HttpGet("analysis-data")]
public async Task<UserAttendanceDataAnalysisInfoResponse> GetPageAsync([FromQuery] UserAttendanceDataAnalysisPageRequest request)
{
return await _userAttendanceDataAnalysisService.GetPageAsync(request);
}
}

View File

@@ -0,0 +1,11 @@
using Kurisu.AspNetCore.DataAccess.Dto;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Request;
public class UserAttendanceDataAnalysisPageRequest : PageDto
{
/// <summary>
/// 查询日期
/// </summary>
public DateOnly Date { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Request;
public class UserAttendanceUserCodesRequest
{
/// <summary>
/// 工号
/// </summary>
[MinLength(1, ErrorMessage = "人员不可为空")]
public List<string> UserCodes { get; set; }
}

View File

@@ -0,0 +1,63 @@
using Dlhis.Entity.Basic.Enums;
using Dlhis.ScheduledTask.DeviceUploadLog.Services.Implements;
using Kurisu.AspNetCore.DataAccess.Dto;
using Kurisu.AspNetCore.Utils.Extensions;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Response;
public class UserAttendanceDataAnalysisInfoResponse
{
/// <summary>
/// 班次时间范围
/// </summary>
public List<ShiftTimeRange> ShiftTimes { get; set; }
/// <summary>
/// 截止到当前时间
/// </summary>
public DateTime LastAnalysisDateTime { get; set; }
/// <summary>
/// 人员考勤数据分析结果
/// </summary>
public Pagination<UserAttendanceDataAnalysisResponse> Pagination { get; set; }
}
public class UserAttendanceDataAnalysisResponse
{
/// <summary>
/// 工号
/// </summary>
public string UserCode { get; set; }
/// <summary>
/// 名称
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 部门名称
/// </summary>
public string DepartmentName { get; set; }
/// <summary>
/// 上班分析结果类型
/// </summary>
public List<AttendanceDataAnalysisResultEnumType> StartAnalysisResultTypes { get; } = new();
/// <summary>
/// 结果
/// </summary>
public List<string> StartResults => StartAnalysisResultTypes.Select(x => x.GetDescription()).ToList();
/// <summary>
/// 下班分析结果类型
/// </summary>
public List<AttendanceDataAnalysisResultEnumType> EndAnalysisResultTypes { get; } = new();
/// <summary>
/// 结果
/// </summary>
public List<string> EndResults => EndAnalysisResultTypes.Select(x => x.GetDescription()).ToList();
}

View File

@@ -0,0 +1,30 @@
using Dlhis.ScheduledTask.DeviceUploadLog.Request;
using Dlhis.ScheduledTask.DeviceUploadLog.Response;
using Kurisu.AspNetCore.DataAccess.Dto;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Services;
/// <summary>
/// 用户考勤数据分析服务接口
/// </summary>
public interface IUserAttendanceDataAnalysisService : IScopeDependency
{
/// <summary>
/// 异步保存用户考勤数据
/// </summary>
/// <param name="request">用户考勤用户编码请求</param>
Task SaveAsync(UserAttendanceUserCodesRequest request);
/// <summary>
/// 异步删除用户考勤数据
/// </summary>
/// <param name="request">用户考勤用户编码请求</param>
Task DeleteAsync(UserAttendanceUserCodesRequest request);
/// <summary>
/// 分页获取用户考勤数据分析结果
/// </summary>
/// <param name="request">分页请求参数</param>
/// <returns>用户编码列表</returns>
Task<UserAttendanceDataAnalysisInfoResponse> GetPageAsync(UserAttendanceDataAnalysisPageRequest request);
}

View File

@@ -0,0 +1,269 @@
using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Basic.Enums;
using Dlhis.Entity.Device.Entities;
using Dlhis.Entity.Device.Enums;
using Dlhis.ScheduledTask.DeviceUploadLog.Request;
using Dlhis.ScheduledTask.DeviceUploadLog.Response;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.Utils.Extensions;
using Org.BouncyCastle.Asn1.X9;
using SqlSugar;
namespace Dlhis.ScheduledTask.DeviceUploadLog.Services.Implements;
public class UserAttendanceDataAnalysisService : IUserAttendanceDataAnalysisService
{
private readonly IDbContext _dbContext;
public UserAttendanceDataAnalysisService(IDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task SaveAsync(UserAttendanceUserCodesRequest request)
{
//获取人员时候,自动过滤存在的人员,并且只获取在职的
var gainfos = await _dbContext.Queryable<GainfoEntity>()
.Where(x => request.UserCodes.Contains(x.Code))
.Where(x => SqlFunc.Subqueryable<UserAttendanceDataAnalysisEntity>()
.Where(p => p.UserCode == x.Code)
.NotAny()
)
.ToListAsync();
await _dbContext.InsertAsync(gainfos.Select(x => new UserAttendanceDataAnalysisEntity
{
UserCode = x.Code
}).ToList()
);
}
public async Task DeleteAsync(UserAttendanceUserCodesRequest request)
{
//直接删除
await _dbContext.Deleteable<UserAttendanceDataAnalysisEntity>()
.Where(x => request.UserCodes.Contains(x.UserCode))
.ExecuteCommandAsync();
}
public async Task<UserAttendanceDataAnalysisInfoResponse> GetPageAsync(UserAttendanceDataAnalysisPageRequest request)
{
//查询日期的时间范围
var searchStart = request.Date.ToDateTime(TimeOnly.MinValue);
var searchEnd = request.Date.ToDateTime(TimeOnly.MaxValue).AddSeconds(1);
var page = await _dbContext.Queryable<UserAttendanceDataAnalysisEntity>()
.LeftJoin<GainfoEntity>((x, user) => x.UserCode == user.Code)
.LeftJoin<DepartmentEntity>((x, user, dept) => user.DepartmentNo == dept.No)
.WhereIF(request.Search.IsPresent(), x => x.UserCode == request.Search)
.Where(x => SqlFunc.Subqueryable<GainfoEntity>().Where(p => p.Code == x.UserCode).Any())
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.Select((x, user, dept) => new UserAttendanceDataAnalysisResponse
{
UserCode = x.UserCode,
UserName = user.Name,
DepartmentName = dept.Name
})
.ToPageListAsync(request);
var (shiftStartRange, shiftEndRange) = await GetShiftTimeRangesAsync(request.Date);
if (await IsRestDayAsync(request.Date))
{
foreach (var item in page.Data)
{
item.StartAnalysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.Rest);
item.EndAnalysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.Rest);
}
}
else
{
var userCodes = page.Data.Select(x => x.UserCode).ToList();
var logs = await _dbContext.Queryable<DeviceUploadLogEntity>()
.Where(x => x.RaiseDateTime >= searchStart && x.RaiseDateTime < searchEnd)
.Where(x => x.Type == DeviceUploadLogTypeEnumType.Attendance)
.Where(x => userCodes.Contains(x.UserUniqueNo))
.ToListAsync();
foreach (var item in page.Data)
{
var userLogs = logs.Where(x => x.UserUniqueNo == item.UserCode).ToList();
//无考勤记录
if (userLogs.Count == 0)
{
item.StartAnalysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.NoAttendanceRecord);
item.EndAnalysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.NoAttendanceRecord);
continue;
}
HandlerClockIn(item.StartAnalysisResultTypes, userLogs, shiftStartRange);
HandlerClockOut(item.EndAnalysisResultTypes, userLogs, shiftEndRange);
}
}
return new UserAttendanceDataAnalysisInfoResponse
{
ShiftTimes = new List<ShiftTimeRange>(2) { shiftStartRange, shiftEndRange },
LastAnalysisDateTime = request.Date < DateOnly.FromDateTime(DateTime.Now.Date)
? request.Date.ToDateTime(TimeOnly.MaxValue)
: DateTime.Now,
Pagination = page
};
}
/// <summary>
/// 处理上班考勤数据分析结果。
/// </summary>
/// <param name="analysisResultTypes"></param>
/// <param name="userLogs"></param>
/// <param name="shift"></param>
private void HandlerClockIn(List<AttendanceDataAnalysisResultEnumType> analysisResultTypes, List<DeviceUploadLogEntity> userLogs, ShiftTimeRange shift)
{
var inLogs = userLogs.OrderBy(x => x.RaiseDateTime)
.Where(x => x.RaiseDateTime >= shift.Start && x.RaiseDateTime <= shift.End)
.ToList();
if (inLogs.Count == 0)
{
analysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.NoAttendanceRecord);
}
else
{
var isClockIn = inLogs.Any(x => x.RaiseDateTime <= shift.Time);
if (!isClockIn)
{
var isLate = inLogs.Any(x => !(x.RaiseDateTime <= shift.Time) && x.RaiseDateTime > shift.Time && x.RaiseDateTime <= shift.End);
analysisResultTypes.Add(
isLate
? AttendanceDataAnalysisResultEnumType.Late
: AttendanceDataAnalysisResultEnumType.Missing
);
}
else
{
analysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.Normal);
}
}
}
/// <summary>
/// 处理下班考勤数据分析结果。
/// </summary>
/// <param name="analysisResultTypes"></param>
/// <param name="userLogs"></param>
/// <param name="shift"></param>
private void HandlerClockOut(List<AttendanceDataAnalysisResultEnumType> analysisResultTypes, List<DeviceUploadLogEntity> userLogs, ShiftTimeRange shift)
{
var outLogs = userLogs.OrderBy(x => x.RaiseDateTime)
.Where(x => x.RaiseDateTime >= shift.Start)
.ToList();
if (outLogs.Count == 0)
{
analysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.NoAttendanceRecord);
}
else
{
var isClockOut = outLogs.Any(x => x.RaiseDateTime >= shift.Time);
if (!isClockOut)
{
var isEarly = outLogs.Any(x => x.RaiseDateTime < shift.Time);
analysisResultTypes.Add(
isEarly
? AttendanceDataAnalysisResultEnumType.Early
: AttendanceDataAnalysisResultEnumType.Missing
);
}
else
{
analysisResultTypes.Add(AttendanceDataAnalysisResultEnumType.Normal);
}
}
}
private async Task<(ShiftTimeRange ShiftStartRange, ShiftTimeRange shiftEndRange)> GetShiftTimeRangesAsync(DateOnly date)
{
var longDayShifts = await _dbContext.Queryable<BasicTypeEntity>()
.Where(x => x.Type.StartsWith("LongDayShift"))
.Select(x => new { x.Code, x.Type })
.ToListAsync();
var longDayShift = longDayShifts.FirstOrDefault(x => x.Type == "LongDayShift")?.Code;
longDayShift.IsPresent().ThrowIfFalse("请先配置长白班的时间段LongDayShift");
// 上班时间范围
var longDayShiftStart = longDayShifts.FirstOrDefault(x => x.Type == "LongDayShift-Start")?.Code;
longDayShiftStart.IsPresent().ThrowIfFalse("请先配置长白班的时间段LongDayShift-Start");
var startArr = longDayShiftStart!.Split(",");
// 下班时间范围
var longDayShiftEnd = longDayShifts.FirstOrDefault(x => x.Type == "LongDayShift-End")?.Code;
longDayShiftEnd.IsPresent().ThrowIfFalse("请先配置长白班的时间段LongDayShift-End");
var arr = longDayShift!.Split("-");
var shiftStart = TimeOnly.Parse(arr[0]);
var shiftEnd = TimeOnly.Parse(arr[1]);
var startRange = new ShiftTimeRange(date, shiftStart.AddMinutes(int.Parse(startArr[0])), shiftStart, shiftStart.AddMinutes(int.Parse(startArr[1])));
var endRange = new ShiftTimeRange(date, shiftEnd.AddMinutes(int.Parse(longDayShiftEnd!)), shiftEnd, TimeOnly.MaxValue);
return (startRange, endRange);
}
/// <summary>
/// 检查指定日期是否为休息日或节假日。
/// </summary>
/// <param name="searchDate"></param>
/// <returns></returns>
private async Task<bool> IsRestDayAsync(DateOnly searchDate)
{
var now = DateTime.Now;
var currentHolidays = await _dbContext.Queryable<HolidayEntity>()
.Where(x => x.Year == now.Year && searchDate >= x.Start && searchDate <= x.End)
.Select(x => new { x.Type, x.Name, x.Start, x.End })
.ToListAsync();
// 优先判断节假日类型
if (currentHolidays.Any(x => x.Type == HolidayEnumType.Rest))
{
return true;
}
if (currentHolidays.Any(x => x.Type == HolidayEnumType.NeedWork))
{
return false;
}
// 否则按周末判断
var isWeekend = searchDate.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
return isWeekend;
}
}
/// <summary>
///
/// </summary>
public class ShiftTimeRange
{
/// <summary>
///
/// </summary>
/// <param name="date"></param>
/// <param name="start"></param>
/// <param name="time"></param>
/// <param name="end"></param>
public ShiftTimeRange(DateOnly date, TimeOnly start, TimeOnly time, TimeOnly end)
{
Start = date.ToDateTime(start);
Time = date.ToDateTime(time);
End = date.ToDateTime(end);
}
public DateTime Start { get; set; }
public DateTime Time { get; set; }
public DateTime End { get; set; }
}

View File

@@ -9,6 +9,10 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="log\**" />
<Content Remove="log\**" />

View File

@@ -1,5 +1,4 @@
using Dlhis.Sys.Api;
using Dlhis.Sys.Api.Request;
using Dlhis.Jip.Api.ClientHandlers;
using Kurisu.AspNetCore.Cache.Extensions;
using Kurisu.AspNetCore.Startup;