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;