1251 lines
50 KiB
C#
1251 lines
50 KiB
C#
using OfficeOpenXml;
|
||
using OfficeOpenXml.Style;
|
||
using SixLabors.ImageSharp;
|
||
using System.Text.Json;
|
||
|
||
namespace SimpleAdmin.Application;
|
||
|
||
/// <summary>
|
||
/// 实现员工档案查询、人事状态维护和明细导出能力。
|
||
/// </summary>
|
||
public class OnboardingEmployeeService : IOnboardingEmployeeService
|
||
{
|
||
private readonly SqlSugarScope _db;
|
||
|
||
public OnboardingEmployeeService()
|
||
{
|
||
_db = DbContext.DB;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询审核通过后生成的员工档案。
|
||
/// </summary>
|
||
public async Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input)
|
||
{
|
||
var result = await BuildEmployeeQuery(input)
|
||
.OrderByDescending(item => item.CreateTime)
|
||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||
|
||
if (result.List != null && result.List.Any())
|
||
await FillEmployeeMetadata(result.List);
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<OnboardingEmployeeExportFile> Export(OnboardingEmployeePageInput input)
|
||
{
|
||
input ??= new OnboardingEmployeePageInput();
|
||
var employees = await BuildEmployeeQuery(input)
|
||
.OrderBy(item => item.CompanyId)
|
||
.OrderBy(item => item.DepartmentId)
|
||
.OrderBy(item => item.PositionId)
|
||
.OrderBy(item => item.EmployeeName)
|
||
.ToListAsync();
|
||
if (employees.Count == 0)
|
||
throw Oops.Bah("当前筛选条件没有可导出的员工");
|
||
|
||
await FillEmployeeMetadata(employees);
|
||
var employeeIds = employees.Select(item => item.Id).ToList();
|
||
var tasks = await _db.Queryable<ObOnboardingTask>()
|
||
.Where(item => item.EmployeeId.HasValue
|
||
&& employeeIds.Contains(item.EmployeeId.Value))
|
||
.OrderByDescending(item => item.CreateTime)
|
||
.ToListAsync();
|
||
var taskIds = tasks.Select(item => item.Id).Distinct().ToList();
|
||
var employeeInfos = taskIds.Count == 0
|
||
? new List<ObTaskEmployeeInfo>()
|
||
: await _db.Queryable<ObTaskEmployeeInfo>()
|
||
.Where(item => taskIds.Contains(item.TaskId))
|
||
.ToListAsync();
|
||
var snapshots = await _db.Queryable<ObEmployeeContractSnapshot>()
|
||
.Where(item => employeeIds.Contains(item.EmployeeId))
|
||
.OrderBy(item => item.EmployeeId)
|
||
.OrderBy(item => item.SnapshotAt)
|
||
.OrderBy(item => item.Id)
|
||
.ToListAsync();
|
||
|
||
var taskByEmployeeId = tasks
|
||
.Where(item => item.EmployeeId.HasValue)
|
||
.GroupBy(item => item.EmployeeId.Value)
|
||
.ToDictionary(group => group.Key, group => group.First());
|
||
var employeeInfoByTaskId = employeeInfos
|
||
.GroupBy(item => item.TaskId)
|
||
.ToDictionary(group => group.Key, group => group.First());
|
||
var snapshotsByEmployeeId = snapshots
|
||
.GroupBy(item => item.EmployeeId)
|
||
.ToDictionary(group => group.Key, group => group.ToList());
|
||
|
||
var employeeRows = employees.Select(employee =>
|
||
{
|
||
var info = DeserializeEmployeeInfo(employee.ExtraInfoJson);
|
||
if (info == null
|
||
&& taskByEmployeeId.TryGetValue(employee.Id, out var task)
|
||
&& employeeInfoByTaskId.TryGetValue(task.Id, out var taskInfo))
|
||
{
|
||
info = taskInfo;
|
||
}
|
||
|
||
var employeeSnapshots = snapshotsByEmployeeId.GetValueOrDefault(employee.Id)
|
||
?? new List<ObEmployeeContractSnapshot>();
|
||
return new EmployeeExportContext
|
||
{
|
||
Employee = employee,
|
||
EmployeeInfo = info,
|
||
PrimaryContract = SelectPrimaryContract(employeeSnapshots)
|
||
};
|
||
}).ToList();
|
||
|
||
using var package = new ExcelPackage();
|
||
package.Workbook.Properties.Title = "员工明细";
|
||
package.Workbook.Properties.Subject = "员工基本、人事状态及主要合同信息";
|
||
package.Workbook.Properties.Created = DateTime.Now;
|
||
|
||
WriteEmployeeSheet(package.Workbook.Worksheets.Add("员工明细"), employeeRows);
|
||
|
||
return new OnboardingEmployeeExportFile
|
||
{
|
||
FileBytes = package.GetAsByteArray(),
|
||
FileName = $"员工明细_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询正式员工及其来源任务、合同和审核记录。
|
||
/// </summary>
|
||
public async Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input)
|
||
{
|
||
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>()
|
||
.Where(item => item.TaskId == task.Id)
|
||
.ToListAsync();
|
||
var contractSnapshots = await _db.Queryable<ObEmployeeContractSnapshot>()
|
||
.Where(item => item.EmployeeId == employee.Id)
|
||
.OrderBy(item => item.CreateTime)
|
||
.OrderBy(item => item.Id)
|
||
.ToListAsync();
|
||
var materials = await GetEmployeeMaterials(task?.Id);
|
||
var electronicSignProcess = task == null
|
||
? null
|
||
: await _db.Queryable<ObElectronicSignProcess>()
|
||
.Where(item => item.TaskId == task.Id && item.ArchiveFileId.HasValue)
|
||
.OrderByDescending(item => item.Id)
|
||
.FirstAsync();
|
||
var audits = task == null
|
||
? new List<ObTaskAudit>()
|
||
: await _db.Queryable<ObTaskAudit>()
|
||
.Where(item => item.TaskId == task.Id)
|
||
.OrderByDescending(item => item.AuditedAt)
|
||
.ToListAsync();
|
||
var changeRecords = await GetEmployeeChangeRecords(employee.Id);
|
||
|
||
return new OnboardingEmployeeDetailOutput
|
||
{
|
||
Employee = employee,
|
||
Task = task,
|
||
EmployeeInfo = employeeInfo,
|
||
Contracts = contracts,
|
||
ContractSnapshots = contractSnapshots,
|
||
Materials = materials,
|
||
ElectronicSignArchiveFileId = electronicSignProcess?.ArchiveFileId,
|
||
Audits = audits,
|
||
ChangeRecords = changeRecords
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task UpdateHrInfo(OnboardingEmployeeHrUpdateInput input)
|
||
{
|
||
ValidateEmploymentType(input.EmploymentType);
|
||
ValidateSocialInsuranceStatus(input.SocialInsuranceStatus);
|
||
|
||
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id)
|
||
?? throw Oops.Bah("员工档案不存在");
|
||
var before = CreateHrState(employee);
|
||
var regularizationStatus = employee.RegularizationStatus;
|
||
var regularizationDate = employee.RegularizationDate;
|
||
if (input.EmploymentType == EmployeeEmploymentType.PartTime)
|
||
{
|
||
regularizationStatus = EmployeeRegularizationStatus.NotApplicable;
|
||
regularizationDate = null;
|
||
}
|
||
else if (employee.EmploymentType == EmployeeEmploymentType.PartTime
|
||
|| string.IsNullOrWhiteSpace(regularizationStatus)
|
||
|| regularizationStatus == EmployeeRegularizationStatus.NotApplicable)
|
||
{
|
||
regularizationStatus = EmployeeRegularizationStatus.Probation;
|
||
regularizationDate = null;
|
||
}
|
||
|
||
var contractNature = NormalizeOptionalText(input.ContractNature);
|
||
var socialInsuranceCompany = NormalizeOptionalText(input.SocialInsuranceCompany);
|
||
var noChanges = employee.EmploymentType == input.EmploymentType
|
||
&& employee.RegularizationStatus == regularizationStatus
|
||
&& employee.RegularizationDate == regularizationDate
|
||
&& string.Equals(employee.ContractNature, contractNature, StringComparison.Ordinal)
|
||
&& employee.SocialInsuranceStatus == input.SocialInsuranceStatus
|
||
&& string.Equals(
|
||
employee.SocialInsuranceCompany,
|
||
socialInsuranceCompany,
|
||
StringComparison.Ordinal);
|
||
if (noChanges)
|
||
return;
|
||
|
||
var after = new
|
||
{
|
||
employmentType = input.EmploymentType,
|
||
regularizationStatus,
|
||
regularizationDate,
|
||
contractNature,
|
||
socialInsuranceStatus = input.SocialInsuranceStatus,
|
||
socialInsuranceCompany
|
||
};
|
||
|
||
await _db.Ado.BeginTranAsync();
|
||
try
|
||
{
|
||
await _db.Updateable<ObEmployee>()
|
||
.SetColumns(item => new ObEmployee
|
||
{
|
||
EmploymentType = input.EmploymentType,
|
||
RegularizationStatus = regularizationStatus,
|
||
RegularizationDate = regularizationDate,
|
||
ContractNature = contractNature,
|
||
SocialInsuranceStatus = input.SocialInsuranceStatus,
|
||
SocialInsuranceCompany = socialInsuranceCompany,
|
||
UpdateTime = DateTime.Now,
|
||
UpdateUserId = UserManager.UserId,
|
||
UpdateUser = UserManager.Name
|
||
})
|
||
.Where(item => item.Id == employee.Id)
|
||
.ExecuteCommandAsync();
|
||
await InsertChangeRecord(
|
||
employee.Id,
|
||
EmployeeChangeType.HrInfoUpdate,
|
||
null,
|
||
before,
|
||
after,
|
||
"编辑员工人事信息");
|
||
await _db.Ado.CommitTranAsync();
|
||
}
|
||
catch
|
||
{
|
||
await _db.Ado.RollbackTranAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task Regularize(OnboardingEmployeeRegularizeInput input)
|
||
{
|
||
if (!input.RegularizationDate.HasValue)
|
||
throw Oops.Bah("请选择转正日期");
|
||
|
||
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id)
|
||
?? throw Oops.Bah("员工档案不存在");
|
||
if (employee.EmploymentType == EmployeeEmploymentType.PartTime)
|
||
throw Oops.Bah("兼职员工不适用转正流程,请先调整用工类型");
|
||
if (employee.RegularizationStatus == EmployeeRegularizationStatus.Regular)
|
||
throw Oops.Bah("该员工已经转正,请勿重复办理");
|
||
|
||
var regularizationDate = input.RegularizationDate.Value.Date;
|
||
if (employee.EntryDate.HasValue && regularizationDate < employee.EntryDate.Value.Date)
|
||
throw Oops.Bah("转正日期不能早于入职日期");
|
||
|
||
var fileIds = (input.FileIds ?? new List<long>())
|
||
.Where(item => item > 0)
|
||
.Distinct()
|
||
.ToList();
|
||
var files = fileIds.Count == 0
|
||
? new List<SysFile>()
|
||
: await _db.Queryable<SysFile>()
|
||
.Where(item => fileIds.Contains(item.Id))
|
||
.ToListAsync();
|
||
if (files.Count != fileIds.Count)
|
||
throw Oops.Bah("部分转正材料不存在或已被删除,请重新上传");
|
||
|
||
var before = CreateHrState(employee);
|
||
var after = new
|
||
{
|
||
employee.EmploymentType,
|
||
regularizationStatus = EmployeeRegularizationStatus.Regular,
|
||
regularizationDate,
|
||
employee.ContractNature,
|
||
employee.SocialInsuranceStatus,
|
||
employee.SocialInsuranceCompany
|
||
};
|
||
|
||
await _db.Ado.BeginTranAsync();
|
||
try
|
||
{
|
||
var changedRows = await _db.Updateable<ObEmployee>()
|
||
.SetColumns(item => new ObEmployee
|
||
{
|
||
RegularizationStatus = EmployeeRegularizationStatus.Regular,
|
||
RegularizationDate = regularizationDate,
|
||
UpdateTime = DateTime.Now,
|
||
UpdateUserId = UserManager.UserId,
|
||
UpdateUser = UserManager.Name
|
||
})
|
||
.Where(item => item.Id == employee.Id
|
||
&& item.RegularizationStatus != EmployeeRegularizationStatus.Regular)
|
||
.ExecuteCommandAsync();
|
||
if (changedRows != 1)
|
||
throw Oops.Bah("该员工状态已发生变化,请刷新后重试");
|
||
|
||
var changeRecord = await InsertChangeRecord(
|
||
employee.Id,
|
||
EmployeeChangeType.Regularization,
|
||
regularizationDate,
|
||
before,
|
||
after,
|
||
NormalizeOptionalText(input.Remark));
|
||
if (files.Count > 0)
|
||
{
|
||
var now = DateTime.Now;
|
||
var attachments = files.Select(file => new ObEmployeeAttachment
|
||
{
|
||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||
EmployeeId = employee.Id,
|
||
ChangeRecordId = changeRecord.Id,
|
||
BusinessType = EmployeeAttachmentBusinessType.Regularization,
|
||
FileId = file.Id,
|
||
FileName = file.Name,
|
||
UploadedAt = now,
|
||
Status = CommonStatusConst.ENABLE,
|
||
CreateTime = now,
|
||
CreateUserId = UserManager.UserId,
|
||
CreateUser = UserManager.Name
|
||
}).ToList();
|
||
await _db.Insertable(attachments).ExecuteCommandAsync();
|
||
}
|
||
|
||
await _db.Ado.CommitTranAsync();
|
||
}
|
||
catch
|
||
{
|
||
await _db.Ado.RollbackTranAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task RevokeRegularization(OnboardingEmployeeRegularizationRevokeInput input)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.Remark))
|
||
throw Oops.Bah("请填写撤销原因");
|
||
|
||
var employee = await _db.Queryable<ObEmployee>().InSingleAsync(input.Id)
|
||
?? throw Oops.Bah("员工档案不存在");
|
||
if (employee.RegularizationStatus != EmployeeRegularizationStatus.Regular)
|
||
throw Oops.Bah("该员工当前不是已转正状态");
|
||
|
||
var before = CreateHrState(employee);
|
||
var after = new
|
||
{
|
||
employee.EmploymentType,
|
||
regularizationStatus = EmployeeRegularizationStatus.Probation,
|
||
regularizationDate = (DateTime?)null,
|
||
employee.ContractNature,
|
||
employee.SocialInsuranceStatus,
|
||
employee.SocialInsuranceCompany
|
||
};
|
||
|
||
await _db.Ado.BeginTranAsync();
|
||
try
|
||
{
|
||
var changedRows = await _db.Updateable<ObEmployee>()
|
||
.SetColumns(item => new ObEmployee
|
||
{
|
||
RegularizationStatus = EmployeeRegularizationStatus.Probation,
|
||
RegularizationDate = null,
|
||
UpdateTime = DateTime.Now,
|
||
UpdateUserId = UserManager.UserId,
|
||
UpdateUser = UserManager.Name
|
||
})
|
||
.Where(item => item.Id == employee.Id
|
||
&& item.RegularizationStatus == EmployeeRegularizationStatus.Regular)
|
||
.ExecuteCommandAsync();
|
||
if (changedRows != 1)
|
||
throw Oops.Bah("该员工状态已发生变化,请刷新后重试");
|
||
|
||
await InsertChangeRecord(
|
||
employee.Id,
|
||
EmployeeChangeType.RegularizationRevoke,
|
||
DateTime.Now.Date,
|
||
before,
|
||
after,
|
||
input.Remark.Trim());
|
||
await _db.Ado.CommitTranAsync();
|
||
}
|
||
catch
|
||
{
|
||
await _db.Ado.RollbackTranAsync();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询员工来源任务中上传的全部材料,并补充原始文件名、后缀和文件大小。
|
||
/// </summary>
|
||
private async Task<List<OnboardingTaskMaterialDetailOutput>> GetEmployeeMaterials(long? taskId)
|
||
{
|
||
if (!taskId.HasValue)
|
||
return new List<OnboardingTaskMaterialDetailOutput>();
|
||
|
||
var materials = await _db.Queryable<ObTaskMaterial>()
|
||
.Where(item => item.TaskId == taskId.Value)
|
||
.ToListAsync();
|
||
if (materials.Count == 0)
|
||
return new List<OnboardingTaskMaterialDetailOutput>();
|
||
|
||
var fileIds = materials
|
||
.Select(item => item.FileId)
|
||
.Distinct()
|
||
.ToList();
|
||
var files = await _db.Queryable<SysFile>()
|
||
.Where(item => fileIds.Contains(item.Id))
|
||
.ToListAsync();
|
||
var fileMap = files.ToDictionary(item => item.Id);
|
||
|
||
var requirements = await _db.Queryable<ObTaskMaterialRequirement>()
|
||
.Where(item => item.TaskId == taskId.Value)
|
||
.ToListAsync();
|
||
var sortMap = requirements
|
||
.GroupBy(item => item.MaterialTypeId)
|
||
.ToDictionary(group => group.Key, group => group.Min(item => item.SortCode));
|
||
|
||
return materials
|
||
.Select(material =>
|
||
{
|
||
var output = material.Adapt<OnboardingTaskMaterialDetailOutput>();
|
||
if (fileMap.TryGetValue(material.FileId, out var file))
|
||
{
|
||
output.FileName = file.Name;
|
||
output.FileSuffix = file.Suffix;
|
||
output.FileSizeInfo = file.SizeInfo;
|
||
}
|
||
|
||
return output;
|
||
})
|
||
.OrderBy(item => sortMap.GetValueOrDefault(item.MaterialTypeId, int.MaxValue))
|
||
.ThenBy(item => item.UploadedAt)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询员工人事变更记录,并补充每条记录关联材料的文件信息。
|
||
/// </summary>
|
||
private async Task<List<ObEmployeeChangeRecord>> GetEmployeeChangeRecords(long employeeId)
|
||
{
|
||
var records = await _db.Queryable<ObEmployeeChangeRecord>()
|
||
.Where(item => item.EmployeeId == employeeId)
|
||
.OrderByDescending(item => item.CreateTime)
|
||
.OrderByDescending(item => item.Id)
|
||
.ToListAsync();
|
||
if (records.Count == 0)
|
||
return records;
|
||
|
||
var recordIds = records.Select(item => item.Id).ToList();
|
||
var attachments = await _db.Queryable<ObEmployeeAttachment>()
|
||
.Where(item => item.EmployeeId == employeeId
|
||
&& item.ChangeRecordId.HasValue
|
||
&& recordIds.Contains(item.ChangeRecordId.Value))
|
||
.OrderBy(item => item.UploadedAt)
|
||
.ToListAsync();
|
||
if (attachments.Count > 0)
|
||
{
|
||
var fileIds = attachments.Select(item => item.FileId).Distinct().ToList();
|
||
var files = await _db.Queryable<SysFile>()
|
||
.Where(item => fileIds.Contains(item.Id))
|
||
.ToListAsync();
|
||
var fileMap = files.ToDictionary(item => item.Id);
|
||
foreach (var attachment in attachments)
|
||
{
|
||
if (!fileMap.TryGetValue(attachment.FileId, out var file))
|
||
continue;
|
||
attachment.FileName = string.IsNullOrWhiteSpace(attachment.FileName)
|
||
? file.Name
|
||
: attachment.FileName;
|
||
attachment.FileSuffix = file.Suffix;
|
||
attachment.FileSizeInfo = file.SizeInfo;
|
||
}
|
||
|
||
var attachmentMap = attachments
|
||
.Where(item => item.ChangeRecordId.HasValue)
|
||
.GroupBy(item => item.ChangeRecordId.Value)
|
||
.ToDictionary(group => group.Key, group => group.ToList());
|
||
foreach (var record in records)
|
||
record.Attachments = attachmentMap.GetValueOrDefault(record.Id) ?? new List<ObEmployeeAttachment>();
|
||
}
|
||
|
||
return records;
|
||
}
|
||
|
||
private async Task<ObEmployeeChangeRecord> InsertChangeRecord(
|
||
long employeeId,
|
||
string changeType,
|
||
DateTime? effectiveDate,
|
||
object before,
|
||
object after,
|
||
string remark)
|
||
{
|
||
var now = DateTime.Now;
|
||
var record = new ObEmployeeChangeRecord
|
||
{
|
||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||
EmployeeId = employeeId,
|
||
ChangeType = changeType,
|
||
EffectiveDate = effectiveDate,
|
||
BeforeJson = JsonSerializer.Serialize(before),
|
||
AfterJson = JsonSerializer.Serialize(after),
|
||
Remark = remark,
|
||
Status = CommonStatusConst.ENABLE,
|
||
CreateTime = now,
|
||
CreateUserId = UserManager.UserId,
|
||
CreateUser = UserManager.Name
|
||
};
|
||
await _db.Insertable(record).ExecuteCommandAsync();
|
||
return record;
|
||
}
|
||
|
||
private static object CreateHrState(ObEmployee employee)
|
||
{
|
||
return new
|
||
{
|
||
employee.EmploymentType,
|
||
employee.RegularizationStatus,
|
||
employee.RegularizationDate,
|
||
employee.ContractNature,
|
||
employee.SocialInsuranceStatus,
|
||
employee.SocialInsuranceCompany
|
||
};
|
||
}
|
||
|
||
private static string NormalizeOptionalText(string value)
|
||
{
|
||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||
}
|
||
|
||
private static void ValidateEmploymentType(string value)
|
||
{
|
||
if (value != EmployeeEmploymentType.FullTime
|
||
&& value != EmployeeEmploymentType.PartTime)
|
||
{
|
||
throw Oops.Bah("用工类型不正确");
|
||
}
|
||
}
|
||
|
||
private static void ValidateSocialInsuranceStatus(string value)
|
||
{
|
||
if (value != EmployeeSocialInsuranceStatus.Unknown
|
||
&& value != EmployeeSocialInsuranceStatus.NotPurchased
|
||
&& value != EmployeeSocialInsuranceStatus.Purchased
|
||
&& value != EmployeeSocialInsuranceStatus.Stopped
|
||
&& value != EmployeeSocialInsuranceStatus.NotApplicable)
|
||
{
|
||
throw Oops.Bah("社保状态不正确");
|
||
}
|
||
}
|
||
|
||
/// <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 ISugarQueryable<ObEmployee> BuildEmployeeQuery(OnboardingEmployeePageInput input)
|
||
{
|
||
input ??= new OnboardingEmployeePageInput();
|
||
return _db.Queryable<ObEmployee>()
|
||
.WhereIF(
|
||
!string.IsNullOrWhiteSpace(input.EmployeeName),
|
||
item => item.EmployeeName.Contains(input.EmployeeName)
|
||
|| item.IdCardNumber.Contains(input.EmployeeName))
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Phone), item => item.Phone.Contains(input.Phone))
|
||
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
|
||
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
|
||
.WhereIF(input.PositionId.HasValue, item => item.PositionId == input.PositionId.Value)
|
||
.WhereIF(
|
||
!string.IsNullOrWhiteSpace(input.EmploymentType),
|
||
item => item.EmploymentType == input.EmploymentType)
|
||
.WhereIF(
|
||
!string.IsNullOrWhiteSpace(input.RegularizationStatus),
|
||
item => item.RegularizationStatus == input.RegularizationStatus)
|
||
.WhereIF(
|
||
!string.IsNullOrWhiteSpace(input.SocialInsuranceStatus),
|
||
item => item.SocialInsuranceStatus == input.SocialInsuranceStatus);
|
||
}
|
||
|
||
private static void WriteEmployeeSheet(
|
||
ExcelWorksheet worksheet,
|
||
IReadOnlyList<EmployeeExportContext> rows)
|
||
{
|
||
ExportColumn<EmployeeExportContext> TextColumn(
|
||
string header,
|
||
Func<EmployeeExportContext, object> value,
|
||
double? width = null) => CreateTextColumn(header, value, width);
|
||
ExportColumn<EmployeeExportContext> DateColumn(
|
||
string header,
|
||
Func<EmployeeExportContext, object> value,
|
||
string format = "yyyy-mm-dd") => CreateDateColumn(header, value, format);
|
||
ExportColumn<EmployeeExportContext> NumberColumn(
|
||
string header,
|
||
Func<EmployeeExportContext, object> value,
|
||
string format = "#,##0.00") => CreateNumberColumn(header, value, format);
|
||
|
||
var columns = new List<ExportColumn<EmployeeExportContext>>
|
||
{
|
||
TextColumn("姓名", row => row.Employee.EmployeeName),
|
||
TextColumn("所属公司", row => row.Employee.CompanyName, 22),
|
||
TextColumn("部门", row => row.Employee.DepartmentName),
|
||
TextColumn("岗位", row => row.Employee.PositionName),
|
||
TextColumn("用工类型", row => EmploymentTypeText(row.Employee.EmploymentType)),
|
||
TextColumn("性别", row => row.Employee.Gender),
|
||
TextColumn("手机号", row => row.Employee.Phone),
|
||
TextColumn("身份证号码", row => row.Employee.IdCardNumber),
|
||
TextColumn("院校", row => FirstNotBlank(row.Employee.College, row.EmployeeInfo?.GraduatedSchool)),
|
||
TextColumn("学历", row => FirstNotBlank(row.Employee.Education, row.EmployeeInfo?.Education)),
|
||
TextColumn("专业", row => FirstNotBlank(row.Employee.Major, row.EmployeeInfo?.Major)),
|
||
DateColumn("出生日期", row => row.Employee.BirthDate ?? ParseDate(row.EmployeeInfo?.Birthday)),
|
||
NumberColumn("年龄", row => CalculateAge(row.Employee.BirthDate ?? ParseDate(row.EmployeeInfo?.Birthday)), "0"),
|
||
TextColumn("户籍所在地", row => FirstNotBlank(row.Employee.RegisteredResidence, row.EmployeeInfo?.RegisteredResidence)),
|
||
TextColumn("民族", row => FirstNotBlank(row.Employee.Nation, row.EmployeeInfo?.Nation)),
|
||
TextColumn("通讯地址", row => FirstNotBlank(row.Employee.ContactAddress, row.EmployeeInfo?.CurrentAddress), 24),
|
||
DateColumn("入职时间", row => row.Employee.EntryDate),
|
||
TextColumn("司龄", row => FormatTenure(row.Employee.EntryDate)),
|
||
DateColumn("合同开始日期", row => row.PrimaryContract?.ContractStartDate ?? row.Employee.ContractStartDate),
|
||
DateColumn("合同终止日期", row => row.PrimaryContract?.ContractEndDate ?? row.Employee.ContractEndDate),
|
||
TextColumn("试用期", row => row.PrimaryContract?.ProbationTerm),
|
||
NumberColumn("试用薪资", row => row.PrimaryContract?.ProbationSalary),
|
||
DateColumn("转正日期", row => row.Employee.RegularizationDate),
|
||
TextColumn("是否转正", row => RegularizationText(row.Employee)),
|
||
NumberColumn("转正薪资", row => row.PrimaryContract?.RegularSalary),
|
||
TextColumn("是否购买社保", row => SocialInsuranceText(row.Employee.SocialInsuranceStatus)),
|
||
TextColumn("合同社保单位", row => row.Employee.SocialInsuranceCompany, 22),
|
||
TextColumn("合同性质", row => row.Employee.ContractNature),
|
||
TextColumn("工资卡账号", row => row.Employee.BankCardNumber),
|
||
TextColumn("工资卡开户行", row => row.Employee.BankName, 22)
|
||
};
|
||
|
||
WriteWorksheet(worksheet, columns, rows);
|
||
}
|
||
|
||
private static void WriteContractSheet(
|
||
ExcelWorksheet worksheet,
|
||
IReadOnlyList<ContractExportContext> rows)
|
||
{
|
||
ExportColumn<ContractExportContext> TextColumn(
|
||
string header,
|
||
Func<ContractExportContext, object> value,
|
||
double? width = null) => CreateTextColumn(header, value, width);
|
||
ExportColumn<ContractExportContext> DateColumn(
|
||
string header,
|
||
Func<ContractExportContext, object> value,
|
||
string format = "yyyy-mm-dd") => CreateDateColumn(header, value, format);
|
||
ExportColumn<ContractExportContext> NumberColumn(
|
||
string header,
|
||
Func<ContractExportContext, object> value,
|
||
string format = "#,##0.00") => CreateNumberColumn(header, value, format);
|
||
|
||
var columns = new List<ExportColumn<ContractExportContext>>
|
||
{
|
||
TextColumn("姓名", row => row.Employee.EmployeeName),
|
||
TextColumn("员工编号", row => FirstNotBlank(row.Employee.EmployeeNo, row.Snapshot.EmployeeNo)),
|
||
TextColumn("所属公司", row => row.Employee.CompanyName, 22),
|
||
TextColumn("所属部门", row => row.Employee.DepartmentName),
|
||
TextColumn("所属岗位", row => row.Employee.PositionName),
|
||
TextColumn("手机号码", row => row.Employee.Phone),
|
||
TextColumn("身份证号码", row => row.Employee.IdCardNumber),
|
||
TextColumn("入职任务编号", row => row.TaskNo, 22),
|
||
TextColumn("合同名称", row => row.Snapshot.ContractName, 24),
|
||
TextColumn("合同类型", row => row.Snapshot.ContractType),
|
||
DateColumn("合同开始日期", row => row.Snapshot.ContractStartDate),
|
||
DateColumn("合同结束日期", row => row.Snapshot.ContractEndDate),
|
||
TextColumn("合同期限", row => row.Snapshot.ContractTerm),
|
||
DateColumn("试用期开始日期", row => row.Snapshot.ProbationStartDate),
|
||
DateColumn("试用期结束日期", row => row.Snapshot.ProbationEndDate),
|
||
TextColumn("试用期期限", row => row.Snapshot.ProbationTerm),
|
||
NumberColumn("试用薪资(元)", row => row.Snapshot.ProbationSalary),
|
||
NumberColumn("基本工资(元)", row => row.Snapshot.BaseSalary),
|
||
NumberColumn("岗位工资(元)", row => row.Snapshot.PositionSalary),
|
||
NumberColumn("绩效津贴(元)", row => row.Snapshot.PerformanceAllowance),
|
||
NumberColumn("全勤奖(元)", row => row.Snapshot.AttendanceBonus),
|
||
NumberColumn("转正后薪资(元)", row => row.Snapshot.RegularSalary),
|
||
TextColumn("工作地点", row => row.Snapshot.WorkCity),
|
||
TextColumn("合同岗位名称", row => row.Snapshot.PositionName),
|
||
DateColumn("快照生成时间", row => row.Snapshot.SnapshotAt, "yyyy-mm-dd hh:mm:ss")
|
||
};
|
||
|
||
var variableColumns = rows
|
||
.SelectMany(row => row.Variables.Values)
|
||
.GroupBy(item => item.Code, StringComparer.OrdinalIgnoreCase)
|
||
.Select(group => new
|
||
{
|
||
Code = group.Key,
|
||
Name = group.Select(item => item.Name).FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)),
|
||
SortCode = group.Min(item => item.SortCode)
|
||
})
|
||
.OrderBy(item => item.SortCode)
|
||
.ThenBy(item => item.Code, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
foreach (var variable in variableColumns)
|
||
{
|
||
var code = variable.Code;
|
||
var header = string.IsNullOrWhiteSpace(variable.Name)
|
||
? code
|
||
: $"{variable.Name}\n({code})";
|
||
columns.Add(TextColumn(
|
||
header,
|
||
row => row.Variables.TryGetValue(code, out var value) ? value.Value : null,
|
||
24));
|
||
}
|
||
|
||
WriteWorksheet(worksheet, columns, rows);
|
||
}
|
||
|
||
private static void WriteWorksheet<T>(
|
||
ExcelWorksheet worksheet,
|
||
IReadOnlyList<ExportColumn<T>> columns,
|
||
IReadOnlyList<T> rows)
|
||
{
|
||
worksheet.View.ShowGridLines = false;
|
||
worksheet.View.FreezePanes(2, 1);
|
||
|
||
for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
|
||
{
|
||
var column = columns[columnIndex];
|
||
var cell = worksheet.Cells[1, columnIndex + 1];
|
||
cell.Value = column.Header;
|
||
cell.Style.WrapText = true;
|
||
}
|
||
|
||
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
|
||
{
|
||
var excelRow = rowIndex + 2;
|
||
for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
|
||
{
|
||
var column = columns[columnIndex];
|
||
var cell = worksheet.Cells[excelRow, columnIndex + 1];
|
||
var value = column.Value(rows[rowIndex]);
|
||
if (value != null)
|
||
cell.Value = value;
|
||
if (!string.IsNullOrWhiteSpace(column.NumberFormat))
|
||
cell.Style.Numberformat.Format = column.NumberFormat;
|
||
cell.Style.HorizontalAlignment = column.Kind == ExportCellKind.Text
|
||
? ExcelHorizontalAlignment.Left
|
||
: ExcelHorizontalAlignment.Right;
|
||
}
|
||
|
||
if (rowIndex % 2 == 1)
|
||
{
|
||
worksheet.Cells[excelRow, 1, excelRow, columns.Count].Style.Fill.PatternType = ExcelFillStyle.Solid;
|
||
worksheet.Cells[excelRow, 1, excelRow, columns.Count].Style.Fill.BackgroundColor.SetColor(
|
||
Color.FromRgb(247, 249, 252));
|
||
}
|
||
}
|
||
|
||
var lastRow = Math.Max(rows.Count + 1, 1);
|
||
var allCells = worksheet.Cells[1, 1, lastRow, columns.Count];
|
||
allCells.Style.Font.Name = "Microsoft YaHei";
|
||
allCells.Style.Font.Size = 10;
|
||
allCells.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
|
||
|
||
var header = worksheet.Cells[1, 1, 1, columns.Count];
|
||
header.Style.Fill.PatternType = ExcelFillStyle.Solid;
|
||
header.Style.Fill.BackgroundColor.SetColor(Color.FromRgb(31, 78, 121));
|
||
header.Style.Font.Color.SetColor(Color.White);
|
||
header.Style.Font.Bold = true;
|
||
header.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
|
||
header.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
|
||
header.AutoFilter = true;
|
||
worksheet.Row(1).Height = 34;
|
||
|
||
if (rows.Count > 0)
|
||
{
|
||
var body = worksheet.Cells[2, 1, lastRow, columns.Count];
|
||
body.Style.VerticalAlignment = ExcelVerticalAlignment.Top;
|
||
body.Style.Border.Bottom.Style = ExcelBorderStyle.Hair;
|
||
body.Style.Border.Bottom.Color.SetColor(Color.FromRgb(224, 229, 236));
|
||
}
|
||
|
||
allCells.AutoFitColumns(10, 36);
|
||
for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
|
||
{
|
||
var preferredWidth = columns[columnIndex].PreferredWidth;
|
||
if (preferredWidth.HasValue)
|
||
{
|
||
worksheet.Column(columnIndex + 1).Width = preferredWidth.Value;
|
||
if (rows.Count > 0)
|
||
worksheet.Cells[2, columnIndex + 1, lastRow, columnIndex + 1].Style.WrapText = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static ExportColumn<T> CreateTextColumn<T>(
|
||
string header,
|
||
Func<T, object> value,
|
||
double? preferredWidth = null)
|
||
{
|
||
return new ExportColumn<T>(header, value, ExportCellKind.Text, "@", preferredWidth);
|
||
}
|
||
|
||
private static ExportColumn<T> CreateDateColumn<T>(
|
||
string header,
|
||
Func<T, object> value,
|
||
string numberFormat = "yyyy-mm-dd")
|
||
{
|
||
return new ExportColumn<T>(header, value, ExportCellKind.Date, numberFormat, 13);
|
||
}
|
||
|
||
private static ExportColumn<T> CreateNumberColumn<T>(
|
||
string header,
|
||
Func<T, object> value,
|
||
string numberFormat = "#,##0.00")
|
||
{
|
||
return new ExportColumn<T>(header, value, ExportCellKind.Number, numberFormat, 14);
|
||
}
|
||
|
||
private static ObEmployeeContractSnapshot SelectPrimaryContract(
|
||
IEnumerable<ObEmployeeContractSnapshot> snapshots)
|
||
{
|
||
return snapshots
|
||
.OrderByDescending(item => ContainsLaborContract(item.ContractName)
|
||
|| ContainsLaborContract(item.ContractType))
|
||
.ThenByDescending(CountCommonContractValues)
|
||
.ThenBy(item => item.SnapshotAt)
|
||
.ThenBy(item => item.Id)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
private static bool ContainsLaborContract(string value)
|
||
{
|
||
return !string.IsNullOrWhiteSpace(value)
|
||
&& value.Contains("劳动合同", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private static int CountCommonContractValues(ObEmployeeContractSnapshot snapshot)
|
||
{
|
||
return new object[]
|
||
{
|
||
snapshot.ContractStartDate, snapshot.ContractEndDate, snapshot.ContractTerm,
|
||
snapshot.ProbationStartDate, snapshot.ProbationEndDate, snapshot.ProbationTerm,
|
||
snapshot.ProbationSalary, snapshot.BaseSalary, snapshot.PositionSalary,
|
||
snapshot.PerformanceAllowance, snapshot.AttendanceBonus, snapshot.RegularSalary,
|
||
snapshot.WorkCity, snapshot.PositionName, snapshot.EmployeeNo
|
||
}.Count(value => value != null && !string.IsNullOrWhiteSpace(value.ToString()));
|
||
}
|
||
|
||
private static ObTaskEmployeeInfo DeserializeEmployeeInfo(string json)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
return null;
|
||
|
||
try
|
||
{
|
||
return JsonSerializer.Deserialize<ObTaskEmployeeInfo>(json);
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static DateTime? ParseDate(string value)
|
||
{
|
||
return DateTime.TryParse(value, out var date) ? date.Date : null;
|
||
}
|
||
|
||
private static string FirstNotBlank(params string[] values)
|
||
{
|
||
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||
}
|
||
|
||
private static int? CalculateAge(DateTime? birthDate)
|
||
{
|
||
if (!birthDate.HasValue || birthDate.Value.Date > DateTime.Today)
|
||
return null;
|
||
|
||
var birthday = birthDate.Value.Date;
|
||
var age = DateTime.Today.Year - birthday.Year;
|
||
if (birthday.AddYears(age) > DateTime.Today)
|
||
age--;
|
||
return age < 0 ? null : age;
|
||
}
|
||
|
||
private static string FormatTenure(DateTime? entryDate)
|
||
{
|
||
if (!entryDate.HasValue || entryDate.Value.Date > DateTime.Today)
|
||
return null;
|
||
|
||
var start = entryDate.Value.Date;
|
||
var today = DateTime.Today;
|
||
var months = (today.Year - start.Year) * 12 + today.Month - start.Month;
|
||
if (today.Day < start.Day)
|
||
months--;
|
||
months = Math.Max(months, 0);
|
||
|
||
var years = months / 12;
|
||
var remainingMonths = months % 12;
|
||
if (years > 0 && remainingMonths > 0)
|
||
return $"{years}年{remainingMonths}个月";
|
||
if (years > 0)
|
||
return $"{years}年";
|
||
return $"{remainingMonths}个月";
|
||
}
|
||
|
||
private static string EmploymentTypeText(string status)
|
||
{
|
||
return status == EmployeeEmploymentType.PartTime ? "兼职" : "正式";
|
||
}
|
||
|
||
private static string RegularizationText(ObEmployee employee)
|
||
{
|
||
if (employee.EmploymentType == EmployeeEmploymentType.PartTime
|
||
|| employee.RegularizationStatus == EmployeeRegularizationStatus.NotApplicable)
|
||
{
|
||
return "不适用";
|
||
}
|
||
|
||
if (employee.RegularizationStatus == EmployeeRegularizationStatus.Regular
|
||
|| employee.RegularizationDate.HasValue)
|
||
{
|
||
return "是";
|
||
}
|
||
|
||
return "否";
|
||
}
|
||
|
||
private static string SocialInsuranceText(string status)
|
||
{
|
||
return status switch
|
||
{
|
||
EmployeeSocialInsuranceStatus.Purchased => "是",
|
||
EmployeeSocialInsuranceStatus.NotPurchased => "否",
|
||
EmployeeSocialInsuranceStatus.Stopped => "已停保",
|
||
EmployeeSocialInsuranceStatus.NotApplicable => "不适用",
|
||
_ => null
|
||
};
|
||
}
|
||
|
||
private static string FormatJsonRows(
|
||
string json,
|
||
params (string Key, string Label)[] fields)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
return null;
|
||
|
||
try
|
||
{
|
||
using var document = JsonDocument.Parse(json);
|
||
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
||
return json;
|
||
|
||
var rows = new List<string>();
|
||
foreach (var item in document.RootElement.EnumerateArray())
|
||
{
|
||
if (item.ValueKind != JsonValueKind.Object)
|
||
continue;
|
||
var values = new List<string>();
|
||
foreach (var field in fields)
|
||
{
|
||
if (!item.TryGetProperty(field.Key, out var property))
|
||
continue;
|
||
var value = JsonValueToString(property);
|
||
if (!string.IsNullOrWhiteSpace(value))
|
||
values.Add($"{field.Label}:{value}");
|
||
}
|
||
if (values.Count > 0)
|
||
rows.Add(string.Join(",", values));
|
||
}
|
||
|
||
return rows.Count == 0 ? null : string.Join(";", rows);
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return json;
|
||
}
|
||
}
|
||
|
||
private static string FormatJsonObject(string json)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
return null;
|
||
|
||
try
|
||
{
|
||
using var document = JsonDocument.Parse(json);
|
||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||
return json;
|
||
return string.Join(
|
||
";",
|
||
document.RootElement.EnumerateObject()
|
||
.Select(property => $"{property.Name}:{JsonValueToString(property.Value)}"));
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return json;
|
||
}
|
||
}
|
||
|
||
private static Dictionary<string, ContractVariableExportValue> ParseContractVariables(string json)
|
||
{
|
||
var result = new Dictionary<string, ContractVariableExportValue>(StringComparer.OrdinalIgnoreCase);
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
return result;
|
||
|
||
try
|
||
{
|
||
using var document = JsonDocument.Parse(json);
|
||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||
return result;
|
||
|
||
foreach (var property in document.RootElement.EnumerateObject())
|
||
{
|
||
var item = property.Value;
|
||
if (item.ValueKind != JsonValueKind.Object)
|
||
continue;
|
||
result[property.Name] = new ContractVariableExportValue
|
||
{
|
||
Code = property.Name,
|
||
Name = TryGetJsonString(item, "variableName"),
|
||
Value = TryGetJsonString(item, "variableValue"),
|
||
SortCode = TryGetJsonInt(item, "sortCode")
|
||
};
|
||
}
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
// 单份历史快照异常时保留其固定字段,跳过无法解析的动态变量。
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static string TryGetJsonString(JsonElement element, string propertyName)
|
||
{
|
||
return element.TryGetProperty(propertyName, out var property)
|
||
? JsonValueToString(property)
|
||
: null;
|
||
}
|
||
|
||
private static int TryGetJsonInt(JsonElement element, string propertyName)
|
||
{
|
||
return element.TryGetProperty(propertyName, out var property)
|
||
&& property.TryGetInt32(out var value)
|
||
? value
|
||
: int.MaxValue;
|
||
}
|
||
|
||
private static string JsonValueToString(JsonElement element)
|
||
{
|
||
return element.ValueKind switch
|
||
{
|
||
JsonValueKind.String => element.GetString(),
|
||
JsonValueKind.Null => null,
|
||
JsonValueKind.Undefined => null,
|
||
_ => element.ToString()
|
||
};
|
||
}
|
||
|
||
private sealed class EmployeeExportContext
|
||
{
|
||
public ObEmployee Employee { get; set; }
|
||
|
||
public ObTaskEmployeeInfo EmployeeInfo { get; set; }
|
||
|
||
public ObEmployeeContractSnapshot PrimaryContract { get; set; }
|
||
|
||
public int ContractCount { get; set; }
|
||
}
|
||
|
||
private sealed class ContractExportContext
|
||
{
|
||
public ObEmployee Employee { get; set; }
|
||
|
||
public ObEmployeeContractSnapshot Snapshot { get; set; }
|
||
|
||
public string TaskNo { get; set; }
|
||
|
||
public Dictionary<string, ContractVariableExportValue> Variables { get; set; }
|
||
}
|
||
|
||
private sealed class ContractVariableExportValue
|
||
{
|
||
public string Code { get; set; }
|
||
|
||
public string Name { get; set; }
|
||
|
||
public string Value { get; set; }
|
||
|
||
public int SortCode { get; set; }
|
||
}
|
||
|
||
private sealed class ExportColumn<T>
|
||
{
|
||
public ExportColumn(
|
||
string header,
|
||
Func<T, object> value,
|
||
ExportCellKind kind,
|
||
string numberFormat,
|
||
double? preferredWidth)
|
||
{
|
||
Header = header;
|
||
Value = value;
|
||
Kind = kind;
|
||
NumberFormat = numberFormat;
|
||
PreferredWidth = preferredWidth;
|
||
}
|
||
|
||
public string Header { get; }
|
||
|
||
public Func<T, object> Value { get; }
|
||
|
||
public ExportCellKind Kind { get; }
|
||
|
||
public string NumberFormat { get; }
|
||
|
||
public double? PreferredWidth { get; }
|
||
}
|
||
|
||
private enum ExportCellKind
|
||
{
|
||
Text,
|
||
Date,
|
||
Number
|
||
}
|
||
|
||
/// <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];
|
||
}
|
||
}
|
||
}
|