This commit is contained in:
fuenmao
2026-08-28 17:33:30 +08:00
parent 58804be611
commit f5b40e5659
10 changed files with 537 additions and 5 deletions
@@ -37,6 +37,12 @@ public class ObEmployee : BaseEntity
/// </summary>
public long CompanyId { get; set; }
/// <summary>
/// 所属公司名称,仅用于接口展示,不映射数据库字段。
/// </summary>
[SugarColumn(IsIgnore = true)]
public string CompanyName { get; set; }
/// <summary>
/// 所属入职业务部门 Id。
/// </summary>
@@ -53,6 +59,12 @@ public class ObEmployee : BaseEntity
/// </summary>
public long PositionId { get; set; }
/// <summary>
/// 所属岗位名称,仅用于接口展示,不映射数据库字段。
/// </summary>
[SugarColumn(IsIgnore = true)]
public string PositionName { get; set; }
/// <summary>
/// 性别。
/// </summary>
@@ -44,6 +44,8 @@ public class OnboardingAuditService : IOnboardingAuditService
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;
@@ -61,7 +63,8 @@ public class OnboardingAuditService : IOnboardingAuditService
employee.College = employeeInfo.GraduatedSchool;
employee.BirthDate = ParseDate(employeeInfo.Birthday);
employee.ContactAddress = employeeInfo.CurrentAddress;
employee.EntryDate = ParseDate(employeeInfo.OnboardingDate);
employee.EntryDate = contractStartDate;
employee.ContractStartDate = contractStartDate;
employee.Remark = employeeInfo.OtherRemarks;
employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(employeeInfo);
employee.Status = CommonStatusConst.ENABLE;
@@ -170,6 +173,48 @@ public class OnboardingAuditService : IOnboardingAuditService
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>
@@ -41,6 +41,11 @@ public class OnboardingEmployeeDetailOutput
/// </summary>
public ObEmployee Employee { get; set; }
/// <summary>
/// 审核通过时对应的完整入职登记表。
/// </summary>
public ObTaskEmployeeInfo EmployeeInfo { get; set; }
/// <summary>
/// 入职任务主数据。
/// </summary>
@@ -15,9 +15,9 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
/// <summary>
/// 分页查询审核通过后生成的正式员工档案。
/// </summary>
public Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input)
public async Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input)
{
return _db.Queryable<ObEmployee>()
var result = await _db.Queryable<ObEmployee>()
.WhereIF(
!string.IsNullOrWhiteSpace(input.EmployeeName),
item => item.EmployeeName.Contains(input.EmployeeName)
@@ -28,6 +28,11 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
.WhereIF(input.PositionId.HasValue, item => item.PositionId == input.PositionId.Value)
.OrderByDescending(item => item.CreateTime)
.ToPagedListAsync(input.PageNum, input.PageSize);
if (result.List != null && result.List.Any())
await FillEmployeeMetadata(result.List);
return result;
}
/// <summary>
@@ -38,6 +43,25 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id)
?? throw Oops.Bah("正式员工不存在");
var task = await _db.Queryable<ObOnboardingTask>().FirstAsync(x => x.EmployeeId == employee.Id);
await FillEmployeeMetadata(new[] { employee });
var employeeInfo = task == null
? null
: await _db.Queryable<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id);
if (employeeInfo == null && !string.IsNullOrWhiteSpace(employee.ExtraInfoJson))
{
try
{
employeeInfo = global::System.Text.Json.JsonSerializer.Deserialize<ObTaskEmployeeInfo>(
employee.ExtraInfoJson);
}
catch (global::System.Text.Json.JsonException)
{
// 兼容历史异常快照:员工主档仍可正常查看,其余信息显示为空。
}
}
var contracts = task == null
? new List<ObTaskContract>()
: await _db.Queryable<ObTaskContract>()
@@ -54,8 +78,121 @@ public class OnboardingEmployeeService : IOnboardingEmployeeService
{
Employee = employee,
Task = task,
EmployeeInfo = employeeInfo,
Contracts = contracts,
Audits = audits
};
}
/// <summary>
/// 批量补充员工所属公司、部门和岗位名称,避免列表逐行查询数据库。
/// </summary>
private async Task FillEmployeeMetadata(IEnumerable<ObEmployee> employees)
{
var employeeList = employees.ToList();
if (employeeList.Count == 0)
return;
var companyIds = employeeList.Select(item => item.CompanyId).Distinct().ToList();
var departmentIds = employeeList
.Where(item => item.DepartmentId.HasValue)
.Select(item => item.DepartmentId.Value)
.Distinct()
.ToList();
var positionIds = employeeList.Select(item => item.PositionId).Distinct().ToList();
var companies = await _db.Queryable<ObCompany>()
.Where(item => companyIds.Contains(item.Id))
.ToListAsync();
var departments = departmentIds.Count == 0
? new List<ObDepartment>()
: await _db.Queryable<ObDepartment>()
.Where(item => departmentIds.Contains(item.Id))
.ToListAsync();
var positions = await _db.Queryable<ObPosition>()
.Where(item => positionIds.Contains(item.Id))
.ToListAsync();
var companyMap = companies.ToDictionary(item => item.Id, item => item.CompanyName);
var departmentMap = departments.ToDictionary(item => item.Id, item => item.DepartmentName);
var positionMap = positions.ToDictionary(item => item.Id, item => item.PositionName);
foreach (var employee in employeeList)
{
if (companyMap.TryGetValue(employee.CompanyId, out var companyName))
employee.CompanyName = companyName;
if (employee.DepartmentId.HasValue
&& departmentMap.TryGetValue(employee.DepartmentId.Value, out var departmentName))
{
employee.DepartmentName = departmentName;
}
if (positionMap.TryGetValue(employee.PositionId, out var positionName))
employee.PositionName = positionName;
}
await FillMissingEntryDates(employeeList);
}
/// <summary>
/// 兼容已建档但入职日期为空的历史员工,从来源任务的合同开始日期进行只读回填。
/// 新审核通过的员工会直接持久化该日期,不会进入此兼容分支。
/// </summary>
private async Task FillMissingEntryDates(List<ObEmployee> employees)
{
var missingEmployees = employees
.Where(item => !item.EntryDate.HasValue || !item.ContractStartDate.HasValue)
.ToList();
if (missingEmployees.Count == 0)
return;
var employeeIds = missingEmployees.Select(item => item.Id).ToList();
var tasks = await _db.Queryable<ObOnboardingTask>()
.Where(item => item.EmployeeId.HasValue
&& employeeIds.Contains(item.EmployeeId.Value))
.ToListAsync();
if (tasks.Count == 0)
return;
var taskIds = tasks.Select(item => item.Id).ToList();
var contracts = await _db.Queryable<ObTaskContract>()
.Where(item => taskIds.Contains(item.TaskId))
.ToListAsync();
if (contracts.Count == 0)
return;
var contractIds = contracts.Select(item => item.Id).ToList();
var startDateVariables = await _db.Queryable<ObTaskContractVariable>()
.Where(item => contractIds.Contains(item.TaskContractId)
&& item.VariableCode == "hr.ContractStartDate")
.ToListAsync();
var employeeIdByTaskId = tasks
.Where(item => item.EmployeeId.HasValue)
.ToDictionary(item => item.Id, item => item.EmployeeId.Value);
var taskIdByContractId = contracts.ToDictionary(item => item.Id, item => item.TaskId);
var datesByEmployeeId = startDateVariables
.Where(item => !string.IsNullOrWhiteSpace(item.VariableValue)
&& taskIdByContractId.ContainsKey(item.TaskContractId))
.Select(item => new
{
EmployeeId = employeeIdByTaskId[taskIdByContractId[item.TaskContractId]],
EntryDate = DateTime.TryParse(item.VariableValue, out var value)
? value.Date
: (DateTime?)null
})
.Where(item => item.EntryDate.HasValue)
.GroupBy(item => item.EmployeeId)
.ToDictionary(
group => group.Key,
group => group.Select(item => item.EntryDate.Value).Distinct().ToList());
foreach (var employee in missingEmployees)
{
if (!datesByEmployeeId.TryGetValue(employee.Id, out var dates) || dates.Count != 1)
continue;
employee.EntryDate ??= dates[0];
employee.ContractStartDate ??= dates[0];
}
}
}
@@ -0,0 +1,15 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 定义候选人身份重复校验能力。
/// </summary>
public interface IOnboardingIdentityValidationService : ITransient
{
/// <summary>
/// 校验姓名和身份证号没有出现在其他有效入职流程或正式员工档案中。
/// </summary>
Task EnsureNoDuplicate(
long currentTaskId,
string employeeName,
string idCardNumber);
}
@@ -0,0 +1,50 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 统一校验候选人身份,避免同一人员通过多个链接重复提交入职资料。
/// </summary>
public class OnboardingIdentityValidationService : IOnboardingIdentityValidationService
{
private readonly SqlSugarScope _db;
public OnboardingIdentityValidationService()
{
_db = DbContext.DB;
}
/// <inheritdoc />
public async Task EnsureNoDuplicate(
long currentTaskId,
string employeeName,
string idCardNumber)
{
employeeName = employeeName?.Trim();
idCardNumber = idCardNumber?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(employeeName) || string.IsNullOrWhiteSpace(idCardNumber))
return;
var formalEmployeeExists = await _db.Queryable<ObEmployee>()
.AnyAsync(item => item.EmployeeName == employeeName
&& item.IdCardNumber == idCardNumber);
if (formalEmployeeExists)
{
throw Oops.Bah("该姓名和身份证号已完成正式员工建档,请勿重复提交入职资料");
}
// 待填写任务不占用身份,以实际先提交的任务为准;撤销或过期任务允许重新发起。
var duplicateTaskExists = await _db.Queryable<ObTaskEmployeeInfo>()
.InnerJoin<ObOnboardingTask>((info, task) => info.TaskId == task.Id)
.Where((info, task) => info.TaskId != currentTaskId
&& info.EmployeeName == employeeName
&& info.IdCardNumber == idCardNumber
&& (task.TaskStatus == OnboardingTaskStatus.Reviewing
|| task.TaskStatus == OnboardingTaskStatus.Rejected
|| task.TaskStatus == OnboardingTaskStatus.Approved
|| task.TaskStatus == OnboardingTaskStatus.Archived))
.AnyAsync();
if (!duplicateTaskExists)
return;
throw Oops.Bah("该姓名和身份证号已存在正在办理的入职任务,请勿重复提交;如需补充材料,请联系 HR 继续使用原任务链接");
}
}
@@ -7,11 +7,15 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
{
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
private readonly IOnboardingIdentityValidationService _identityValidationService;
public OnboardingPublicSubmissionService(IOnboardingContractSnapshotService snapshotService)
public OnboardingPublicSubmissionService(
IOnboardingContractSnapshotService snapshotService,
IOnboardingIdentityValidationService identityValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
_identityValidationService = identityValidationService;
}
/// <summary>
@@ -125,6 +129,11 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
if (basicInfoIncomplete)
throw Oops.Bah("请先完整填写基本信息");
await _identityValidationService.EnsureNoDuplicate(
task.Id,
info.EmployeeName,
info.IdCardNumber);
await _snapshotService.Build(task.Id, info);
var requiredMaterials = (await GetMaterialRequirements(task.Id))
@@ -8,14 +8,17 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
private readonly IOnboardingContractDocumentService _contractDocumentService;
private readonly IOnboardingIdentityValidationService _identityValidationService;
public OnboardingPublicTaskService(
IOnboardingContractSnapshotService snapshotService,
IOnboardingContractDocumentService contractDocumentService)
IOnboardingContractDocumentService contractDocumentService,
IOnboardingIdentityValidationService identityValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
_contractDocumentService = contractDocumentService;
_identityValidationService = identityValidationService;
}
/// <summary>
@@ -129,6 +132,10 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
}
ValidateEmployeeInfo(input);
await _identityValidationService.EnsureNoDuplicate(
task.Id,
input.EmployeeName,
input.IdCardNumber);
var old = await _db.Queryable<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id);