From 03f20bedc8a3e6457d39ce42e25aa61d80260600 Mon Sep 17 00:00:00 2001 From: liguoyang Date: Tue, 17 Jun 2025 16:02:23 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E4=BA=BA=E5=91=98=E8=80=83=E5=8B=A4?= =?UTF-8?q?=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceDataAnalysisResultEnumType.cs | 46 +++ .../UserAttendanceDataAnalysisController.cs | 54 ++++ .../UserAttendanceDataAnalysisPageRequest.cs | 11 + .../Request/UserAttendanceUserCodesRequest.cs | 12 + .../UserAttendanceDataAnalysisResponse.cs | 63 ++++ .../IUserAttendanceDataAnalysisService.cs | 30 ++ .../UserAttendanceDataAnalysisService.cs | 269 ++++++++++++++++++ .../Dlhis.ScheduledTask.csproj | 4 + Src/Dlhis.ScheduledTask/Program.cs | 3 +- 9 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 Src/Dlhis.Entity/Basic/Enums/AttendanceDataAnalysisResultEnumType.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Controllers/UserAttendanceDataAnalysisController.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceDataAnalysisPageRequest.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceUserCodesRequest.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Response/UserAttendanceDataAnalysisResponse.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/IUserAttendanceDataAnalysisService.cs create mode 100644 Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/Implements/UserAttendanceDataAnalysisService.cs diff --git a/Src/Dlhis.Entity/Basic/Enums/AttendanceDataAnalysisResultEnumType.cs b/Src/Dlhis.Entity/Basic/Enums/AttendanceDataAnalysisResultEnumType.cs new file mode 100644 index 00000000..1f0b26fd --- /dev/null +++ b/Src/Dlhis.Entity/Basic/Enums/AttendanceDataAnalysisResultEnumType.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; + +namespace Dlhis.Entity.Basic.Enums; + +/// +/// 考勤数据分析结果枚举类型。 +/// +[Description("考勤数据分析结果")] +public enum AttendanceDataAnalysisResultEnumType +{ + /// + /// 休息日/节假日 + /// + [Description("休息日/节假日")] + Rest = 0, + + /// + /// 正常考勤状态 + /// + [Description("正常")] + Normal = 10, + + /// + /// 迟到 + /// + [Description("迟到")] + Late = 20, + + /// + /// 早退 + /// + [Description("早退")] + Early = 30, + + /// + /// 缺卡 + /// + [Description("缺卡")] + Missing = 40, + + /// + /// 无考勤记录 + /// + [Description("无考勤记录")] + NoAttendanceRecord = 50 +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Controllers/UserAttendanceDataAnalysisController.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Controllers/UserAttendanceDataAnalysisController.cs new file mode 100644 index 00000000..c91ec05c --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Controllers/UserAttendanceDataAnalysisController.cs @@ -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; + +/// +/// 用户考勤数据分析 +/// +[Authorize] +[Route("api/[controller]")] +[ApiController] +public class UserAttendanceDataAnalysisController : ControllerBase +{ + private readonly IUserAttendanceDataAnalysisService _userAttendanceDataAnalysisService; + + public UserAttendanceDataAnalysisController(IUserAttendanceDataAnalysisService userAttendanceDataAnalysisService) + { + _userAttendanceDataAnalysisService = userAttendanceDataAnalysisService; + } + + /// + /// 保存用户(批量) + /// + /// 用户考勤用户编码请求 + [HttpPost("save")] + public async Task SaveAsync(UserAttendanceUserCodesRequest request) + { + await _userAttendanceDataAnalysisService.SaveAsync(request); + } + + /// + /// 删除用户(批量) + /// + /// 用户考勤用户编码请求 + [HttpPost("delete")] + public async Task DeleteAsync(UserAttendanceUserCodesRequest request) + { + await _userAttendanceDataAnalysisService.DeleteAsync(request); + } + + /// + /// 分页获取用户考勤数据分析结果 + /// + /// 分页请求参数 + /// 用户编码列表 + [HttpGet("analysis-data")] + public async Task GetPageAsync([FromQuery] UserAttendanceDataAnalysisPageRequest request) + { + return await _userAttendanceDataAnalysisService.GetPageAsync(request); + } +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceDataAnalysisPageRequest.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceDataAnalysisPageRequest.cs new file mode 100644 index 00000000..c7de4a56 --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceDataAnalysisPageRequest.cs @@ -0,0 +1,11 @@ +using Kurisu.AspNetCore.DataAccess.Dto; + +namespace Dlhis.ScheduledTask.DeviceUploadLog.Request; + +public class UserAttendanceDataAnalysisPageRequest : PageDto +{ + /// + /// 查询日期 + /// + public DateOnly Date { get; set; } +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceUserCodesRequest.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceUserCodesRequest.cs new file mode 100644 index 00000000..2aa256b8 --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Request/UserAttendanceUserCodesRequest.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dlhis.ScheduledTask.DeviceUploadLog.Request; + +public class UserAttendanceUserCodesRequest +{ + /// + /// 工号 + /// + [MinLength(1, ErrorMessage = "人员不可为空")] + public List UserCodes { get; set; } +} diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Response/UserAttendanceDataAnalysisResponse.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Response/UserAttendanceDataAnalysisResponse.cs new file mode 100644 index 00000000..dda6c334 --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Response/UserAttendanceDataAnalysisResponse.cs @@ -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 +{ + /// + /// 班次时间范围 + /// + public List ShiftTimes { get; set; } + + /// + /// 截止到当前时间 + /// + public DateTime LastAnalysisDateTime { get; set; } + + /// + /// 人员考勤数据分析结果 + /// + public Pagination Pagination { get; set; } +} + +public class UserAttendanceDataAnalysisResponse +{ + /// + /// 工号 + /// + public string UserCode { get; set; } + + /// + /// 名称 + /// + public string UserName { get; set; } + + /// + /// 部门名称 + /// + public string DepartmentName { get; set; } + + /// + /// 上班分析结果类型 + /// + public List StartAnalysisResultTypes { get; } = new(); + + /// + /// 结果 + /// + public List StartResults => StartAnalysisResultTypes.Select(x => x.GetDescription()).ToList(); + + + /// + /// 下班分析结果类型 + /// + public List EndAnalysisResultTypes { get; } = new(); + + /// + /// 结果 + /// + public List EndResults => EndAnalysisResultTypes.Select(x => x.GetDescription()).ToList(); +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/IUserAttendanceDataAnalysisService.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/IUserAttendanceDataAnalysisService.cs new file mode 100644 index 00000000..3b349494 --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/IUserAttendanceDataAnalysisService.cs @@ -0,0 +1,30 @@ +using Dlhis.ScheduledTask.DeviceUploadLog.Request; +using Dlhis.ScheduledTask.DeviceUploadLog.Response; +using Kurisu.AspNetCore.DataAccess.Dto; + +namespace Dlhis.ScheduledTask.DeviceUploadLog.Services; + +/// +/// 用户考勤数据分析服务接口 +/// +public interface IUserAttendanceDataAnalysisService : IScopeDependency +{ + /// + /// 异步保存用户考勤数据 + /// + /// 用户考勤用户编码请求 + Task SaveAsync(UserAttendanceUserCodesRequest request); + + /// + /// 异步删除用户考勤数据 + /// + /// 用户考勤用户编码请求 + Task DeleteAsync(UserAttendanceUserCodesRequest request); + + /// + /// 分页获取用户考勤数据分析结果 + /// + /// 分页请求参数 + /// 用户编码列表 + Task GetPageAsync(UserAttendanceDataAnalysisPageRequest request); +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/Implements/UserAttendanceDataAnalysisService.cs b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/Implements/UserAttendanceDataAnalysisService.cs new file mode 100644 index 00000000..19a15d76 --- /dev/null +++ b/Src/Dlhis.ScheduledTask/DeviceUploadLog/Services/Implements/UserAttendanceDataAnalysisService.cs @@ -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() + .Where(x => request.UserCodes.Contains(x.Code)) + .Where(x => SqlFunc.Subqueryable() + .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() + .Where(x => request.UserCodes.Contains(x.UserCode)) + .ExecuteCommandAsync(); + } + + public async Task GetPageAsync(UserAttendanceDataAnalysisPageRequest request) + { + //查询日期的时间范围 + var searchStart = request.Date.ToDateTime(TimeOnly.MinValue); + var searchEnd = request.Date.ToDateTime(TimeOnly.MaxValue).AddSeconds(1); + + var page = await _dbContext.Queryable() + .LeftJoin((x, user) => x.UserCode == user.Code) + .LeftJoin((x, user, dept) => user.DepartmentNo == dept.No) + .WhereIF(request.Search.IsPresent(), x => x.UserCode == request.Search) + .Where(x => SqlFunc.Subqueryable().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() + .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(2) { shiftStartRange, shiftEndRange }, + LastAnalysisDateTime = request.Date < DateOnly.FromDateTime(DateTime.Now.Date) + ? request.Date.ToDateTime(TimeOnly.MaxValue) + : DateTime.Now, + Pagination = page + }; + } + + /// + /// 处理上班考勤数据分析结果。 + /// + /// + /// + /// + private void HandlerClockIn(List analysisResultTypes, List 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); + } + } + } + + /// + /// 处理下班考勤数据分析结果。 + /// + /// + /// + /// + private void HandlerClockOut(List analysisResultTypes, List 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() + .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); + } + + /// + /// 检查指定日期是否为休息日或节假日。 + /// + /// + /// + private async Task IsRestDayAsync(DateOnly searchDate) + { + var now = DateTime.Now; + + var currentHolidays = await _dbContext.Queryable() + .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; + } +} + +/// +/// +/// +public class ShiftTimeRange +{ + /// + /// + /// + /// + /// + /// + /// + 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; } +} \ No newline at end of file diff --git a/Src/Dlhis.ScheduledTask/Dlhis.ScheduledTask.csproj b/Src/Dlhis.ScheduledTask/Dlhis.ScheduledTask.csproj index 43855326..01b99815 100644 --- a/Src/Dlhis.ScheduledTask/Dlhis.ScheduledTask.csproj +++ b/Src/Dlhis.ScheduledTask/Dlhis.ScheduledTask.csproj @@ -9,6 +9,10 @@ True + + 1701;1702;1591 + + diff --git a/Src/Dlhis.ScheduledTask/Program.cs b/Src/Dlhis.ScheduledTask/Program.cs index 234c2ead..2f8108cb 100644 --- a/Src/Dlhis.ScheduledTask/Program.cs +++ b/Src/Dlhis.ScheduledTask/Program.cs @@ -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;