This commit is contained in:
fuenmao
2026-09-09 14:57:41 +08:00
parent c015bc356c
commit 95b14b198a
27 changed files with 2069 additions and 52 deletions
@@ -11,9 +11,9 @@
namespace SimpleAdmin.Application; namespace SimpleAdmin.Application;
/// <summary> /// <summary>
/// 审核通过后生成的正式员工档案,是入职流程的最终业务数据。 /// 审核通过后生成的员工档案,是入职流程的最终业务数据。
/// </summary> /// </summary>
[SugarTable("ob_employee", TableDescription = "正式员工档案")] [SugarTable("ob_employee", TableDescription = "员工档案")]
[Tenant(SqlSugarConst.DB_DEFAULT)] [Tenant(SqlSugarConst.DB_DEFAULT)]
public class ObEmployee : BaseEntity public class ObEmployee : BaseEntity
{ {
@@ -59,6 +59,21 @@ public class ObEmployee : BaseEntity
/// </summary> /// </summary>
public long PositionId { get; set; } 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>
/// 所属岗位名称,仅用于接口展示,不映射数据库字段。 /// 所属岗位名称,仅用于接口展示,不映射数据库字段。
/// </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> /// </summary>
public long PositionId { get; set; } public long PositionId { get; set; }
/// <summary>
/// 用工类型:full_time 正式用工,part_time 兼职用工。
/// </summary>
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
/// <summary> /// <summary>
/// 创建任务时锁定的入职模板 Id。 /// 创建任务时锁定的入职模板 Id。
/// </summary> /// </summary>
@@ -106,4 +111,4 @@ public class ObOnboardingTask : BaseEntity
/// </summary> /// </summary>
[SugarColumn(IsIgnore = true)] [SugarColumn(IsIgnore = true)]
public string OnboardingTemplateName { get; set; } public string OnboardingTemplateName { get; set; }
} }
@@ -36,7 +36,7 @@ public static class OnboardingTaskStatus
public const string Rejected = "rejected"; public const string Rejected = "rejected";
/// <summary> /// <summary>
/// 已通过,系统已经生成正式员工档案。 /// 已通过,系统已经生成员工档案。
/// </summary> /// </summary>
public const string Approved = "approved"; public const string Approved = "approved";
@@ -144,6 +144,55 @@ public static class TaskContractStatus
public const string Archived = "archived"; 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>
/// 电子签服务商。 /// 电子签服务商。
/// </summary> /// </summary>
@@ -8,14 +8,17 @@ public class OnboardingAuditService : IOnboardingAuditService
private readonly SqlSugarScope _db; private readonly SqlSugarScope _db;
private readonly IConfigService _config; private readonly IConfigService _config;
private readonly IOnboardingContractSnapshotService _snapshotService; private readonly IOnboardingContractSnapshotService _snapshotService;
private readonly IOnboardingEmployeeContractSnapshotService _employeeContractSnapshotService;
public OnboardingAuditService( public OnboardingAuditService(
IConfigService config, IConfigService config,
IOnboardingContractSnapshotService snapshotService) IOnboardingContractSnapshotService snapshotService,
IOnboardingEmployeeContractSnapshotService employeeContractSnapshotService)
{ {
_db = DbContext.DB; _db = DbContext.DB;
_config = config; _config = config;
_snapshotService = snapshotService; _snapshotService = snapshotService;
_employeeContractSnapshotService = employeeContractSnapshotService;
} }
/// <summary> /// <summary>
@@ -51,6 +54,13 @@ public class OnboardingAuditService : IOnboardingAuditService
employee.CompanyId = task.CompanyId; employee.CompanyId = task.CompanyId;
employee.DepartmentId = task.DepartmentId; employee.DepartmentId = task.DepartmentId;
employee.PositionId = task.PositionId; 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) if (task.DepartmentId.HasValue)
{ {
var dept = await _db.Queryable<ObDepartment>().InSingleAsync(task.DepartmentId.Value); 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 _snapshotService.Build(task.Id, employeeInfo);
await _db.Insertable(employee).ExecuteCommandAsync(); await _db.Insertable(employee).ExecuteCommandAsync();
await _employeeContractSnapshotService.Build(employee.Id, task.Id);
task.EmployeeId = employee.Id; task.EmployeeId = employee.Id;
task.TaskStatus = OnboardingTaskStatus.Approved; task.TaskStatus = OnboardingTaskStatus.Approved;
@@ -197,7 +208,7 @@ public class OnboardingAuditService : IOnboardingAuditService
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
if (normalizedValues.Count == 0) if (normalizedValues.Count == 0)
throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成正式员工档案"); throw Oops.Bah("签署材料参数“合同开始日期”尚未填写,无法生成员工档案");
var parsedDates = new List<DateTime>(); var parsedDates = new List<DateTime>();
foreach (var value in normalizedValues) 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) 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.QiyuesuoCategoryId = company.QiyuesuoCategoryId?.Trim();
company.QiyuesuoSealId = company.QiyuesuoSealId?.Trim(); company.QiyuesuoSealId = company.QiyuesuoSealId?.Trim();
@@ -450,13 +450,12 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
/// </summary> /// </summary>
private static void NormalizeInput(OnboardingContractTemplateAddInput input) private static void NormalizeInput(OnboardingContractTemplateAddInput input)
{ {
input.TemplateCode = input.TemplateCode?.Trim(); input.TemplateCode = OnboardingCodeValidator.Normalize(input.TemplateCode, "签署材料模板编码", 50);
input.TemplateName = input.TemplateName?.Trim(); input.TemplateName = input.TemplateName?.Trim();
input.ContractType = input.ContractType?.Trim(); input.ContractType = input.ContractType?.Trim();
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>(); input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
if (string.IsNullOrWhiteSpace(input.TemplateCode) if (string.IsNullOrWhiteSpace(input.TemplateName)
|| string.IsNullOrWhiteSpace(input.TemplateName)
|| string.IsNullOrWhiteSpace(input.ContractType)) || string.IsNullOrWhiteSpace(input.ContractType))
{ {
throw Oops.Bah("请填写模板编码、模板名称和合同类型"); throw Oops.Bah("请填写模板编码、模板名称和合同类型");
@@ -133,12 +133,11 @@ public class OnboardingDepartmentService : DbRepository<ObDepartment>, IOnboardi
/// </summary> /// </summary>
private static void NormalizeInput(OnboardingDepartmentAddInput input) private static void NormalizeInput(OnboardingDepartmentAddInput input)
{ {
input.DepartmentCode = input.DepartmentCode?.Trim(); input.DepartmentCode = OnboardingCodeValidator.Normalize(input.DepartmentCode, "部门编码", 50);
input.DepartmentName = input.DepartmentName?.Trim(); input.DepartmentName = input.DepartmentName?.Trim();
if (string.IsNullOrWhiteSpace(input.DepartmentCode) if (string.IsNullOrWhiteSpace(input.DepartmentName))
|| string.IsNullOrWhiteSpace(input.DepartmentName))
{ {
throw Oops.Bah("请填写部门编码和部门名称"); throw Oops.Bah("请填写部门名称");
} }
if (input.Status != "enable" && input.Status != "disable") if (input.Status != "enable" && input.Status != "disable")
@@ -138,7 +138,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
{ {
var employee = await _db.Queryable<ObEmployee>() var employee = await _db.Queryable<ObEmployee>()
.InSingleAsync(task.EmployeeId.Value) .InSingleAsync(task.EmployeeId.Value)
?? throw Oops.Bah("正式员工档案不存在,不能发起电子签"); ?? throw Oops.Bah("员工档案不存在,不能发起电子签");
if (string.IsNullOrWhiteSpace(employee.EmployeeName)) if (string.IsNullOrWhiteSpace(employee.EmployeeName))
throw Oops.Bah("正式员工姓名为空,不能发起电子签"); throw Oops.Bah("正式员工姓名为空,不能发起电子签");
if (string.IsNullOrWhiteSpace(employee.Phone)) if (string.IsNullOrWhiteSpace(employee.Phone))
@@ -1417,7 +1417,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
} }
if (differentFields.Count > 0) if (differentFields.Count > 0)
throw Oops.Bah($"正式员工档案的{string.Join("、", differentFields)}与审核通过的入职登记资料不一致,请先核对并修正后再发起电子签"); throw Oops.Bah($"员工档案的{string.Join("、", differentFields)}与审核通过的入职登记资料不一致,请先核对并修正后再发起电子签");
} }
private static string NormalizeIdentityValue(string value) private static string NormalizeIdentityValue(string value)
@@ -1468,7 +1468,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
private static void ValidateTaskCanSign(ObOnboardingTask task) private static void ValidateTaskCanSign(ObOnboardingTask task)
{ {
if (!task.EmployeeId.HasValue) if (!task.EmployeeId.HasValue)
throw Oops.Bah("入职任务尚未生成正式员工档案,不能发起电子签"); throw Oops.Bah("入职任务尚未生成员工档案,不能发起电子签");
if (task.TaskStatus != OnboardingTaskStatus.Approved if (task.TaskStatus != OnboardingTaskStatus.Approved
&& task.TaskStatus != OnboardingTaskStatus.Archived) && task.TaskStatus != OnboardingTaskStatus.Archived)
{ {
@@ -1527,7 +1527,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
bool processAllowed) bool processAllowed)
{ {
if (!task.EmployeeId.HasValue) if (!task.EmployeeId.HasValue)
return "审核通过并生成正式员工档案后才可发起电子签"; return "审核通过并生成员工档案后才可发起电子签";
if (!taskAllowed) if (!taskAllowed)
return "当前入职任务状态不能发起电子签"; return "当前入职任务状态不能发起电子签";
if (!processAllowed) if (!processAllowed)
@@ -1542,7 +1542,7 @@ public class OnboardingElectronicSignService : IOnboardingElectronicSignService
bool reinitiateAllowed) bool reinitiateAllowed)
{ {
if (!task.EmployeeId.HasValue) if (!task.EmployeeId.HasValue)
return "审核通过并生成正式员工档案后才可重新发起电子签"; return "审核通过并生成员工档案后才可重新发起电子签";
if (!taskAllowed) if (!taskAllowed)
return "当前入职任务状态不能重新发起电子签"; return "当前入职任务状态不能重新发起电子签";
if (process == null) if (process == null)
@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace SimpleAdmin.Application; namespace SimpleAdmin.Application;
/// <summary> /// <summary>
@@ -29,6 +31,84 @@ public class OnboardingEmployeePageInput : BasePageInput
/// 岗位 Id。 /// 岗位 Id。
/// </summary> /// </summary>
public long? PositionId { get; set; } 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> /// <summary>
@@ -56,6 +136,12 @@ public class OnboardingEmployeeDetailOutput
/// </summary> /// </summary>
public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>(); public List<ObTaskContract> Contracts { get; set; } = new List<ObTaskContract>();
/// <summary>
/// 审核建档时冻结的员工合同业务快照,包含常用字段及全部自定义变量。
/// </summary>
public List<ObEmployeeContractSnapshot> ContractSnapshots { get; set; }
= new List<ObEmployeeContractSnapshot>();
/// <summary> /// <summary>
/// 员工入职时上传的身份证、学历证明、体检报告等资料。 /// 员工入职时上传的身份证、学历证明、体检报告等资料。
/// </summary> /// </summary>
@@ -71,4 +157,10 @@ public class OnboardingEmployeeDetailOutput
/// 提交与审核记录列表。 /// 提交与审核记录列表。
/// </summary> /// </summary>
public List<ObTaskAudit> Audits { get; set; } = new List<ObTaskAudit>(); 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; namespace SimpleAdmin.Application;
/// <summary> /// <summary>
/// 定义正式员工档案查询业务能力。 /// 定义员工档案查询和人事维护业务能力。
/// </summary> /// </summary>
public interface IOnboardingEmployeeService : ITransient public interface IOnboardingEmployeeService : ITransient
{ {
@@ -10,6 +10,22 @@ public interface IOnboardingEmployeeService : ITransient
/// </summary> /// </summary>
Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input); Task<SqlSugarPagedList<ObEmployee>> Page(OnboardingEmployeePageInput input);
/// <summary> /// <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> /// </summary>
Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input); Task<OnboardingEmployeeDetailOutput> Detail(BaseIdInput input);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 定义正式员工合同快照归档能力。
/// </summary>
public interface IOnboardingEmployeeContractSnapshotService : ITransient
{
/// <summary>冻结一名员工来源任务中的全部合同和变量。</summary>
Task Build(long employeeId, long taskId);
}
@@ -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;
}
}
@@ -27,6 +27,11 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
/// </summary> /// </summary>
public async Task SaveFormField(OnboardingFormFieldSaveInput input) 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>() var fieldCodeQuery = _db.Queryable<ObFormField>()
.Where(x => x.FieldCode == input.FieldCode); .Where(x => x.FieldCode == input.FieldCode);
if (input.Id.HasValue) if (input.Id.HasValue)
@@ -115,6 +120,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
/// </summary> /// </summary>
public async Task SaveMaterialType(OnboardingMaterialTypeSaveInput input) 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) if (input.MaxFileCount <= 0)
throw Oops.Bah("最大文件数量必须大于0"); throw Oops.Bah("最大文件数量必须大于0");
@@ -169,4 +178,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync(); await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
} }
} }
@@ -92,12 +92,22 @@ public class OnboardingTemplateService : IOnboardingTemplateService
NormalizeAndValidateInput(input); NormalizeAndValidateInput(input);
await ValidateReferences(input); await ValidateReferences(input);
var templateId = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); var codeOwner = await _db.Queryable<ObOnboardingTemplate>()
var codeExists = await _db.Queryable<ObOnboardingTemplate>() .ClearFilter()
.AnyAsync(item => item.TemplateCode == input.TemplateCode && item.Id != templateId); .FirstAsync(item => item.TemplateCode == input.TemplateCode
if (codeExists) && (!input.Id.HasValue || item.Id != input.Id.Value));
if (codeOwner != null && !codeOwner.IsDelete)
throw Oops.Bah("入职模板编码已存在"); 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 var entity = new ObOnboardingTemplate
{ {
Id = templateId, Id = templateId,
@@ -106,15 +116,17 @@ public class OnboardingTemplateService : IOnboardingTemplateService
CompanyId = input.CompanyId, CompanyId = input.CompanyId,
DepartmentId = input.DepartmentId, DepartmentId = input.DepartmentId,
Description = input.Description, Description = input.Description,
Status = input.Status Status = input.Status,
IsDelete = false
}; };
await _db.Ado.BeginTranAsync(); await _db.Ado.BeginTranAsync();
try 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) if (!exists)
throw Oops.Bah("入职模板不存在"); throw Oops.Bah("入职模板不存在");
await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
@@ -189,7 +201,10 @@ public class OnboardingTemplateService : IOnboardingTemplateService
private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input) private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input)
{ {
input.TemplateCode = input.TemplateCode?.Trim(); input.TemplateCode = OnboardingCodeValidator.Normalize(
input.TemplateCode,
"入职模板编码",
100);
input.TemplateName = input.TemplateName?.Trim(); input.TemplateName = input.TemplateName?.Trim();
input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>(); input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List<long>();
input.Contracts ??= new List<OnboardingTemplateContractInput>(); input.Contracts ??= new List<OnboardingTemplateContractInput>();
@@ -208,8 +223,8 @@ public class OnboardingTemplateService : IOnboardingTemplateService
.Select(group => group.First()) .Select(group => group.First())
.ToList() ?? new List<OnboardingTemplateMaterialInput>(); .ToList() ?? new List<OnboardingTemplateMaterialInput>();
if (string.IsNullOrWhiteSpace(input.TemplateCode) || string.IsNullOrWhiteSpace(input.TemplateName)) if (string.IsNullOrWhiteSpace(input.TemplateName))
throw Oops.Bah("请填写入职模板编码和名称"); throw Oops.Bah("请填写入职模板名称");
if (input.CompanyId <= 0) if (input.CompanyId <= 0)
throw Oops.Bah("请选择所属公司"); throw Oops.Bah("请选择所属公司");
if (input.DepartmentId <= 0) if (input.DepartmentId <= 0)
@@ -168,12 +168,11 @@ public class OnboardingPositionService : DbRepository<ObPosition>, IOnboardingPo
/// </summary> /// </summary>
private static void NormalizeInput(OnboardingPositionAddInput input) private static void NormalizeInput(OnboardingPositionAddInput input)
{ {
input.PositionCode = input.PositionCode?.Trim(); input.PositionCode = OnboardingCodeValidator.Normalize(input.PositionCode, "岗位编码", 50);
input.PositionName = input.PositionName?.Trim(); input.PositionName = input.PositionName?.Trim();
if (string.IsNullOrWhiteSpace(input.PositionCode) if (string.IsNullOrWhiteSpace(input.PositionName))
|| string.IsNullOrWhiteSpace(input.PositionName))
{ {
throw Oops.Bah("请填写岗位编码和岗位名称"); throw Oops.Bah("请填写岗位名称");
} }
if (input.Status != "enable" && input.Status != "disable") if (input.Status != "enable" && input.Status != "disable")
@@ -25,6 +25,11 @@ public class OnboardingTaskCreateInput
[IdNotNull(ErrorMessage = "岗位Id不能为空")] [IdNotNull(ErrorMessage = "岗位Id不能为空")]
public long PositionId { get; set; } public long PositionId { get; set; }
/// <summary>
/// 用工类型:full_time 正式用工,part_time 兼职用工。
/// </summary>
public string EmploymentType { get; set; } = EmployeeEmploymentType.FullTime;
/// <summary> /// <summary>
/// 本次任务使用的入职模板 Id。 /// 本次任务使用的入职模板 Id。
/// </summary> /// </summary>
@@ -26,6 +26,8 @@ public class OnboardingTaskService : IOnboardingTaskService
if (string.IsNullOrWhiteSpace(input.Remark)) if (string.IsNullOrWhiteSpace(input.Remark))
throw Oops.Bah("请填写人事备注"); throw Oops.Bah("请填写人事备注");
ValidateEmploymentType(input.EmploymentType);
var companyEnabled = await _db.Queryable<ObCompany>() var companyEnabled = await _db.Queryable<ObCompany>()
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable"); .AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
if (!companyEnabled) if (!companyEnabled)
@@ -123,6 +125,7 @@ public class OnboardingTaskService : IOnboardingTaskService
CompanyId = input.CompanyId, CompanyId = input.CompanyId,
DepartmentId = input.DepartmentId, DepartmentId = input.DepartmentId,
PositionId = input.PositionId, PositionId = input.PositionId,
EmploymentType = input.EmploymentType,
OnboardingTemplateId = onboardingTemplate.Id, OnboardingTemplateId = onboardingTemplate.Id,
AssignedReviewerUserId = input.AssignedReviewerUserId, AssignedReviewerUserId = input.AssignedReviewerUserId,
LinkExpireAt = expiresAt, LinkExpireAt = expiresAt,
@@ -366,6 +369,9 @@ public class OnboardingTaskService : IOnboardingTaskService
CompanyId = sourceTask.CompanyId, CompanyId = sourceTask.CompanyId,
DepartmentId = sourceTask.DepartmentId, DepartmentId = sourceTask.DepartmentId,
PositionId = sourceTask.PositionId, PositionId = sourceTask.PositionId,
EmploymentType = string.IsNullOrWhiteSpace(sourceTask.EmploymentType)
? EmployeeEmploymentType.FullTime
: sourceTask.EmploymentType,
OnboardingTemplateId = sourceTask.OnboardingTemplateId, OnboardingTemplateId = sourceTask.OnboardingTemplateId,
AssignedReviewerUserId = sourceTask.AssignedReviewerUserId, AssignedReviewerUserId = sourceTask.AssignedReviewerUserId,
LinkExpireAt = expiresAt, LinkExpireAt = expiresAt,
@@ -553,4 +559,13 @@ public class OnboardingTaskService : IOnboardingTaskService
if (endDate.Date < startDate.Date) if (endDate.Date < startDate.Date)
throw Oops.Bah($"{rangeName}结束日期不能早于开始日期"); throw Oops.Bah($"{rangeName}结束日期不能早于开始日期");
} }
private static void ValidateEmploymentType(string employmentType)
{
if (employmentType != EmployeeEmploymentType.FullTime
&& employmentType != EmployeeEmploymentType.PartTime)
{
throw Oops.Bah("用工类型不正确,请选择正式员工或兼职员工");
}
}
} }
@@ -2,37 +2,112 @@ using System.ComponentModel;
namespace SimpleAdmin.Web.Core; namespace SimpleAdmin.Web.Core;
/// <summary> /// <summary>
/// 提供正式员工档案相关 HTTP 接口。 /// 提供员工档案及人事维护相关 HTTP 接口。
/// </summary> /// </summary>
[ApiDescriptionSettings("Application", Tag = "入职正式员工档案")] [ApiDescriptionSettings("Application", Tag = "入职员工档案")]
[Route("biz/onboarding/employee")] [Route("biz/onboarding/employee")]
[RolePermission] [RolePermission]
public class OnboardingEmployeeController public class OnboardingEmployeeController
{ {
private readonly IOnboardingEmployeeService _service; private readonly IOnboardingEmployeeService _service;
private readonly IFileService _fileService;
public OnboardingEmployeeController(IOnboardingEmployeeService service) public OnboardingEmployeeController(
IOnboardingEmployeeService service,
IFileService fileService)
{ {
_service = service; _service = service;
_fileService = fileService;
} }
/// <summary> /// <summary>
/// 分页查询正式员工档案。 /// 分页查询正式员工档案。
/// </summary> /// </summary>
[HttpGet("page")] [HttpGet("page")]
[DisplayName("分页查询正式员工档案")] [DisplayName("分页查询员工档案")]
public async Task<dynamic> Page([FromQuery] OnboardingEmployeePageInput input) public async Task<dynamic> Page([FromQuery] OnboardingEmployeePageInput input)
{ {
return await _service.Page(input); return await _service.Page(input);
} }
/// <summary>
/// 按当前列表筛选条件导出员工基本信息和合同快照。
/// </summary>
[HttpPost("export")]
[DisplayName("导出员工明细")]
public async Task<FileContentResult> Export([FromBody] OnboardingEmployeePageInput input)
{
var file = await _service.Export(input);
return new FileContentResult(
file.FileBytes,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = file.FileName
};
}
/// <summary> /// <summary>
/// 查询正式员工详情及来源记录。 /// 查询正式员工详情及来源记录。
/// </summary> /// </summary>
[HttpGet("detail")] [HttpGet("detail")]
[DisplayName("查询正式员工详情")] [DisplayName("查询员工档案详情")]
public async Task<dynamic> Detail([FromQuery] BaseIdInput input) public async Task<dynamic> Detail([FromQuery] BaseIdInput input)
{ {
return await _service.Detail(input); return await _service.Detail(input);
} }
/// <summary>
/// 编辑员工用工、社保及合同性质等人事信息。
/// </summary>
[HttpPost("hr-info/update")]
[DisplayName("编辑员工人事信息")]
public async Task UpdateHrInfo([FromBody] OnboardingEmployeeHrUpdateInput input)
{
await _service.UpdateHrInfo(input);
}
/// <summary>
/// 上传转正等员工人事材料。
/// </summary>
[HttpPost("attachment/upload")]
[Consumes("multipart/form-data")]
[RequestSizeLimit(20 * 1024 * 1024)]
[DisplayName("上传员工人事材料")]
public async Task<long> UploadAttachment([FromForm] IFormFile file)
{
if (file == null || file.Length == 0)
throw Oops.Bah("请选择需要上传的材料文件");
var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".pdf", ".doc", ".docx", ".xls", ".xlsx",
".jpg", ".jpeg", ".png", ".webp", ".zip"
};
var extension = Path.GetExtension(file.FileName);
if (!allowedExtensions.Contains(extension))
throw Oops.Bah("仅支持 PDF、Word、Excel、图片或 ZIP 文件");
return await _fileService.UploadFile(SysDictConst.FILE_ENGINE_ALIYUN_OSS, file);
}
/// <summary>
/// 人工办理员工转正。
/// </summary>
[HttpPost("regularization/confirm")]
[DisplayName("办理员工转正")]
public async Task Regularize([FromBody] OnboardingEmployeeRegularizeInput input)
{
await _service.Regularize(input);
}
/// <summary>
/// 撤销员工转正。
/// </summary>
[HttpPost("regularization/revoke")]
[DisplayName("撤销员工转正")]
public async Task RevokeRegularization(
[FromBody] OnboardingEmployeeRegularizationRevokeInput input)
{
await _service.RevokeRegularization(input);
}
} }
@@ -0,0 +1,163 @@
-- 员工合同快照:一名正式员工的一份任务合同对应一条记录。
-- 常用字段独立存储用于筛选导出,VariablesJson 按 VariableCode 保存全部标准及自定义变量。
-- 适用数据库:MySQL 8.0+
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `ob_employee_contract_snapshot` (
`Id` BIGINT NOT NULL COMMENT '主键',
`EmployeeId` BIGINT NOT NULL COMMENT '正式员工Id',
`TaskId` BIGINT NOT NULL COMMENT '来源入职任务Id',
`TaskContractId` BIGINT NOT NULL COMMENT '来源任务合同Id',
`TemplateId` BIGINT NOT NULL COMMENT '合同模板Id',
`TemplateVersionId` BIGINT NOT NULL COMMENT '合同模板版本Id',
`ContractName` VARCHAR(300) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '合同名称快照',
`ContractType` VARCHAR(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合同类型快照',
`ContractStartDate` DATE DEFAULT NULL COMMENT '合同开始日期',
`ContractEndDate` DATE DEFAULT NULL COMMENT '合同结束日期',
`ContractTerm` VARCHAR(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合同期限原始文本',
`ProbationStartDate` DATE DEFAULT NULL COMMENT '试用期开始日期',
`ProbationEndDate` DATE DEFAULT NULL COMMENT '试用期结束日期',
`ProbationTerm` VARCHAR(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '试用期期限原始文本',
`ProbationSalary` DECIMAL(18,2) DEFAULT NULL COMMENT '试用薪资,对应hr.ProbationSalary,不自动推算',
`BaseSalary` DECIMAL(18,2) DEFAULT NULL COMMENT '基本工资',
`PositionSalary` DECIMAL(18,2) DEFAULT NULL COMMENT '岗位工资',
`PerformanceAllowance` DECIMAL(18,2) DEFAULT NULL COMMENT '绩效津贴',
`AttendanceBonus` DECIMAL(18,2) DEFAULT NULL COMMENT '全勤奖',
`RegularSalary` DECIMAL(18,2) DEFAULT NULL COMMENT '转正后薪资',
`WorkCity` VARCHAR(300) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工作地点或城市',
`PositionName` VARCHAR(300) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合同岗位名称',
`EmployeeNo` VARCHAR(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合同员工工号',
`VariablesJson` JSON NOT NULL COMMENT '按VariableCode索引的完整合同变量快照',
`SnapshotVersion` INT NOT NULL DEFAULT 1 COMMENT '快照结构版本',
`SnapshotAt` DATETIME(3) NOT NULL COMMENT '快照生成时间',
`ExtJson` LONGTEXT COLLATE utf8mb4_unicode_ci COMMENT '扩展信息',
`Status` VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'enable' COMMENT '记录状态',
`CreateTime` DATETIME(3) DEFAULT NULL COMMENT '创建时间',
`UpdateTime` DATETIME(3) DEFAULT NULL COMMENT '更新时间',
`CreateUserId` BIGINT DEFAULT NULL COMMENT '创建者Id',
`UpdateUserId` BIGINT DEFAULT NULL COMMENT '修改者Id',
`CreateUser` VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
`UpdateUser` VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
`IsDelete` TINYINT(1) DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_ob_employee_contract_snapshot_contract` (`TaskContractId`),
KEY `idx_ob_employee_contract_snapshot_employee` (`EmployeeId`, `IsDelete`),
KEY `idx_ob_employee_contract_snapshot_task` (`TaskId`, `IsDelete`),
KEY `idx_ob_employee_contract_snapshot_dates` (`ContractStartDate`, `ContractEndDate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='正式员工合同业务快照';
-- 为已经审核建档且仍保留任务合同变量的员工补建快照。
INSERT IGNORE INTO `ob_employee_contract_snapshot` (
`Id`, `EmployeeId`, `TaskId`, `TaskContractId`, `TemplateId`, `TemplateVersionId`,
`ContractName`, `ContractType`, `ContractStartDate`, `ContractEndDate`, `ContractTerm`,
`ProbationStartDate`, `ProbationEndDate`, `ProbationTerm`, `ProbationSalary`,
`BaseSalary`, `PositionSalary`, `PerformanceAllowance`, `AttendanceBonus`, `RegularSalary`,
`WorkCity`, `PositionName`, `EmployeeNo`, `VariablesJson`, `SnapshotVersion`, `SnapshotAt`,
`Status`, `CreateTime`, `CreateUserId`, `CreateUser`, `IsDelete`
)
SELECT
contract.`Id`,
task.`EmployeeId`,
task.`Id`,
contract.`Id`,
contract.`TemplateId`,
contract.`TemplateVersionId`,
contract.`ContractName`,
contract.`ContractType`,
CASE
WHEN TRIM(variable.`ContractStartDateText`) REGEXP '^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$'
THEN STR_TO_DATE(TRIM(variable.`ContractStartDateText`), '%Y-%m-%d')
ELSE NULL
END,
CASE
WHEN TRIM(variable.`ContractEndDateText`) REGEXP '^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$'
THEN STR_TO_DATE(TRIM(variable.`ContractEndDateText`), '%Y-%m-%d')
ELSE NULL
END,
variable.`ContractTerm`,
CASE
WHEN TRIM(variable.`ProbationStartDateText`) REGEXP '^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$'
THEN STR_TO_DATE(TRIM(variable.`ProbationStartDateText`), '%Y-%m-%d')
ELSE NULL
END,
CASE
WHEN TRIM(variable.`ProbationEndDateText`) REGEXP '^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$'
THEN STR_TO_DATE(TRIM(variable.`ProbationEndDateText`), '%Y-%m-%d')
ELSE NULL
END,
variable.`ProbationTerm`,
CASE WHEN TRIM(variable.`ProbationSalaryText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`ProbationSalaryText`) AS DECIMAL(18,2)) ELSE NULL END,
CASE WHEN TRIM(variable.`BaseSalaryText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`BaseSalaryText`) AS DECIMAL(18,2)) ELSE NULL END,
CASE WHEN TRIM(variable.`PositionSalaryText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`PositionSalaryText`) AS DECIMAL(18,2)) ELSE NULL END,
CASE WHEN TRIM(variable.`PerformanceAllowanceText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`PerformanceAllowanceText`) AS DECIMAL(18,2)) ELSE NULL END,
CASE WHEN TRIM(variable.`AttendanceBonusText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`AttendanceBonusText`) AS DECIMAL(18,2)) ELSE NULL END,
CASE WHEN TRIM(variable.`RegularSalaryText`) REGEXP '^-?[0-9]+([.][0-9]+)?$'
THEN CAST(TRIM(variable.`RegularSalaryText`) AS DECIMAL(18,2)) ELSE NULL END,
variable.`WorkCity`,
variable.`PositionName`,
variable.`EmployeeNo`,
COALESCE(variable.`VariablesJson`, JSON_OBJECT()),
1,
COALESCE(task.`AuditedAt`, employee.`CreateTime`, NOW(3)),
'enable',
COALESCE(employee.`CreateTime`, NOW(3)),
employee.`CreateUserId`,
employee.`CreateUser`,
0
FROM `ob_onboarding_task` task
INNER JOIN `ob_employee` employee
ON employee.`Id` = task.`EmployeeId`
INNER JOIN `ob_task_contract` contract
ON contract.`TaskId` = task.`Id`
LEFT JOIN (
SELECT
row_value.`TaskContractId`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ContractStartDate' THEN row_value.`VariableValue` END) AS `ContractStartDateText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ContractEndDate' THEN row_value.`VariableValue` END) AS `ContractEndDateText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ContractTerm' THEN row_value.`VariableValue` END) AS `ContractTerm`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ProbationStartDate' THEN row_value.`VariableValue` END) AS `ProbationStartDateText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ProbationEndDate' THEN row_value.`VariableValue` END) AS `ProbationEndDateText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ProbationTerm' THEN row_value.`VariableValue` END) AS `ProbationTerm`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.ProbationSalary' THEN row_value.`VariableValue` END) AS `ProbationSalaryText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.BaseSalary' THEN row_value.`VariableValue` END) AS `BaseSalaryText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.PositionSalary' THEN row_value.`VariableValue` END) AS `PositionSalaryText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.PerformanceAllowance' THEN row_value.`VariableValue` END) AS `PerformanceAllowanceText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.AttendanceBonus' THEN row_value.`VariableValue` END) AS `AttendanceBonusText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.RegularSalary' THEN row_value.`VariableValue` END) AS `RegularSalaryText`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.WorkCity' THEN row_value.`VariableValue` END) AS `WorkCity`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.PositionName' THEN row_value.`VariableValue` END) AS `PositionName`,
MAX(CASE WHEN row_value.`VariableCode` = 'hr.EmployeeNo' THEN row_value.`VariableValue` END) AS `EmployeeNo`,
JSON_OBJECTAGG(
row_value.`VariableCode`,
JSON_OBJECT(
'variableName', row_value.`VariableName`,
'variableType', row_value.`VariableType`,
'dataType', row_value.`DataType`,
'variableValue', row_value.`VariableValue`,
'sourceType', row_value.`SourceType`,
'isManualOverride', row_value.`IsManualOverride`,
'sortCode', row_value.`SortCode`
)
) AS `VariablesJson`
FROM `ob_task_contract_variable` row_value
WHERE row_value.`IsDelete` = 0
GROUP BY row_value.`TaskContractId`
) variable
ON variable.`TaskContractId` = contract.`Id`
WHERE task.`EmployeeId` IS NOT NULL
AND task.`IsDelete` = 0
AND employee.`IsDelete` = 0
AND contract.`IsDelete` = 0;
SELECT
COUNT(*) AS `员工合同快照数量`,
COUNT(DISTINCT `EmployeeId`) AS `已归档员工数量`,
COUNT(DISTINCT `TaskContractId`) AS `已归档合同数量`
FROM `ob_employee_contract_snapshot`
WHERE `IsDelete` = 0;
@@ -0,0 +1,172 @@
-- 员工用工类型、人工转正、社保状态及人事变更材料。
-- 适用数据库:MySQL 8.0+
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS ob_upgrade_employee_hr_management;
DELIMITER $$
CREATE PROCEDURE ob_upgrade_employee_hr_management()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_onboarding_task'
AND COLUMN_NAME = 'EmploymentType'
) THEN
ALTER TABLE ob_onboarding_task
ADD COLUMN EmploymentType VARCHAR(20) COLLATE utf8mb4_unicode_ci
NOT NULL DEFAULT 'full_time' COMMENT '用工类型:full_time正式、part_time兼职'
AFTER PositionId;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee'
AND COLUMN_NAME = 'EmploymentType'
) THEN
ALTER TABLE ob_employee
ADD COLUMN EmploymentType VARCHAR(20) COLLATE utf8mb4_unicode_ci
NOT NULL DEFAULT 'full_time' COMMENT '用工类型:full_time正式、part_time兼职'
AFTER PositionId;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee'
AND COLUMN_NAME = 'RegularizationStatus'
) THEN
ALTER TABLE ob_employee
ADD COLUMN RegularizationStatus VARCHAR(20) COLLATE utf8mb4_unicode_ci
NOT NULL DEFAULT 'probation' COMMENT '转正状态:probation、regular、not_applicable'
AFTER EmploymentType;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee'
AND COLUMN_NAME = 'SocialInsuranceStatus'
) THEN
ALTER TABLE ob_employee
ADD COLUMN SocialInsuranceStatus VARCHAR(20) COLLATE utf8mb4_unicode_ci
NOT NULL DEFAULT 'unknown' COMMENT '社保状态:unknown、not_purchased、purchased、stopped、not_applicable'
AFTER RegularizationStatus;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee_contract_snapshot'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee_contract_snapshot'
AND COLUMN_NAME = 'ProbationSalary'
) THEN
ALTER TABLE ob_employee_contract_snapshot
ADD COLUMN ProbationSalary DECIMAL(18,2) DEFAULT NULL
COMMENT '试用薪资,对应hr.ProbationSalary,不自动推算'
AFTER ProbationTerm;
END IF;
END$$
DELIMITER ;
CALL ob_upgrade_employee_hr_management();
DROP PROCEDURE IF EXISTS ob_upgrade_employee_hr_management;
-- 历史任务及员工默认按正式用工处理;历史转正日期仅用于一次性初始化状态。
UPDATE ob_onboarding_task
SET EmploymentType = 'full_time'
WHERE EmploymentType IS NULL OR EmploymentType NOT IN ('full_time', 'part_time');
UPDATE ob_employee
SET
EmploymentType = CASE
WHEN EmploymentType = 'part_time' THEN 'part_time'
ELSE 'full_time'
END,
RegularizationStatus = CASE
WHEN EmploymentType = 'part_time' THEN 'not_applicable'
WHEN RegularizationDate IS NOT NULL THEN 'regular'
ELSE 'probation'
END,
SocialInsuranceStatus = CASE
WHEN SocialInsuranceStatus IN (
'unknown', 'not_purchased', 'purchased', 'stopped', 'not_applicable'
) THEN SocialInsuranceStatus
ELSE 'unknown'
END;
CREATE TABLE IF NOT EXISTS ob_employee_change_record (
Id BIGINT NOT NULL COMMENT '主键',
EmployeeId BIGINT NOT NULL COMMENT '员工Id',
ChangeType VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '变更类型',
EffectiveDate DATE DEFAULT NULL COMMENT '业务生效日期',
BeforeJson LONGTEXT COLLATE utf8mb4_unicode_ci COMMENT '变更前快照',
AfterJson LONGTEXT COLLATE utf8mb4_unicode_ci COMMENT '变更后快照',
Remark VARCHAR(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
ExtJson LONGTEXT COLLATE utf8mb4_unicode_ci COMMENT '扩展信息',
Status VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'enable' COMMENT '记录状态',
CreateTime DATETIME(3) DEFAULT NULL COMMENT '创建时间',
UpdateTime DATETIME(3) DEFAULT NULL COMMENT '更新时间',
CreateUserId BIGINT DEFAULT NULL COMMENT '创建者Id',
UpdateUserId BIGINT DEFAULT NULL COMMENT '修改者Id',
CreateUser VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
UpdateUser VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
IsDelete TINYINT(1) DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (Id),
KEY idx_ob_employee_change_record_employee (EmployeeId, IsDelete, CreateTime),
KEY idx_ob_employee_change_record_type (ChangeType, EffectiveDate)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工人事信息变更记录';
CREATE TABLE IF NOT EXISTS ob_employee_attachment (
Id BIGINT NOT NULL COMMENT '主键',
EmployeeId BIGINT NOT NULL COMMENT '员工Id',
ChangeRecordId BIGINT DEFAULT NULL COMMENT '关联人事变更记录Id',
BusinessType VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '附件业务类型',
FileId BIGINT NOT NULL COMMENT '系统文件Id',
FileName VARCHAR(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '上传时文件名快照',
UploadedAt DATETIME(3) NOT NULL COMMENT '上传关联时间',
ExtJson LONGTEXT COLLATE utf8mb4_unicode_ci COMMENT '扩展信息',
Status VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'enable' COMMENT '记录状态',
CreateTime DATETIME(3) DEFAULT NULL COMMENT '创建时间',
UpdateTime DATETIME(3) DEFAULT NULL COMMENT '更新时间',
CreateUserId BIGINT DEFAULT NULL COMMENT '创建者Id',
UpdateUserId BIGINT DEFAULT NULL COMMENT '修改者Id',
CreateUser VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人',
UpdateUser VARCHAR(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人',
IsDelete TINYINT(1) DEFAULT 0 COMMENT '软删除',
PRIMARY KEY (Id),
KEY idx_ob_employee_attachment_employee (EmployeeId, BusinessType, IsDelete),
KEY idx_ob_employee_attachment_change (ChangeRecordId, IsDelete),
KEY idx_ob_employee_attachment_file (FileId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工人事材料附件';
-- 标准试用薪资变量;已有同编码历史数据时恢复并统一为内置数值变量。
INSERT INTO ob_variable_definition (
Id, VariableCode, VariableName, VariableCategory, DataType,
IsBuiltIn, Required, SortCode, Remark, Status,
CreateTime, IsDelete
) VALUES (
841400000000115, 'hr.ProbationSalary', '试用薪资(元)', 'hr', 'number',
1, 0, 335, '合同约定试用期薪资,不自动推算', 'enable',
NOW(), 0
)
ON DUPLICATE KEY UPDATE
VariableName = VALUES(VariableName),
VariableCategory = 'hr',
DataType = 'number',
IsBuiltIn = 1,
SortCode = 335,
Remark = VALUES(Remark),
Status = 'enable',
IsDelete = 0;
SELECT
(SELECT COUNT(*) FROM ob_employee_change_record) AS '员工人事变更记录数',
(SELECT COUNT(*) FROM ob_employee_attachment) AS '员工人事附件数',
(SELECT COUNT(*) FROM ob_variable_definition
WHERE VariableCode = 'hr.ProbationSalary' AND IsDelete = 0) AS '试用薪资标准变量数';
@@ -49,6 +49,35 @@ BEGIN
START TRANSACTION; START TRANSACTION;
-- 一、删除任务运行数据及快照。 -- 一、删除任务运行数据及快照。
-- 兼容尚未执行员工人事管理迁移的旧数据库。
IF EXISTS (
SELECT 1
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee_attachment'
) THEN
DELETE FROM ob_employee_attachment;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ob_employee_change_record'
) THEN
DELETE FROM ob_employee_change_record;
END IF;
-- 兼容尚未执行员工合同快照迁移的旧数据库。
IF EXISTS (
SELECT 1
FROM `information_schema`.`TABLES`
WHERE `TABLE_SCHEMA` = DATABASE()
AND `TABLE_NAME` = 'ob_employee_contract_snapshot'
) THEN
SET @delete_employee_contract_snapshot_sql = 'DELETE FROM `ob_employee_contract_snapshot`';
PREPARE delete_employee_contract_snapshot_statement FROM @delete_employee_contract_snapshot_sql;
EXECUTE delete_employee_contract_snapshot_statement;
DEALLOCATE PREPARE delete_employee_contract_snapshot_statement;
END IF;
DELETE FROM `ob_task_operation_log`; DELETE FROM `ob_task_operation_log`;
DELETE FROM `ob_task_audit`; DELETE FROM `ob_task_audit`;
DELETE FROM `ob_task_material`; DELETE FROM `ob_task_material`;
@@ -43,6 +43,9 @@ BEGIN
'ob_electronic_sign_event', 'ob_electronic_sign_event',
'ob_electronic_sign_document', 'ob_electronic_sign_document',
'ob_electronic_sign_process', 'ob_electronic_sign_process',
'ob_employee_attachment',
'ob_employee_change_record',
'ob_employee_contract_snapshot',
'ob_task_operation_log', 'ob_task_operation_log',
'ob_task_audit', 'ob_task_audit',
'ob_task_material', 'ob_task_material',
@@ -61,6 +64,9 @@ BEGIN
'ob_electronic_sign_event', 'ob_electronic_sign_event',
'ob_electronic_sign_document', 'ob_electronic_sign_document',
'ob_electronic_sign_process', 'ob_electronic_sign_process',
'ob_employee_attachment',
'ob_employee_change_record',
'ob_employee_contract_snapshot',
'ob_task_operation_log', 'ob_task_operation_log',
'ob_task_audit', 'ob_task_audit',
'ob_task_material', 'ob_task_material',
@@ -166,6 +172,9 @@ BEGIN
'ob_electronic_sign_event', 'ob_electronic_sign_event',
'ob_electronic_sign_document', 'ob_electronic_sign_document',
'ob_electronic_sign_process', 'ob_electronic_sign_process',
'ob_employee_attachment',
'ob_employee_change_record',
'ob_employee_contract_snapshot',
'ob_task_operation_log', 'ob_task_operation_log',
'ob_task_audit', 'ob_task_audit',
'ob_task_material', 'ob_task_material',
@@ -199,6 +208,9 @@ WHERE `TABLE_SCHEMA` = DATABASE()
'ob_electronic_sign_event', 'ob_electronic_sign_event',
'ob_electronic_sign_document', 'ob_electronic_sign_document',
'ob_electronic_sign_process', 'ob_electronic_sign_process',
'ob_employee_attachment',
'ob_employee_change_record',
'ob_employee_contract_snapshot',
'ob_task_operation_log', 'ob_task_operation_log',
'ob_task_audit', 'ob_task_audit',
'ob_task_material', 'ob_task_material',