first commit

This commit is contained in:
yanpeng200212
2026-10-08 10:43:59 +08:00
commit 8430ba0452
288 changed files with 29595 additions and 0 deletions
@@ -0,0 +1,573 @@
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<AttendanceRecord> _attRepo;
private readonly IRepository<LeaveRequest> _leaveRepo;
private readonly IRepository<LeaveApprover> _approverRepo;
private readonly IRepository<RBAC.User> _empRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<RBAC.Role> _roleRepo;
private readonly IRepository<RBAC.UserRole> _empRoleRepo;
private readonly IRepository<NotificationMessage> _msgRepo;
private readonly IRepository<NotificationReceiver> _receiverRepo;
private readonly IRepository<TodoItem> _todoRepo;
private readonly IRepository<NotificationType> _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<AttendanceRecord> attRepo,
IRepository<LeaveRequest> leaveRepo,
IRepository<LeaveApprover> approverRepo,
IRepository<RBAC.User> empRepo,
IRepository<SysDept.Department> deptRepo,
IRepository<RBAC.Role> roleRepo,
IRepository<RBAC.UserRole> empRoleRepo,
IRepository<NotificationMessage> msgRepo,
IRepository<NotificationReceiver> receiverRepo,
IRepository<TodoItem> todoRepo,
IRepository<NotificationType> 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<PunchResultDto> 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<PageResult<AttendanceDto>> 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<AttendanceDto>
{
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<AttendanceSummaryDto> 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<PageResult<AttendanceDto>> 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<AttendanceDto>
{
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<LeaveRequestDto> ApplyLeaveAsync(long employeeId, CreateLeaveDto dto)
{
if (dto.EndTime < dto.StartTime)
throw new ServiceException("结束时间不能早于开始时间");
// 确定审批人:优先使用前端选择的审批人,为空则回退到管理员
var approverIds = (dto.ApproverIds ?? new List<long>()).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<List<LeaveRequestDto>> GetMyLeavesAsync(long employeeId)
{
var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId);
var result = new List<LeaveRequestDto>();
foreach (var l in leaves.OrderByDescending(l => l.CreatedAt))
{
result.Add(await ToLeaveDto(l));
}
return result;
}
public async Task<List<LeaveRequestDto>> 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<LeaveRequestDto>();
foreach (var l in leaves.OrderByDescending(l => l.CreatedAt))
{
result.Add(await ToLeaveDto(l));
}
return result;
}
public async Task<LeaveRequestDto> 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);
}
/// <summary>查询审批人(admin 角色用户)ID 列表(未指定审批人时的兜底)</summary>
private async Task<List<long>> GetApproverIdsAsync()
{
var adminRole = await _roleRepo.FirstOrDefaultAsync(r => r.Code == "admin");
if (adminRole == null) return new List<long>();
var relations = await _empRoleRepo.FindAsync(ur => ur.RoleId == adminRole.Id);
return relations.Select(ur => ur.UserId).Distinct().ToList();
}
/// <summary>
/// 请假审批待办消息:一张请假单仅保留一条,随审批流转复用。
/// 首次创建,流转时更新内容与当前审批人并重置为待投递(投递服务幂等,只推新审批人)。
/// </summary>
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<long> { 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<long> { 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();
}
}
/// <summary>完成当前审批人的待办,并标记其消息已读(流转时调用,避免已处理人残留未读红点)</summary>
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);
}
}
/// <summary>流程终结:完成该单所有待处理待办,并标记所有未读消息为已读</summary>
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);
}
}
/// <summary>通知申请人审批结果</summary>
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<long> { leave.EmployeeId }
));
}
private async Task<LeaveRequestDto> 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
};
}
}