namespace SimpleAdmin.Application; /// /// 实现入职任务审核与退回业务能力。 /// public class OnboardingAuditService : IOnboardingAuditService { private readonly SqlSugarScope _db; private readonly IConfigService _config; private readonly IOnboardingContractSnapshotService _snapshotService; private readonly IOnboardingEmployeeContractSnapshotService _employeeContractSnapshotService; private readonly IOnboardingDataPermissionService _dataPermission; public OnboardingAuditService( IConfigService config, IOnboardingContractSnapshotService snapshotService, IOnboardingEmployeeContractSnapshotService employeeContractSnapshotService, IOnboardingDataPermissionService dataPermission) { _db = DbContext.DB; _config = config; _snapshotService = snapshotService; _employeeContractSnapshotService = employeeContractSnapshotService; _dataPermission = dataPermission; } /// /// 审核通过入职任务,并在同一事务中创建正式员工档案。 /// public async Task Approve(OnboardingAuditActionInput input) { await OnboardingConcurrency.RunTask(_db, input.TaskId, () => ApproveCore(input), protectIdentity: true); } private async Task ApproveCore(OnboardingAuditActionInput input) { var task = await GetAuditableTask(input.TaskId); var employeeInfo = await _db.Queryable() .FirstAsync(item => item.TaskId == task.Id) ?? throw Oops.Bah("候选人资料不存在"); var hasUnapprovedMaterial = await _db.Queryable() .AnyAsync(item => item.TaskId == task.Id && item.ReviewStatus != TaskMaterialReviewStatus.Passed); if (hasUnapprovedMaterial) { throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务"); } // 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。 var hasExistingEmployee = await _db.Queryable() .AnyAsync(item => item.IdCardNumber == employeeInfo.IdCardNumber || item.Phone == employeeInfo.Phone); if (hasExistingEmployee) { throw Oops.Bah("该身份证号或手机号已存在正式员工"); } var contractStartDate = await ResolveContractStartDate(task.Id); var employee = employeeInfo.Adapt(); employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); employee.CompanyId = task.CompanyId; employee.DepartmentId = task.DepartmentId; employee.PositionId = task.PositionId; employee.EmploymentType = string.IsNullOrWhiteSpace(task.EmploymentType) ? EmployeeEmploymentType.FullTime : task.EmploymentType; employee.RegularizationStatus = employee.EmploymentType == EmployeeEmploymentType.PartTime ? EmployeeRegularizationStatus.NotApplicable : EmployeeRegularizationStatus.Probation; employee.SocialInsuranceStatus = EmployeeSocialInsuranceStatus.Unknown; if (task.DepartmentId.HasValue) { var dept = await _db.Queryable().InSingleAsync(task.DepartmentId.Value); employee.DepartmentName = dept?.DepartmentName ?? employeeInfo.OnboardingDepartment; } else { employee.DepartmentName = employeeInfo.OnboardingDepartment; } employee.College = employeeInfo.GraduatedSchool; employee.BirthDate = ParseDate(employeeInfo.Birthday); employee.ContactAddress = employeeInfo.CurrentAddress; employee.EntryDate = contractStartDate; employee.ContractStartDate = contractStartDate; employee.Remark = employeeInfo.OtherRemarks; employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(employeeInfo); employee.Status = CommonStatusConst.ENABLE; employee.CreateTime = DateTime.Now; // 员工档案归属于原任务创建人,而不是执行审核的人。 employee.CreateUserId = task.CreateUserId; employee.CreateUser = task.CreateUser; employee.UpdateTime = null; employee.UpdateUserId = null; employee.UpdateUser = null; employee.IsDelete = false; // 员工档案、任务状态和审核记录必须同时成功,避免出现“已通过但没有员工档案”的中间状态。 await _db.Ado.BeginTranAsync(); try { var updatedRows = await _db.Updateable() .SetColumns(item => new ObOnboardingTask { EmployeeId = employee.Id, TaskStatus = OnboardingTaskStatus.Approved, AuditedAt = DateTime.Now }) .Where(item => item.Id == task.Id && item.TaskStatus == OnboardingTaskStatus.Reviewing && item.EmployeeId == null) .ExecuteCommandAsync(); if (updatedRows != 1) throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看"); await _snapshotService.Build(task.Id, employeeInfo); await _db.Insertable(employee).ExecuteCommandAsync(); // 全局插入审计会把 CreateUserId 设置为当前审核人。管理员代审核他人任务时, // 员工档案仍应归属于原任务创建人,否则普通人事将无法继续维护自己发起的员工。 if (task.CreateUserId.HasValue && task.CreateUserId != UserManager.UserId) { await _db.Updateable() .SetColumns(item => new ObEmployee { CreateUserId = task.CreateUserId, CreateUser = task.CreateUser }) .Where(item => item.Id == employee.Id) .ExecuteCommandAsync(); var ownerPersisted = await _db.Queryable() .AnyAsync(item => item.Id == employee.Id && item.CreateUserId == task.CreateUserId); if (!ownerPersisted) throw Oops.Bah("员工档案创建人归属保存失败,请稍后重试"); } await _employeeContractSnapshotService.Build(employee.Id, task.Id); task.EmployeeId = employee.Id; task.TaskStatus = OnboardingTaskStatus.Approved; task.AuditedAt = DateTime.Now; await WriteAudit(task, OnboardingAuditAction.Approve, input.AuditOpinion); await _db.Ado.CommitTranAsync(); } catch { await _db.Ado.RollbackTranAsync(); throw; } } /// /// 退回入职任务,保存审核意见并允许候选人补充资料。 /// public async Task Reject(OnboardingAuditActionInput input) { await OnboardingConcurrency.RunTask(_db, input.TaskId, () => RejectCore(input)); } private async Task RejectCore(OnboardingAuditActionInput input) { if (string.IsNullOrWhiteSpace(input.AuditOpinion)) { throw Oops.Bah("请填写退回原因"); } var task = await GetAuditableTask(input.TaskId); await _db.Ado.BeginTranAsync(); try { var auditedAt = DateTime.Now; var updatedRows = await _db.Updateable() .SetColumns(item => new ObOnboardingTask { TaskStatus = OnboardingTaskStatus.Rejected, AuditedAt = auditedAt, RejectReason = input.AuditOpinion }) .Where(item => item.Id == task.Id && item.TaskStatus == OnboardingTaskStatus.Reviewing) .ExecuteCommandAsync(); if (updatedRows != 1) throw Oops.Bah("该入职任务已被其他审核人处理,请刷新后查看"); task.TaskStatus = OnboardingTaskStatus.Rejected; task.AuditedAt = auditedAt; task.RejectReason = input.AuditOpinion; await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion); await _db.Ado.CommitTranAsync(); } catch { await _db.Ado.RollbackTranAsync(); throw; } } private async Task GetAuditableTask(long taskId) { var task = await _db.Queryable().InSingleAsync(taskId) ?? throw Oops.Bah("入职任务不存在"); if (task.TaskStatus != OnboardingTaskStatus.Reviewing) { throw Oops.Bah("当前任务不在审核中"); } await _dataPermission.EnsureCanManage( task.CreateUserId, OnboardingPermissionCodes.TaskManageAll, "入职任务"); // 人事管理员/超级管理员可处理全部任务;普通人事还必须属于配置的审核人。 if (await _dataPermission.CanManage(null, OnboardingPermissionCodes.TaskManageAll)) return task; var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds") ?? throw Oops.Bah("请配置入职审核人"); // 审核人由全局配置与公司专属配置合并得出,不依赖独立业务表。 var reviewerIds = OnboardingReviewerConfigParser.Parse(config.ConfigValue, task.CompanyId); if (!reviewerIds.Contains(UserManager.UserId)) { throw Oops.Bah("您不是该入职任务的审核人"); } return task; } /// /// 从任务签署材料参数中读取唯一的合同开始日期,作为正式员工入职日期。 /// private async Task ResolveContractStartDate(long taskId) { var taskContractIds = await _db.Queryable() .Where(item => item.TaskId == taskId) .Select(item => item.Id) .ToListAsync(); if (taskContractIds.Count == 0) throw Oops.Bah("入职任务没有签署材料,无法确定入职日期"); var startDateValues = await _db.Queryable() .Where(item => taskContractIds.Contains(item.TaskContractId) && item.VariableCode == "hr.ContractStartDate") .Select(item => item.VariableValue) .ToListAsync(); var normalizedValues = startDateValues .Where(item => !string.IsNullOrWhiteSpace(item)) .Select(item => item.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (normalizedValues.Count == 0) throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成员工档案"); var parsedDates = new List(); foreach (var value in normalizedValues) { if (!DateTime.TryParse(value, out var parsedDate)) throw Oops.Bah("签署材料参数“合同开始日期”格式不正确"); parsedDates.Add(parsedDate.Date); } var distinctDates = parsedDates.Distinct().ToList(); if (distinctDates.Count > 1) throw Oops.Bah("多个签署材料的合同开始日期不一致,请统一后再审核通过"); return distinctDates[0]; } /// /// 将登记表中的日期文本转换为正式员工档案日期。 /// private static DateTime? ParseDate(string value) { return DateTime.TryParse(value, out var result) ? result.Date : null; } private Task WriteAudit(ObOnboardingTask task, string action, string opinion) { return _db.Insertable(new ObTaskAudit { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = task.Id, AuditRound = task.SubmitCount, AuditAction = action, AuditorUserId = UserManager.UserId, AuditOpinion = opinion, AuditedAt = DateTime.Now }).ExecuteCommandAsync(); } }