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;
/// <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();
@@ -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("请填写模板编码、模板名称和合同类型");
@@ -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")
@@ -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);
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>
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();
}
}
}
@@ -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("用工类型不正确,请选择正式员工或兼职员工");
}
}
}