111
This commit is contained in:
@@ -11,9 +11,9 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过后生成的正式员工档案,是入职流程的最终业务数据。
|
||||
/// 审核通过后生成的员工档案,是入职流程的最终业务数据。
|
||||
/// </summary>
|
||||
[SugarTable("ob_employee", TableDescription = "正式员工档案")]
|
||||
[SugarTable("ob_employee", TableDescription = "员工档案")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObEmployee : BaseEntity
|
||||
{
|
||||
@@ -59,6 +59,21 @@ public class ObEmployee : BaseEntity
|
||||
/// </summary>
|
||||
public long PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用工类型:full_time 正式用工,part_time 兼职用工。
|
||||
/// </summary>
|
||||
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
|
||||
|
||||
/// <summary>
|
||||
/// 转正状态:probation 试用期、regular 已转正、not_applicable 不适用。
|
||||
/// </summary>
|
||||
public string RegularizationStatus { get; set; } = EmployeeRegularizationStatus.Probation;
|
||||
|
||||
/// <summary>
|
||||
/// 社保状态:unknown 未维护、not_purchased 未购买、purchased 已购买、stopped 已停保、not_applicable 不适用。
|
||||
/// </summary>
|
||||
public string SocialInsuranceStatus { get; set; } = EmployeeSocialInsuranceStatus.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// 所属岗位名称,仅用于接口展示,不映射数据库字段。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 员工人事材料附件,可关联转正记录及后续扩展的人事业务。
|
||||
/// </summary>
|
||||
[SugarTable("ob_employee_attachment", TableDescription = "员工人事材料附件")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObEmployeeAttachment : BaseEntity
|
||||
{
|
||||
public long EmployeeId { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public long? ChangeRecordId { get; set; }
|
||||
|
||||
public string BusinessType { get; set; }
|
||||
|
||||
public long FileId { get; set; }
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
public DateTime UploadedAt { get; set; }
|
||||
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string FileSuffix { get; set; }
|
||||
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string FileSizeInfo { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 员工人事信息变更记录,保留转正及后续扩展业务的前后快照。
|
||||
/// </summary>
|
||||
[SugarTable("ob_employee_change_record", TableDescription = "员工人事信息变更记录")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObEmployeeChangeRecord : BaseEntity
|
||||
{
|
||||
public long EmployeeId { get; set; }
|
||||
|
||||
public string ChangeType { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? EffectiveDate { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||
public string BeforeJson { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
|
||||
public string AfterJson { get; set; }
|
||||
|
||||
public string Remark { get; set; }
|
||||
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public List<ObEmployeeAttachment> Attachments { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 正式员工的一份合同在审核建档时冻结的业务快照。
|
||||
/// 常用日期和薪资使用独立列,全部标准及自定义变量同时保存在 VariablesJson 中。
|
||||
/// </summary>
|
||||
[SugarTable("ob_employee_contract_snapshot", TableDescription = "员工合同快照")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObEmployeeContractSnapshot : BaseEntity
|
||||
{
|
||||
/// <summary>正式员工 Id。</summary>
|
||||
public long EmployeeId { get; set; }
|
||||
|
||||
/// <summary>来源入职任务 Id。</summary>
|
||||
public long TaskId { get; set; }
|
||||
|
||||
/// <summary>来源任务合同 Id,同一任务合同只允许生成一份员工快照。</summary>
|
||||
public long TaskContractId { get; set; }
|
||||
|
||||
/// <summary>合同模板 Id。</summary>
|
||||
public long TemplateId { get; set; }
|
||||
|
||||
/// <summary>合同模板版本 Id。</summary>
|
||||
public long TemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>合同名称快照。</summary>
|
||||
public string ContractName { get; set; }
|
||||
|
||||
/// <summary>合同类型快照。</summary>
|
||||
public string ContractType { get; set; }
|
||||
|
||||
/// <summary>合同开始日期。</summary>
|
||||
public DateTime? ContractStartDate { get; set; }
|
||||
|
||||
/// <summary>合同结束日期。</summary>
|
||||
public DateTime? ContractEndDate { get; set; }
|
||||
|
||||
/// <summary>合同期限原始文本。</summary>
|
||||
public string ContractTerm { get; set; }
|
||||
|
||||
/// <summary>试用期开始日期。</summary>
|
||||
public DateTime? ProbationStartDate { get; set; }
|
||||
|
||||
/// <summary>试用期结束日期。</summary>
|
||||
public DateTime? ProbationEndDate { get; set; }
|
||||
|
||||
/// <summary>试用期期限原始文本。</summary>
|
||||
public string ProbationTerm { get; set; }
|
||||
|
||||
/// <summary>明确使用 hr.ProbationSalary 保存的试用薪资;不从其他工资项目推算。</summary>
|
||||
public decimal? ProbationSalary { get; set; }
|
||||
|
||||
/// <summary>基本工资。</summary>
|
||||
public decimal? BaseSalary { get; set; }
|
||||
|
||||
/// <summary>岗位工资。</summary>
|
||||
public decimal? PositionSalary { get; set; }
|
||||
|
||||
/// <summary>绩效津贴。</summary>
|
||||
public decimal? PerformanceAllowance { get; set; }
|
||||
|
||||
/// <summary>全勤奖。</summary>
|
||||
public decimal? AttendanceBonus { get; set; }
|
||||
|
||||
/// <summary>转正后薪资。</summary>
|
||||
public decimal? RegularSalary { get; set; }
|
||||
|
||||
/// <summary>工作地点或城市。</summary>
|
||||
public string WorkCity { get; set; }
|
||||
|
||||
/// <summary>合同中的岗位名称。</summary>
|
||||
public string PositionName { get; set; }
|
||||
|
||||
/// <summary>合同中的员工工号。</summary>
|
||||
public string EmployeeNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 以 VariableCode 为键保存该合同当时的全部变量元数据和值。
|
||||
/// 导出时通过稳定编码判断变量是否存在,不依赖可修改的中文名称。
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = false)]
|
||||
public string VariablesJson { get; set; }
|
||||
|
||||
/// <summary>快照结构版本。</summary>
|
||||
public int SnapshotVersion { get; set; } = 1;
|
||||
|
||||
/// <summary>快照生成时间。</summary>
|
||||
public DateTime SnapshotAt { get; set; }
|
||||
}
|
||||
@@ -33,6 +33,11 @@ public class ObOnboardingTask : BaseEntity
|
||||
/// </summary>
|
||||
public long PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用工类型:full_time 正式用工,part_time 兼职用工。
|
||||
/// </summary>
|
||||
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时锁定的入职模板 Id。
|
||||
/// </summary>
|
||||
@@ -106,4 +111,4 @@ public class ObOnboardingTask : BaseEntity
|
||||
/// </summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string OnboardingTemplateName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public static class OnboardingTaskStatus
|
||||
public const string Rejected = "rejected";
|
||||
|
||||
/// <summary>
|
||||
/// 已通过,系统已经生成正式员工档案。
|
||||
/// 已通过,系统已经生成员工档案。
|
||||
/// </summary>
|
||||
public const string Approved = "approved";
|
||||
|
||||
@@ -144,6 +144,55 @@ public static class TaskContractStatus
|
||||
public const string Archived = "archived";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工用工类型。
|
||||
/// </summary>
|
||||
public static class EmployeeEmploymentType
|
||||
{
|
||||
public const string FullTime = "full_time";
|
||||
public const string PartTime = "part_time";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工转正状态,仅由后台人事操作改变,不根据合同日期自动推算。
|
||||
/// </summary>
|
||||
public static class EmployeeRegularizationStatus
|
||||
{
|
||||
public const string Probation = "probation";
|
||||
public const string Regular = "regular";
|
||||
public const string NotApplicable = "not_applicable";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工社保状态。
|
||||
/// </summary>
|
||||
public static class EmployeeSocialInsuranceStatus
|
||||
{
|
||||
public const string Unknown = "unknown";
|
||||
public const string NotPurchased = "not_purchased";
|
||||
public const string Purchased = "purchased";
|
||||
public const string Stopped = "stopped";
|
||||
public const string NotApplicable = "not_applicable";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工人事变更类型。
|
||||
/// </summary>
|
||||
public static class EmployeeChangeType
|
||||
{
|
||||
public const string HrInfoUpdate = "hr_info_update";
|
||||
public const string Regularization = "regularization";
|
||||
public const string RegularizationRevoke = "regularization_revoke";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工附件业务类型。
|
||||
/// </summary>
|
||||
public static class EmployeeAttachmentBusinessType
|
||||
{
|
||||
public const string Regularization = "regularization";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 电子签服务商。
|
||||
/// </summary>
|
||||
|
||||
@@ -8,14 +8,17 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
private readonly SqlSugarScope _db;
|
||||
private readonly IConfigService _config;
|
||||
private readonly IOnboardingContractSnapshotService _snapshotService;
|
||||
private readonly IOnboardingEmployeeContractSnapshotService _employeeContractSnapshotService;
|
||||
|
||||
public OnboardingAuditService(
|
||||
IConfigService config,
|
||||
IOnboardingContractSnapshotService snapshotService)
|
||||
IOnboardingContractSnapshotService snapshotService,
|
||||
IOnboardingEmployeeContractSnapshotService employeeContractSnapshotService)
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
_config = config;
|
||||
_snapshotService = snapshotService;
|
||||
_employeeContractSnapshotService = employeeContractSnapshotService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,6 +54,13 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
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);
|
||||
@@ -96,6 +106,7 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
|
||||
await _snapshotService.Build(task.Id, employeeInfo);
|
||||
await _db.Insertable(employee).ExecuteCommandAsync();
|
||||
await _employeeContractSnapshotService.Build(employee.Id, task.Id);
|
||||
|
||||
task.EmployeeId = employee.Id;
|
||||
task.TaskStatus = OnboardingTaskStatus.Approved;
|
||||
@@ -197,7 +208,7 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (normalizedValues.Count == 0)
|
||||
throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成正式员工档案");
|
||||
throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成员工档案");
|
||||
|
||||
var parsedDates = new List<DateTime>();
|
||||
foreach (var value in normalizedValues)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 统一校验入职业务中的可编辑编码,避免非法字符或超长内容落到数据库后才报 500。
|
||||
/// </summary>
|
||||
internal static class OnboardingCodeValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 规范化并校验业务编码。编码必须以字母开头,可包含字母、数字、点、下划线和短横线。
|
||||
/// </summary>
|
||||
public static string Normalize(string code, string fieldName, int maxLength)
|
||||
{
|
||||
var normalized = code?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
throw Oops.Bah($"{fieldName}不能为空");
|
||||
|
||||
if (normalized.Length > maxLength)
|
||||
throw Oops.Bah($"{fieldName}长度不能超过 {maxLength} 个字符");
|
||||
|
||||
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||
normalized,
|
||||
@"^[A-Za-z][A-Za-z0-9._-]*$"))
|
||||
{
|
||||
throw Oops.Bah($"{fieldName}必须以字母开头,只能包含字母、数字、点、下划线和短横线");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,12 @@ public class OnboardingCompanyService : DbRepository<ObCompany>, IOnboardingComp
|
||||
|
||||
private static void NormalizeAndValidateQiyuesuoConfig(ObCompany company)
|
||||
{
|
||||
company.CompanyCode = OnboardingCodeValidator.Normalize(company.CompanyCode, "公司编码", 50);
|
||||
company.CompanyName = company.CompanyName?.Trim();
|
||||
company.LegalName = company.LegalName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(company.CompanyName))
|
||||
throw Oops.Bah("公司名称不能为空");
|
||||
|
||||
company.QiyuesuoCategoryId = company.QiyuesuoCategoryId?.Trim();
|
||||
company.QiyuesuoSealId = company.QiyuesuoSealId?.Trim();
|
||||
|
||||
|
||||
+2
-3
@@ -450,13 +450,12 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
/// </summary>
|
||||
private static void NormalizeInput(OnboardingContractTemplateAddInput input)
|
||||
{
|
||||
input.TemplateCode = input.TemplateCode?.Trim();
|
||||
input.TemplateCode = OnboardingCodeValidator.Normalize(input.TemplateCode, "签署材料模板编码", 50);
|
||||
input.TemplateName = input.TemplateName?.Trim();
|
||||
input.ContractType = input.ContractType?.Trim();
|
||||
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.TemplateCode)
|
||||
|| string.IsNullOrWhiteSpace(input.TemplateName)
|
||||
if (string.IsNullOrWhiteSpace(input.TemplateName)
|
||||
|| string.IsNullOrWhiteSpace(input.ContractType))
|
||||
{
|
||||
throw Oops.Bah("请填写模板编码、模板名称和合同类型");
|
||||
|
||||
+3
-4
@@ -133,12 +133,11 @@ public class OnboardingDepartmentService : DbRepository<ObDepartment>, IOnboardi
|
||||
/// </summary>
|
||||
private static void NormalizeInput(OnboardingDepartmentAddInput input)
|
||||
{
|
||||
input.DepartmentCode = input.DepartmentCode?.Trim();
|
||||
input.DepartmentCode = OnboardingCodeValidator.Normalize(input.DepartmentCode, "部门编码", 50);
|
||||
input.DepartmentName = input.DepartmentName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input.DepartmentCode)
|
||||
|| string.IsNullOrWhiteSpace(input.DepartmentName))
|
||||
if (string.IsNullOrWhiteSpace(input.DepartmentName))
|
||||
{
|
||||
throw Oops.Bah("请填写部门编码和部门名称");
|
||||
throw Oops.Bah("请填写部门名称");
|
||||
}
|
||||
|
||||
if (input.Status != "enable" && input.Status != "disable")
|
||||
|
||||
+5
-5
@@ -138,7 +138,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
{
|
||||
var employee = await _db.Queryable<ObEmployee>()
|
||||
.InSingleAsync(task.EmployeeId.Value)
|
||||
?? throw Oops.Bah("正式员工档案不存在,不能发起电子签");
|
||||
?? throw Oops.Bah("员工档案不存在,不能发起电子签");
|
||||
if (string.IsNullOrWhiteSpace(employee.EmployeeName))
|
||||
throw Oops.Bah("正式员工姓名为空,不能发起电子签");
|
||||
if (string.IsNullOrWhiteSpace(employee.Phone))
|
||||
@@ -1417,7 +1417,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
}
|
||||
|
||||
if (differentFields.Count > 0)
|
||||
throw Oops.Bah($"正式员工档案的{string.Join("、", differentFields)}与审核通过的入职登记资料不一致,请先核对并修正后再发起电子签");
|
||||
throw Oops.Bah($"员工档案的{string.Join("、", differentFields)}与审核通过的入职登记资料不一致,请先核对并修正后再发起电子签");
|
||||
}
|
||||
|
||||
private static string NormalizeIdentityValue(string value)
|
||||
@@ -1468,7 +1468,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
private static void ValidateTaskCanSign(ObOnboardingTask task)
|
||||
{
|
||||
if (!task.EmployeeId.HasValue)
|
||||
throw Oops.Bah("入职任务尚未生成正式员工档案,不能发起电子签");
|
||||
throw Oops.Bah("入职任务尚未生成员工档案,不能发起电子签");
|
||||
if (task.TaskStatus != OnboardingTaskStatus.Approved
|
||||
&& task.TaskStatus != OnboardingTaskStatus.Archived)
|
||||
{
|
||||
@@ -1527,7 +1527,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
bool processAllowed)
|
||||
{
|
||||
if (!task.EmployeeId.HasValue)
|
||||
return "审核通过并生成正式员工档案后才可发起电子签";
|
||||
return "审核通过并生成员工档案后才可发起电子签";
|
||||
if (!taskAllowed)
|
||||
return "当前入职任务状态不能发起电子签";
|
||||
if (!processAllowed)
|
||||
@@ -1542,7 +1542,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
|
||||
bool reinitiateAllowed)
|
||||
{
|
||||
if (!task.EmployeeId.HasValue)
|
||||
return "审核通过并生成正式员工档案后才可重新发起电子签";
|
||||
return "审核通过并生成员工档案后才可重新发起电子签";
|
||||
if (!taskAllowed)
|
||||
return "当前入职任务状态不能重新发起电子签";
|
||||
if (process == null)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,6 +31,84 @@ public class OnboardingEmployeePageInput : BasePageInput
|
||||
/// 岗位 Id。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用工类型。
|
||||
/// </summary>
|
||||
public string EmploymentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转正状态。
|
||||
/// </summary>
|
||||
public string RegularizationStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 社保状态。
|
||||
/// </summary>
|
||||
public string SocialInsuranceStatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑员工人事信息。
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeHrUpdateInput
|
||||
{
|
||||
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "请选择用工类型")]
|
||||
public string EmploymentType { get; set; }
|
||||
|
||||
public string ContractNature { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "请选择社保状态")]
|
||||
public string SocialInsuranceStatus { get; set; }
|
||||
|
||||
public string SocialInsuranceCompany { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 员工转正办理参数。
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeRegularizeInput
|
||||
{
|
||||
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "请选择转正日期")]
|
||||
public DateTime? RegularizationDate { get; set; }
|
||||
|
||||
public string Remark { get; set; }
|
||||
|
||||
public List<long> FileIds { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销员工转正参数。
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeRegularizationRevokeInput
|
||||
{
|
||||
[IdNotNull(ErrorMessage = "员工Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "请填写撤销原因")]
|
||||
public string Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 正式员工档案导出结果。
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeExportFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel 文件字节。
|
||||
/// </summary>
|
||||
public byte[] FileBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +136,12 @@ public class OnboardingEmployeeDetailOutput
|
||||
/// </summary>
|
||||
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
|
||||
|
||||
/// <summary>
|
||||
/// 审核建档时冻结的员工合同业务快照,包含常用字段及全部自定义变量。
|
||||
/// </summary>
|
||||
public List<ObEmployeeContractSnapshot> ContractSnapshots { get; set; }
|
||||
= new List<ObEmployeeContractSnapshot>();
|
||||
|
||||
/// <summary>
|
||||
/// 员工入职时上传的身份证、学历证明、体检报告等资料。
|
||||
/// </summary>
|
||||
@@ -71,4 +157,10 @@ public class OnboardingEmployeeDetailOutput
|
||||
/// 提交与审核记录列表。
|
||||
/// </summary>
|
||||
public List<ObTaskAudit> Audits { get; set; } = new List<ObTaskAudit>();
|
||||
|
||||
/// <summary>
|
||||
/// 员工转正及人事维护历史。
|
||||
/// </summary>
|
||||
public List<ObEmployeeChangeRecord> ChangeRecords { get; set; }
|
||||
= new List<ObEmployeeChangeRecord>();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义正式员工档案查询业务能力。
|
||||
/// 定义员工档案查询和人事维护业务能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingEmployeeService : ITransient
|
||||
{
|
||||
@@ -10,6 +10,22 @@ public interface IOnboardingEmployeeService : ITransient
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input);
|
||||
/// <summary>
|
||||
/// 按当前列表筛选条件导出员工明细。
|
||||
/// </summary>
|
||||
Task<OnboardingEmployeeExportFile> Export(OnboardingEmployeePageInput input);
|
||||
/// <summary>
|
||||
/// 编辑员工用工、社保及合同性质等人事信息。
|
||||
/// </summary>
|
||||
Task UpdateHrInfo(OnboardingEmployeeHrUpdateInput input);
|
||||
/// <summary>
|
||||
/// 人工办理员工转正。
|
||||
/// </summary>
|
||||
Task Regularize(OnboardingEmployeeRegularizeInput input);
|
||||
/// <summary>
|
||||
/// 撤销一次员工转正,历史记录和附件继续保留。
|
||||
/// </summary>
|
||||
Task RevokeRegularization(OnboardingEmployeeRegularizationRevokeInput input);
|
||||
/// <summary>
|
||||
/// 查询正式员工及其来源任务、合同和审核记录。
|
||||
/// </summary>
|
||||
Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input);
|
||||
|
||||
+1008
-13
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义正式员工合同快照归档能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingEmployeeContractSnapshotService : ITransient
|
||||
{
|
||||
/// <summary>冻结一名员工来源任务中的全部合同和变量。</summary>
|
||||
Task Build(long employeeId, long taskId);
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 在正式员工建档时,将每份任务合同及其全部变量冻结为长期业务快照。
|
||||
/// </summary>
|
||||
public class OnboardingEmployeeContractSnapshotService : IOnboardingEmployeeContractSnapshotService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
|
||||
public OnboardingEmployeeContractSnapshotService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Build(long employeeId, long taskId)
|
||||
{
|
||||
var contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(item => item.TaskId == taskId)
|
||||
.OrderBy(item => item.Id)
|
||||
.ToListAsync();
|
||||
if (contracts.Count == 0)
|
||||
throw Oops.Bah("入职任务没有合同,无法生成员工合同快照");
|
||||
|
||||
var contractIds = contracts.Select(item => item.Id).ToList();
|
||||
var allVariables = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(item => contractIds.Contains(item.TaskContractId))
|
||||
.OrderBy(item => item.SortCode)
|
||||
.OrderBy(item => item.Id)
|
||||
.ToListAsync();
|
||||
var now = DateTime.Now;
|
||||
var snapshots = new List<ObEmployeeContractSnapshot>();
|
||||
|
||||
foreach (var contract in contracts)
|
||||
{
|
||||
var variables = allVariables
|
||||
.Where(item => item.TaskContractId == contract.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ThenBy(item => item.Id)
|
||||
.ToList();
|
||||
var valueMap = variables
|
||||
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Last().VariableValue,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var variableSnapshot = variables
|
||||
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group =>
|
||||
{
|
||||
// 历史任务若意外存在重复变量,以排序后的最后一条最终值为准,避免建档时报 500。
|
||||
var item = group.Last();
|
||||
return (object)new
|
||||
{
|
||||
variableName = item.VariableName,
|
||||
variableType = item.VariableType,
|
||||
dataType = item.DataType,
|
||||
variableValue = item.VariableValue,
|
||||
sourceType = item.SourceType,
|
||||
isManualOverride = item.IsManualOverride,
|
||||
sortCode = item.SortCode
|
||||
};
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
snapshots.Add(new ObEmployeeContractSnapshot
|
||||
{
|
||||
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||
EmployeeId = employeeId,
|
||||
TaskId = taskId,
|
||||
TaskContractId = contract.Id,
|
||||
TemplateId = contract.TemplateId,
|
||||
TemplateVersionId = contract.TemplateVersionId,
|
||||
ContractName = contract.ContractName,
|
||||
ContractType = contract.ContractType,
|
||||
ContractStartDate = ParseDate(GetValue(valueMap, "hr.ContractStartDate")),
|
||||
ContractEndDate = ParseDate(GetValue(valueMap, "hr.ContractEndDate")),
|
||||
ContractTerm = GetValue(valueMap, "hr.ContractTerm"),
|
||||
ProbationStartDate = ParseDate(GetValue(valueMap, "hr.ProbationStartDate")),
|
||||
ProbationEndDate = ParseDate(GetValue(valueMap, "hr.ProbationEndDate")),
|
||||
ProbationTerm = GetValue(valueMap, "hr.ProbationTerm"),
|
||||
ProbationSalary = ParseAmount(GetValue(valueMap, "hr.ProbationSalary")),
|
||||
BaseSalary = ParseAmount(GetValue(valueMap, "hr.BaseSalary")),
|
||||
PositionSalary = ParseAmount(GetValue(valueMap, "hr.PositionSalary")),
|
||||
PerformanceAllowance = ParseAmount(GetValue(valueMap, "hr.PerformanceAllowance")),
|
||||
AttendanceBonus = ParseAmount(GetValue(valueMap, "hr.AttendanceBonus")),
|
||||
RegularSalary = ParseAmount(GetValue(valueMap, "hr.RegularSalary")),
|
||||
WorkCity = GetValue(valueMap, "hr.WorkCity"),
|
||||
PositionName = GetValue(valueMap, "hr.PositionName"),
|
||||
EmployeeNo = GetValue(valueMap, "hr.EmployeeNo"),
|
||||
VariablesJson = global::System.Text.Json.JsonSerializer.Serialize(variableSnapshot),
|
||||
SnapshotVersion = 1,
|
||||
SnapshotAt = now,
|
||||
Status = CommonStatusConst.ENABLE,
|
||||
CreateTime = now,
|
||||
CreateUserId = UserManager.UserId,
|
||||
CreateUser = UserManager.Name,
|
||||
IsDelete = false
|
||||
});
|
||||
}
|
||||
|
||||
// Build 只在审核建档事务内执行;先删后建使异常重试保持幂等。
|
||||
await _db.Deleteable<ObEmployeeContractSnapshot>()
|
||||
.Where(item => item.TaskId == taskId)
|
||||
.ExecuteCommandAsync();
|
||||
await _db.Insertable(snapshots).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private static string GetValue(
|
||||
IReadOnlyDictionary<string, string> valueMap,
|
||||
string variableCode)
|
||||
{
|
||||
return valueMap.TryGetValue(variableCode, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||
? value.Trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DateTime? ParseDate(string value)
|
||||
{
|
||||
return DateTime.TryParse(value, out var date) ? date.Date : null;
|
||||
}
|
||||
|
||||
private static decimal? ParseAmount(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
var normalized = value
|
||||
.Replace("¥", string.Empty)
|
||||
.Replace("¥", string.Empty)
|
||||
.Replace("元", string.Empty)
|
||||
.Trim();
|
||||
if (decimal.TryParse(
|
||||
normalized,
|
||||
NumberStyles.Number | NumberStyles.AllowCurrencySymbol,
|
||||
CultureInfo.GetCultureInfo("zh-CN"),
|
||||
out var amount)
|
||||
|| decimal.TryParse(
|
||||
normalized,
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture,
|
||||
out amount))
|
||||
{
|
||||
return amount;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -27,6 +27,11 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
/// </summary>
|
||||
public async Task SaveFormField(OnboardingFormFieldSaveInput input)
|
||||
{
|
||||
input.FieldCode = OnboardingCodeValidator.Normalize(input.FieldCode, "字段编码", 100);
|
||||
input.FieldName = input.FieldName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input.FieldName))
|
||||
throw Oops.Bah("字段名称不能为空");
|
||||
|
||||
var fieldCodeQuery = _db.Queryable<ObFormField>()
|
||||
.Where(x => x.FieldCode == input.FieldCode);
|
||||
if (input.Id.HasValue)
|
||||
@@ -115,6 +120,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
/// </summary>
|
||||
public async Task SaveMaterialType(OnboardingMaterialTypeSaveInput input)
|
||||
{
|
||||
input.MaterialCode = OnboardingCodeValidator.Normalize(input.MaterialCode, "资料编码", 100);
|
||||
input.MaterialName = input.MaterialName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input.MaterialName))
|
||||
throw Oops.Bah("资料名称不能为空");
|
||||
if (input.MaxFileCount <= 0)
|
||||
throw Oops.Bah("最大文件数量必须大于0");
|
||||
|
||||
@@ -169,4 +178,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-10
@@ -92,12 +92,22 @@ public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
NormalizeAndValidateInput(input);
|
||||
await ValidateReferences(input);
|
||||
|
||||
var templateId = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
var codeExists = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.AnyAsync(item => item.TemplateCode == input.TemplateCode && item.Id != templateId);
|
||||
if (codeExists)
|
||||
var codeOwner = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.ClearFilter()
|
||||
.FirstAsync(item => item.TemplateCode == input.TemplateCode
|
||||
&& (!input.Id.HasValue || item.Id != input.Id.Value));
|
||||
if (codeOwner != null && !codeOwner.IsDelete)
|
||||
throw Oops.Bah("入职模板编码已存在");
|
||||
|
||||
if (input.Id.HasValue && codeOwner != null && codeOwner.IsDelete)
|
||||
throw Oops.Bah("该入职模板编码属于一条已删除记录,请通过新增模板重新启用该编码");
|
||||
|
||||
// 新增时如果编码命中历史软删除记录,复用原 Id 并恢复,避免唯一索引直接抛 500。
|
||||
var isRestore = !input.Id.HasValue && codeOwner != null && codeOwner.IsDelete;
|
||||
var templateId = isRestore
|
||||
? codeOwner.Id
|
||||
: input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
|
||||
var entity = new ObOnboardingTemplate
|
||||
{
|
||||
Id = templateId,
|
||||
@@ -106,15 +116,17 @@ public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
CompanyId = input.CompanyId,
|
||||
DepartmentId = input.DepartmentId,
|
||||
Description = input.Description,
|
||||
Status = input.Status
|
||||
Status = input.Status,
|
||||
IsDelete = false
|
||||
};
|
||||
|
||||
await _db.Ado.BeginTranAsync();
|
||||
try
|
||||
{
|
||||
if (input.Id.HasValue)
|
||||
if (input.Id.HasValue || isRestore)
|
||||
{
|
||||
var exists = await _db.Queryable<ObOnboardingTemplate>().AnyAsync(item => item.Id == templateId);
|
||||
var exists = isRestore || await _db.Queryable<ObOnboardingTemplate>()
|
||||
.AnyAsync(item => item.Id == templateId);
|
||||
if (!exists)
|
||||
throw Oops.Bah("入职模板不存在");
|
||||
await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
|
||||
@@ -189,7 +201,10 @@ public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
|
||||
private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input)
|
||||
{
|
||||
input.TemplateCode = input.TemplateCode?.Trim();
|
||||
input.TemplateCode = OnboardingCodeValidator.Normalize(
|
||||
input.TemplateCode,
|
||||
"入职模板编码",
|
||||
100);
|
||||
input.TemplateName = input.TemplateName?.Trim();
|
||||
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
|
||||
input.Contracts ??= new List<OnboardingTemplateContractInput>();
|
||||
@@ -208,8 +223,8 @@ public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
.Select(group => group.First())
|
||||
.ToList() ?? new List<OnboardingTemplateMaterialInput>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.TemplateCode) || string.IsNullOrWhiteSpace(input.TemplateName))
|
||||
throw Oops.Bah("请填写入职模板编码和名称");
|
||||
if (string.IsNullOrWhiteSpace(input.TemplateName))
|
||||
throw Oops.Bah("请填写入职模板名称");
|
||||
if (input.CompanyId <= 0)
|
||||
throw Oops.Bah("请选择所属公司");
|
||||
if (input.DepartmentId <= 0)
|
||||
|
||||
@@ -168,12 +168,11 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
|
||||
/// </summary>
|
||||
private static void NormalizeInput(OnboardingPositionAddInput input)
|
||||
{
|
||||
input.PositionCode = input.PositionCode?.Trim();
|
||||
input.PositionCode = OnboardingCodeValidator.Normalize(input.PositionCode, "岗位编码", 50);
|
||||
input.PositionName = input.PositionName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input.PositionCode)
|
||||
|| string.IsNullOrWhiteSpace(input.PositionName))
|
||||
if (string.IsNullOrWhiteSpace(input.PositionName))
|
||||
{
|
||||
throw Oops.Bah("请填写岗位编码和岗位名称");
|
||||
throw Oops.Bah("请填写岗位名称");
|
||||
}
|
||||
|
||||
if (input.Status != "enable" && input.Status != "disable")
|
||||
|
||||
@@ -25,6 +25,11 @@ public class OnboardingTaskCreateInput
|
||||
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
||||
public long PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用工类型:full_time 正式用工,part_time 兼职用工。
|
||||
/// </summary>
|
||||
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
|
||||
|
||||
/// <summary>
|
||||
/// 本次任务使用的入职模板 Id。
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,8 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
if (string.IsNullOrWhiteSpace(input.Remark))
|
||||
throw Oops.Bah("请填写人事备注");
|
||||
|
||||
ValidateEmploymentType(input.EmploymentType);
|
||||
|
||||
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||
if (!companyEnabled)
|
||||
@@ -123,6 +125,7 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
CompanyId = input.CompanyId,
|
||||
DepartmentId = input.DepartmentId,
|
||||
PositionId = input.PositionId,
|
||||
EmploymentType = input.EmploymentType,
|
||||
OnboardingTemplateId = onboardingTemplate.Id,
|
||||
AssignedReviewerUserId = input.AssignedReviewerUserId,
|
||||
LinkExpireAt = expiresAt,
|
||||
@@ -366,6 +369,9 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
CompanyId = sourceTask.CompanyId,
|
||||
DepartmentId = sourceTask.DepartmentId,
|
||||
PositionId = sourceTask.PositionId,
|
||||
EmploymentType = string.IsNullOrWhiteSpace(sourceTask.EmploymentType)
|
||||
? EmployeeEmploymentType.FullTime
|
||||
: sourceTask.EmploymentType,
|
||||
OnboardingTemplateId = sourceTask.OnboardingTemplateId,
|
||||
AssignedReviewerUserId = sourceTask.AssignedReviewerUserId,
|
||||
LinkExpireAt = expiresAt,
|
||||
@@ -553,4 +559,13 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
if (endDate.Date < startDate.Date)
|
||||
throw Oops.Bah($"{rangeName}结束日期不能早于开始日期");
|
||||
}
|
||||
|
||||
private static void ValidateEmploymentType(string employmentType)
|
||||
{
|
||||
if (employmentType != EmployeeEmploymentType.FullTime
|
||||
&& employmentType != EmployeeEmploymentType.PartTime)
|
||||
{
|
||||
throw Oops.Bah("用工类型不正确,请选择正式员工或兼职员工");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user