Files
OnboardXAdmin/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
T
2026-09-17 09:53:50 +08:00

297 lines
12 KiB
C#

namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职任务审核与退回业务能力。
/// </summary>
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;
}
/// <summary>
/// 审核通过入职任务,并在同一事务中创建正式员工档案。
/// </summary>
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<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id)
?? throw Oops.Bah("候选人资料不存在");
var hasUnapprovedMaterial = await _db.Queryable<ObTaskMaterial>()
.AnyAsync(item => item.TaskId == task.Id
&& item.ReviewStatus != TaskMaterialReviewStatus.Passed);
if (hasUnapprovedMaterial)
{
throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务");
}
// 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。
var hasExistingEmployee = await _db.Queryable<ObEmployee>()
.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<ObEmployee>();
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<ObDepartment>().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<ObOnboardingTask>()
.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<ObEmployee>()
.SetColumns(item => new ObEmployee
{
CreateUserId = task.CreateUserId,
CreateUser = task.CreateUser
})
.Where(item => item.Id == employee.Id)
.ExecuteCommandAsync();
var ownerPersisted = await _db.Queryable<ObEmployee>()
.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;
}
}
/// <summary>
/// 退回入职任务,保存审核意见并允许候选人补充资料。
/// </summary>
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<ObOnboardingTask>()
.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<ObOnboardingTask> GetAuditableTask(long taskId)
{
var task = await _db.Queryable<ObOnboardingTask>().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;
}
/// <summary>
/// 从任务签署材料参数中读取唯一的合同开始日期,作为正式员工入职日期。
/// </summary>
private async Task<DateTime> ResolveContractStartDate(long taskId)
{
var taskContractIds = await _db.Queryable<ObTaskContract>()
.Where(item => item.TaskId == taskId)
.Select(item => item.Id)
.ToListAsync();
if (taskContractIds.Count == 0)
throw Oops.Bah("入职任务没有签署材料,无法确定入职日期");
var startDateValues = await _db.Queryable<ObTaskContractVariable>()
.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<DateTime>();
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];
}
/// <summary>
/// 将登记表中的日期文本转换为正式员工档案日期。
/// </summary>
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();
}
}