using System.Text.Json; using F9WebAiTest.Application.Common; using F9WebAiTest.Application.Dtos; using F9WebAiTest.Application.Dtos.Attendance; using F9WebAiTest.Application.Services.Notice; using F9WebAiTest.Domain.Common; using F9WebAiTest.Domain.Entities; using F9WebAiTest.Domain.Entities.Attendance; using F9WebAiTest.Domain.Entities.Notice; using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; using MediatR; namespace F9WebAiTest.Application.Services.Attendance; public class AttendanceService : IAttendanceService { private readonly IRepository _attRepo; private readonly IRepository _leaveRepo; private readonly IRepository _approverRepo; private readonly IRepository _empRepo; private readonly IRepository _deptRepo; private readonly IRepository _roleRepo; private readonly IRepository _empRoleRepo; private readonly IRepository _msgRepo; private readonly IRepository _receiverRepo; private readonly IRepository _todoRepo; private readonly IRepository _typeRepo; private readonly IMediator _mediator; // 上下班时间(可配置,这里简化固定) private static readonly TimeSpan WorkStart = new(9, 0, 0); private static readonly TimeSpan WorkEnd = new(18, 0, 0); public AttendanceService( IRepository attRepo, IRepository leaveRepo, IRepository approverRepo, IRepository empRepo, IRepository deptRepo, IRepository roleRepo, IRepository empRoleRepo, IRepository msgRepo, IRepository receiverRepo, IRepository todoRepo, IRepository typeRepo, IMediator mediator) { _attRepo = attRepo; _leaveRepo = leaveRepo; _approverRepo = approverRepo; _empRepo = empRepo; _deptRepo = deptRepo; _roleRepo = roleRepo; _empRoleRepo = empRoleRepo; _msgRepo = msgRepo; _receiverRepo = receiverRepo; _todoRepo = todoRepo; _typeRepo = typeRepo; _mediator = mediator; } public async Task PunchAsync(long employeeId) { var now = DateTime.Now; var today = now.Date; var record = await _attRepo.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.WorkDate == today); // 签到 if (record == null) { var status = now.TimeOfDay <= WorkStart.Add(TimeSpan.FromMinutes(10)) ? "正常" : "迟到"; var newRecord = new AttendanceRecord { EmployeeId = employeeId, WorkDate = today, SignInTime = now, SignInStatus = status, SignOutStatus = "未签退" }; await _attRepo.AddAsync(newRecord); await _attRepo.SaveChangesAsync(); return new PunchResultDto { Type = "signIn", Time = now, Status = status, Message = status == "正常" ? "签到成功,辛苦了!" : "签到成功(已迟到)" }; } // 已签到,进行签退 if (record.SignOutTime == null) { var status = now.TimeOfDay >= WorkEnd ? "正常" : "早退"; record.SignOutTime = now; record.SignOutStatus = status; await _attRepo.UpdateAsync(record); await _attRepo.SaveChangesAsync(); return new PunchResultDto { Type = "signOut", Time = now, Status = status, Message = status == "正常" ? "签退成功,明天见!" : "签退成功(早退)" }; } // 已签退 return new PunchResultDto { Type = "done", Time = now, Status = "已完成", Message = "今日已签到和签退,无需重复打卡" }; } public async Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize) { var records = (await _attRepo.FindAsync(a => a.EmployeeId == employeeId)) .OrderByDescending(a => a.WorkDate) .ToList(); var total = records.Count; var items = records.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); var emp = await _empRepo.GetByIdAsync(employeeId); var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; return new PageResult { Items = items.Select(a => new AttendanceDto { Id = a.Id, EmployeeId = a.EmployeeId, EmployeeName = emp?.UserName ?? "", DepartmentName = dept?.Name ?? "", WorkDate = a.WorkDate, SignInTime = a.SignInTime, SignOutTime = a.SignOutTime, SignInStatus = a.SignInStatus, SignOutStatus = a.SignOutStatus, Remark = a.Remark }).ToList(), Total = total, PageIndex = pageIndex, PageSize = pageSize }; } public async Task GetMySummaryAsync(long employeeId, string month) { // month 格式 yyyy-MM,默认当前月 if (string.IsNullOrWhiteSpace(month)) { month = DateTime.Now.ToString("yyyy-MM"); } var start = DateTime.Parse(month + "-01"); var end = start.AddMonths(1); var records = await _attRepo.FindAsync(a => a.EmployeeId == employeeId && a.WorkDate >= start && a.WorkDate < end); var summary = new AttendanceSummaryDto(); foreach (var r in records) { summary.TotalDays++; if (r.SignInStatus == "迟到") summary.LateDays++; if (r.SignOutStatus == "早退") summary.EarlyLeaveDays++; if (r.SignInTime == null) summary.AbsentDays++; } summary.NormalDays = summary.TotalDays - summary.LateDays - summary.EarlyLeaveDays - summary.AbsentDays; // 请假天数 var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId && l.Status == "已批准" && l.StartTime < end && l.EndTime >= start); summary.LeaveDays = leaves.Sum(l => (l.EndTime - l.StartTime).Days + 1); return summary; } public async Task> GetAllRecordsAsync(AttendanceQueryDto query) { var records = await _attRepo.GetAllAsync(); var filtered = records.AsQueryable(); if (!string.IsNullOrWhiteSpace(query.Date)) { var date = DateTime.Parse(query.Date).Date; filtered = filtered.Where(a => a.WorkDate == date); } var emps = await _empRepo.GetAllAsync(); var depts = await _deptRepo.GetAllAsync(); var empDict = emps.ToDictionary(e => e.Id, e => e); var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); if (query.DepartmentId.HasValue) { var deptEmpIds = emps.Where(e => e.DepartmentId == query.DepartmentId.Value).Select(e => e.Id).ToHashSet(); filtered = filtered.Where(a => deptEmpIds.Contains(a.EmployeeId)); } if (!string.IsNullOrWhiteSpace(query.Keyword)) { var kwEmpIds = emps.Where(e => e.UserName.Contains(query.Keyword) || e.LoginName.Contains(query.Keyword)) .Select(e => e.Id).ToHashSet(); filtered = filtered.Where(a => kwEmpIds.Contains(a.EmployeeId)); } var total = filtered.LongCount(); var items = filtered.OrderByDescending(a => a.WorkDate).ThenByDescending(a => a.SignInTime) .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList(); return new PageResult { Items = items.Select(a => { var emp = empDict.GetValueOrDefault(a.EmployeeId); return new AttendanceDto { Id = a.Id, EmployeeId = a.EmployeeId, EmployeeName = emp?.UserName ?? "", DepartmentName = emp != null ? deptDict.GetValueOrDefault(emp.DepartmentId, "") : "", WorkDate = a.WorkDate, SignInTime = a.SignInTime, SignOutTime = a.SignOutTime, SignInStatus = a.SignInStatus, SignOutStatus = a.SignOutStatus, Remark = a.Remark }; }).ToList(), Total = total, PageIndex = query.PageIndex, PageSize = query.PageSize }; } public async Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto) { if (dto.EndTime < dto.StartTime) throw new ServiceException("结束时间不能早于开始时间"); // 确定审批人:优先使用前端选择的审批人,为空则回退到管理员 var approverIds = (dto.ApproverIds ?? new List()).Where(id => id > 0).Distinct().ToList(); if (approverIds.Count == 0) approverIds = await GetApproverIdsAsync(); if (approverIds.Count == 0) throw new ServiceException("没有可用的审批人,请联系管理员"); var firstApprover = await _empRepo.GetByIdAsync(approverIds[0]); var leave = new LeaveRequest { EmployeeId = employeeId, Type = dto.Type, StartTime = dto.StartTime, EndTime = dto.EndTime, Reason = dto.Reason ?? "", CurrentOrderNo = 1, ApproverId = approverIds[0], ApproverName = firstApprover?.UserName ?? "" }; await _leaveRepo.AddAsync(leave); await _leaveRepo.SaveChangesAsync(); // 创建审批节点(序号 = 审批顺序) for (var i = 0; i < approverIds.Count; i++) { var emp = await _empRepo.GetByIdAsync(approverIds[i]); await _approverRepo.AddAsync(new LeaveApprover { LeaveId = leave.Id, ApproverId = approverIds[i], ApproverName = emp?.UserName ?? "", OrderNo = i + 1 }); } await _approverRepo.SaveChangesAsync(); // 只给第一个审批人发待办(顺序审批,后续审批人待前一节点通过后才收到) await SendLeaveApprovalAsync(leave, approverIds[0], 1, approverIds.Count); // 业务单据已创建,推送事件让各端刷新(多端同步) await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leave.Id.ToString(), "Created")); return await ToLeaveDto(leave); } public async Task> GetMyLeavesAsync(long employeeId) { var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId); var result = new List(); foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) { result.Add(await ToLeaveDto(l)); } return result; } public async Task> GetAllLeavesAsync(string status) { var leaves = await _leaveRepo.GetAllAsync(); if (!string.IsNullOrWhiteSpace(status) && status != "全部") { leaves = leaves.Where(l => l.Status == status).ToList(); } var result = new List(); foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) { result.Add(await ToLeaveDto(l)); } return result; } public async Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto) { var leave = await _leaveRepo.GetByIdAsync(leaveId); if (leave == null) throw new ServiceException("请假申请不存在", 404); if (leave.Status != "待审批") throw new ServiceException("该申请已处理"); var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == leaveId)) .OrderBy(a => a.OrderNo).ToList(); // 校验当前审批节点 var current = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo); if (current == null) throw new ServiceException("审批节点不存在", 404); if (current.ApproverId != approverId) throw new ServiceException($"当前应由「{current.ApproverName}」审批,您无权操作", 403); if (current.Status != "待审批") throw new ServiceException("该节点已处理"); // 更新当前节点 var handleResult = dto.Approved ? "已批准" : "已驳回"; current.Status = handleResult; current.ApproveRemark = dto.Remark ?? ""; current.ApprovedAt = DateTime.Now; await _approverRepo.UpdateAsync(current); if (!dto.Approved) { // 驳回:整体驳回,结束流程 leave.Status = "已驳回"; leave.ApproverId = approverId; leave.ApproverName = approverName; leave.ApproveRemark = dto.Remark ?? ""; leave.ApprovedAt = DateTime.Now; await _leaveRepo.UpdateAsync(leave); await _leaveRepo.SaveChangesAsync(); await _approverRepo.SaveChangesAsync(); // 流程终结:完成该单所有待办 + 标记消息已读,避免残留未读红点 await FinalizeLeaveApprovalAsync(leaveId, handleResult); await NotifyLeaveResultAsync(leave, false, dto.Remark); await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); return await ToLeaveDto(leave); } // 批准:判断是否还有下一级审批人 var next = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo + 1); if (next != null) { // 流转到下一级审批人 leave.CurrentOrderNo = next.OrderNo; leave.ApproverId = next.ApproverId; leave.ApproverName = next.ApproverName; await _leaveRepo.UpdateAsync(leave); await _leaveRepo.SaveChangesAsync(); await _approverRepo.SaveChangesAsync(); // 完成当前审批人待办并标记其消息已读,再把同一条消息流转给下一审批人 await CompleteCurrentApproverAsync(leaveId, approverId, handleResult); await SendLeaveApprovalAsync(leave, next.ApproverId, next.OrderNo, approvers.Count); } else { // 最后一级审批人批准:整体批准 leave.Status = "已批准"; leave.ApproverId = approverId; leave.ApproverName = approverName; leave.ApproveRemark = dto.Remark ?? ""; leave.ApprovedAt = DateTime.Now; await _leaveRepo.UpdateAsync(leave); await _leaveRepo.SaveChangesAsync(); await _approverRepo.SaveChangesAsync(); // 流程终结 await FinalizeLeaveApprovalAsync(leaveId, handleResult); await NotifyLeaveResultAsync(leave, true, dto.Remark); } // 业务状态已变更,推送事件让各端刷新(多端同步) await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); return await ToLeaveDto(leave); } /// 查询审批人(admin 角色用户)ID 列表(未指定审批人时的兜底) private async Task> GetApproverIdsAsync() { var adminRole = await _roleRepo.FirstOrDefaultAsync(r => r.Code == "admin"); if (adminRole == null) return new List(); var relations = await _empRoleRepo.FindAsync(ur => ur.RoleId == adminRole.Id); return relations.Select(ur => ur.UserId).Distinct().ToList(); } /// /// 请假审批待办消息:一张请假单仅保留一条,随审批流转复用。 /// 首次创建,流转时更新内容与当前审批人并重置为待投递(投递服务幂等,只推新审批人)。 /// private async Task SendLeaveApprovalAsync(LeaveRequest leave, long targetApproverId, int currentLevel, int totalLevel) { var businessType = NotificationTypeEnum.LeaveApproval.ToString(); var businessId = leave.Id.ToString(); var content = $"类型:{leave.Type},时间:{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm},事由:{leave.Reason}(第 {currentLevel}/{totalLevel} 级审批)"; var msg = await _msgRepo.FirstOrDefaultAsync(m => m.BusinessType == businessType && m.BusinessId == businessId); if (msg == null) { var type = await _typeRepo.FirstOrDefaultAsync(t => t.Code == businessType); var applicant = await _empRepo.GetByIdAsync(leave.EmployeeId); msg = new NotificationMessage { NotificationTypeId = type?.Id ?? 0, MessageTypeCode = businessType, BusinessType = businessType, BusinessId = businessId, Title = $"{applicant?.UserName ?? "员工"} 提交了请假申请", Content = content, Priority = NotificationPriority.Important, Status = NotificationStatus.Ready, ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }), CreatorId = leave.EmployeeId, CreatorName = applicant?.UserName ?? "", Category = NotificationCategory.Todo, RoutePath = !string.IsNullOrWhiteSpace(type?.RouteTemplate) ? type.RouteTemplate.Replace("{businessId}", businessId) : $"/attendance/approve?leaveId={businessId}" }; await _msgRepo.AddAsync(msg); await _msgRepo.SaveChangesAsync(); } else { // 复用:更新内容与当前审批人,重置为待投递,投递服务幂等只会推送给新审批人 msg.Content = content; msg.ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }); msg.Status = NotificationStatus.Ready; await _msgRepo.UpdateAsync(msg); await _msgRepo.SaveChangesAsync(); } // 当前审批人的待办(幂等:已存在则跳过) var existTodo = await _todoRepo.FirstOrDefaultAsync(t => t.MessageId == msg.Id && t.AssigneeId == targetApproverId); if (existTodo == null) { await _todoRepo.AddAsync(new TodoItem { MessageId = msg.Id, AssigneeId = targetApproverId, TodoStatus = TodoItemStatus.Pending }); await _todoRepo.SaveChangesAsync(); } } /// 完成当前审批人的待办,并标记其消息已读(流转时调用,避免已处理人残留未读红点) private async Task CompleteCurrentApproverAsync(long leaveId, long approverId, string handleResult) { var msgs = await _msgRepo.FindAsync(m => m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); var msgIds = msgs.Select(m => m.Id).ToList(); if (msgIds.Count == 0) return; var todos = await _todoRepo.FindAsync(t => msgIds.Contains(t.MessageId) && t.AssigneeId == approverId && t.TodoStatus == TodoItemStatus.Pending); foreach (var t in todos) { t.TodoStatus = TodoItemStatus.Completed; t.CompletedTime = DateTime.Now; t.HandleResult = handleResult; await _todoRepo.UpdateAsync(t); } var receivers = await _receiverRepo.FindAsync(r => msgIds.Contains(r.MessageId) && r.UserId == approverId && r.ReadStatus == NotificationReadStatus.Unread); foreach (var r in receivers) { r.ReadStatus = NotificationReadStatus.Read; r.ReadTime = DateTime.Now; await _receiverRepo.UpdateAsync(r); } } /// 流程终结:完成该单所有待处理待办,并标记所有未读消息为已读 private async Task FinalizeLeaveApprovalAsync(long leaveId, string handleResult) { var msgs = await _msgRepo.FindAsync(m => m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); var msgIds = msgs.Select(m => m.Id).ToList(); if (msgIds.Count == 0) return; var todos = await _todoRepo.FindAsync(t => msgIds.Contains(t.MessageId) && t.TodoStatus == TodoItemStatus.Pending); foreach (var t in todos) { t.TodoStatus = TodoItemStatus.Completed; t.CompletedTime = DateTime.Now; t.HandleResult = handleResult; await _todoRepo.UpdateAsync(t); } var receivers = await _receiverRepo.FindAsync(r => msgIds.Contains(r.MessageId) && r.ReadStatus == NotificationReadStatus.Unread); foreach (var r in receivers) { r.ReadStatus = NotificationReadStatus.Read; r.ReadTime = DateTime.Now; await _receiverRepo.UpdateAsync(r); } } /// 通知申请人审批结果 private async Task NotifyLeaveResultAsync(LeaveRequest leave, bool approved, string remark) { await _mediator.Send(new SendNotificationCommand( NotificationTypeEnum.LeaveResult, BusinessId: leave.Id.ToString(), Title: approved ? "请假申请已批准" : "请假申请已驳回", Content: $"您的请假({leave.Type},{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm})已{(approved ? "批准" : "驳回")}。审批意见:{remark}", ForceReceivers: new List { leave.EmployeeId } )); } private async Task ToLeaveDto(LeaveRequest l) { var emp = await _empRepo.GetByIdAsync(l.EmployeeId); var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == l.Id)) .OrderBy(a => a.OrderNo) .Select(a => new LeaveApproverDto { Id = a.Id, ApproverId = a.ApproverId, ApproverName = a.ApproverName, OrderNo = a.OrderNo, Status = a.Status, ApproveRemark = a.ApproveRemark, ApprovedAt = a.ApprovedAt }) .ToList(); return new LeaveRequestDto { Id = l.Id, EmployeeId = l.EmployeeId, EmployeeName = emp?.UserName ?? "", DepartmentName = dept?.Name ?? "", Type = l.Type, StartTime = l.StartTime, EndTime = l.EndTime, Reason = l.Reason, Status = l.Status, CurrentOrderNo = l.CurrentOrderNo, ApproverName = l.ApproverName, ApproveRemark = l.ApproveRemark, ApprovedAt = l.ApprovedAt, CreatedAt = l.CreatedAt, Approvers = approvers }; } }