From f004aec10f82ea3a76392f575faa5c5776afb1f8 Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Mon, 14 Sep 2026 15:59:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=85=B3bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingContractTemplateService.cs | 7 -- .../OnboardingEntryConfigService.cs | 18 ++--- .../OnboardingTaskLifecycleService.cs | 23 ++++++- .../OnboardingPublicSubmissionService.cs | 4 +- ...oardingEmployeeFieldRequirementSnapshot.cs | 68 +++++++++++++++++++ .../OnboardingEmployeeInfoValidator.cs | 64 ++++++++++++++--- .../PublicTask/OnboardingPublicTaskService.cs | 18 ++++- .../Onboarding/Task/OnboardingTaskService.cs | 29 ++++++++ ...14_employee_field_requirement_snapshot.sql | 28 ++++++++ 9 files changed, 230 insertions(+), 29 deletions(-) create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeFieldRequirementSnapshot.cs create mode 100644 database/migrations/20260914_employee_field_requirement_snapshot.sql diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs index 73c1083..6ebde40 100644 --- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs @@ -202,10 +202,6 @@ public class OnboardingContractTemplateService : DbRepository() - .Where(it => input.Ids.Contains(it.TemplateId)) - .ExecuteCommandAsync(); - await Context.Deleteable() .Where(item => input.Ids.Contains(item.ContractTemplateId)) .ExecuteCommandAsync(); @@ -245,9 +241,6 @@ public class OnboardingContractTemplateService : DbRepository() .Where(it => it.TemplateVersionId == version.Id) .ExecuteCommandAsync(); - await Context.Deleteable() - .Where(it => it.TemplateVersionId == version.Id) - .ExecuteCommandAsync(); await Context.Deleteable() .In(version.Id) .ExecuteCommandAsync(); diff --git a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs index a616fb2..3cf271d 100644 --- a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs @@ -112,12 +112,11 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService OnboardingPermissionCodes.EntryConfigManageAll, "入职字段"); - var isReferenced = await _db.Queryable() + + var isReferencedByTask = await _db.Queryable() .AnyAsync(x => input.Ids.Contains(x.FieldId)); - var isReferencedByTemplate = await _db.Queryable() - .AnyAsync(x => input.Ids.Contains(x.FieldId)); - if (isReferenced || isReferencedByTemplate) - throw Oops.Bah("字段已被入职模板引用,不能删除"); + if (isReferencedByTask) + throw Oops.Bah("字段已被入职任务引用,不能删除,可改为停用"); await _db.Deleteable().In(input.Ids).ExecuteCommandAsync(); } @@ -196,13 +195,16 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService OnboardingPermissionCodes.EntryConfigManageAll, "资料类型"); - var isReferenced = await _db.Queryable() - .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); var isReferencedByTemplate = await _db.Queryable() .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); - if (isReferenced || isReferencedByTemplate) + if (isReferencedByTemplate) throw Oops.Bah("资料类型已被入职模板引用,不能删除"); + var isReferencedByTask = await _db.Queryable() + .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); + if (isReferencedByTask) + throw Oops.Bah("资料类型已被入职任务引用,不能删除,可改为停用"); + var hasUploads = await _db.Queryable() .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); if (hasUploads) diff --git a/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs index 8d77dd6..1a19244 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs @@ -117,6 +117,28 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService .Select(x => x.Id) .ToListAsync(); + var signProcessIds = await _db.Queryable() + .Where(x => x.TaskId == task.Id) + .Select(x => x.Id) + .ToListAsync(); + + if (signProcessIds.Count > 0) + { + await _db.Deleteable() + .Where(x => x.SignProcessId.HasValue + && signProcessIds.Contains(x.SignProcessId.Value)) + .ExecuteCommandAsync(); + await _db.Deleteable() + .Where(x => signProcessIds.Contains(x.SignProcessId)) + .ExecuteCommandAsync(); + await _db.Deleteable() + .Where(x => signProcessIds.Contains(x.Id)) + .ExecuteCommandAsync(); + } + + await _db.Deleteable() + .Where(x => x.TaskId == task.Id) + .ExecuteCommandAsync(); if (taskContractIds.Count > 0) { await _db.Deleteable() @@ -130,7 +152,6 @@ public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); - await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); await _db.Deleteable().Where(x => x.TaskId == task.Id).ExecuteCommandAsync(); await _db.Deleteable().In(task.Id).ExecuteCommandAsync(); diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs index 845c950..54c442a 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs @@ -126,7 +126,9 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ ?? throw Oops.Bah("请先完整填写基本信息"); // 最终提交必须重新执行全量校验,不能只依赖员工手机端的页面校验。 - OnboardingEmployeeInfoValidator.ValidateAndNormalize(info); + var formRequirements = + await OnboardingEmployeeFieldRequirementSnapshot.GetForTask(_db, task.Id); + OnboardingEmployeeInfoValidator.ValidateAndNormalize(info, formRequirements); EnsureIdentityInformationConfirmed(input, info); await _db.Updateable(info).ExecuteCommandAsync(); diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeFieldRequirementSnapshot.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeFieldRequirementSnapshot.cs new file mode 100644 index 0000000..058a948 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeFieldRequirementSnapshot.cs @@ -0,0 +1,68 @@ +namespace SimpleAdmin.Application; + +/// +/// 生成并读取入职登记表内置员工字段的任务级必填快照。 +/// +internal static class OnboardingEmployeeFieldRequirementSnapshot +{ + private const string EmployeeVariablePrefix = "employee."; + + /// + /// 根据当前内置员工变量配置创建一份任务快照。 + /// + public static async Task> BuildFromCurrentDefinitions( + SqlSugarScope db, + long taskId) + { + var definitions = await db.Queryable() + .Where(item => item.IsBuiltIn + && item.VariableCategory == ContractVariableType.Employee) + .OrderBy(item => item.SortCode) + .OrderBy(item => item.Id) + .ToListAsync(); + + return definitions.Select(definition => new ObTaskFormRequirement + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = taskId, + FieldId = definition.Id, + FieldCode = definition.VariableCode, + FieldName = definition.VariableName, + DataType = definition.DataType, + Required = definition.Required, + SortCode = definition.SortCode + }).ToList(); + } + + /// + /// 读取任务冻结配置;历史任务没有员工字段快照时使用当前配置兼容。 + /// + public static async Task> GetForTask( + SqlSugarScope db, + long taskId) + { + var snapshots = await db.Queryable() + .Where(item => item.TaskId == taskId) + .OrderBy(item => item.SortCode) + .ToListAsync(); + if (snapshots.Any(IsEmployeeField)) + return snapshots; + + var compatibilitySnapshots = await BuildFromCurrentDefinitions(db, taskId); + if (snapshots.Count == 0) + return compatibilitySnapshots; + + snapshots.AddRange(compatibilitySnapshots); + return snapshots.OrderBy(item => item.SortCode).ToList(); + } + + /// + /// 判断快照是否对应入职登记表内置员工字段。 + /// + public static bool IsEmployeeField(ObTaskFormRequirement requirement) + { + return requirement?.FieldCode?.StartsWith( + EmployeeVariablePrefix, + StringComparison.OrdinalIgnoreCase) == true; + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeInfoValidator.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeInfoValidator.cs index fc836a2..20733ff 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeInfoValidator.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingEmployeeInfoValidator.cs @@ -16,9 +16,11 @@ internal static class OnboardingEmployeeInfoValidator }; /// - /// 规范化文本,并校验固定登记表字段的格式。 + /// 规范化文本,并按照任务冻结配置校验固定登记表必填项及字段格式。 /// - public static void ValidateAndNormalize(ObTaskEmployeeInfo input) + public static void ValidateAndNormalize( + ObTaskEmployeeInfo input, + IReadOnlyCollection fieldRequirements = null) { input.EmployeeName = input.EmployeeName?.Trim(); input.Phone = input.Phone?.Trim(); @@ -26,20 +28,27 @@ internal static class OnboardingEmployeeInfoValidator input.IdCardNumber = input.IdCardNumber?.Trim().ToUpperInvariant(); input.BankCardNumber = global::System.Text.RegularExpressions.Regex.Replace( input.BankCardNumber ?? string.Empty, - "[\\s-]", + @"[\s-]", string.Empty); input.BankName = input.BankName?.Trim(); input.BankAccountName = input.EmployeeName; + ValidateRequiredFields(input, fieldRequirements); + + // 电子签约实名认证始终依赖以下三项,不能通过后台配置关闭。 if (string.IsNullOrWhiteSpace(input.EmployeeName) || input.EmployeeName.Length > 100) throw Oops.Bah("员工姓名不能为空且不能超过 100 个字符"); - if (input.Gender != "男" && input.Gender != "女") + if (!string.IsNullOrWhiteSpace(input.Gender) + && input.Gender != "男" + && input.Gender != "女") + { throw Oops.Bah("请选择正确的性别"); + } if (!global::System.Text.RegularExpressions.Regex.IsMatch( input.Phone ?? string.Empty, - "^1[3-9]\\d{9}$")) + @"^1[3-9]\d{9}$")) { throw Oops.Bah("请输入正确的 11 位手机号码"); } @@ -51,10 +60,11 @@ internal static class OnboardingEmployeeInfoValidator ValidateOptionalEmail(input.Email); ValidateOptionalNumber(input.Height, "身高", 100M, 250M, "厘米"); ValidateOptionalNumber(input.Weight, "体重", 30M, 300M, "公斤"); - ValidateBankCard(input.BankCardNumber); + if (!string.IsNullOrWhiteSpace(input.BankCardNumber)) + ValidateBankCard(input.BankCardNumber); - if (string.IsNullOrWhiteSpace(input.BankName) || input.BankName.Length > 100) - throw Oops.Bah("开户银行名称不能为空且不能超过 100 个字符"); + if (!string.IsNullOrWhiteSpace(input.BankName) && input.BankName.Length > 100) + throw Oops.Bah("开户银行名称不能超过 100 个字符"); ValidateOptionalContactPhone(input.EmergencyPhone); ValidateEducationHistory(input.EducationHistoryJson); @@ -63,6 +73,40 @@ internal static class OnboardingEmployeeInfoValidator ValidateJson(input.CustomVariablesJson, global::System.Text.Json.JsonValueKind.Object, "员工补充变量"); } + /// + /// 按任务快照校验后台配置为必填的内置员工字段。 + /// + private static void ValidateRequiredFields( + ObTaskEmployeeInfo input, + IReadOnlyCollection fieldRequirements) + { + if (fieldRequirements == null || fieldRequirements.Count == 0) + return; + + foreach (var requirement in fieldRequirements.Where(item => + item.Required + && OnboardingEmployeeFieldRequirementSnapshot.IsEmployeeField(item))) + { + var separatorIndex = requirement.FieldCode.LastIndexOf('.'); + var propertyName = separatorIndex >= 0 + ? requirement.FieldCode[(separatorIndex + 1)..] + : requirement.FieldCode; + var property = typeof(ObTaskEmployeeInfo).GetProperty( + propertyName, + global::System.Reflection.BindingFlags.Instance + | global::System.Reflection.BindingFlags.Public + | global::System.Reflection.BindingFlags.IgnoreCase); + if (property == null) + { + throw Oops.Bah( + $"内置员工变量“{requirement.FieldCode}”没有对应的入职登记表字段,请联系管理员"); + } + + var value = property.GetValue(input)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + throw Oops.Bah($"请填写入职登记表“{requirement.FieldName}”"); + } + } private static bool TryValidateIdCard( string idCardNumber, out DateTime birthday, @@ -106,7 +150,7 @@ internal static class OnboardingEmployeeInfoValidator return false; } - private static void ValidateBirthday(string birthdayText, DateTime idCardBirthday) + private static void ValidateBirthday(string birthdayText, DateTime? idCardBirthday) { if (string.IsNullOrWhiteSpace(birthdayText)) return; @@ -120,7 +164,7 @@ internal static class OnboardingEmployeeInfoValidator if (!birthdayIsValid || birthday.Date > DateTime.Today) throw Oops.Bah("请输入正确的出生日期"); - if (birthday.Date != idCardBirthday.Date) + if (idCardBirthday.HasValue && birthday.Date != idCardBirthday.Value.Date) throw Oops.Bah("出生日期与身份证号码不一致"); } diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs index 82e7d1f..9995f6c 100644 --- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs @@ -40,6 +40,8 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService .Where(item => item.TaskId == task.Id) .OrderBy(item => item.SortCode) .ToListAsync(); + var formRequirements = + await OnboardingEmployeeFieldRequirementSnapshot.GetForTask(_db, task.Id); var contracts = await _db.Queryable() .Where(item => item.TaskId == task.Id) @@ -106,7 +108,17 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService Contracts = contracts, DynamicValues = new List(), Materials = materialOutputs, - FormFields = new List(), + FormFields = formRequirements + .Where(OnboardingEmployeeFieldRequirementSnapshot.IsEmployeeField) + .Select(requirement => new OnboardingPublicFormFieldOutput + { + FieldId = requirement.FieldId, + FieldCode = requirement.FieldCode, + FieldName = requirement.FieldName, + DataType = requirement.DataType, + OptionsJson = requirement.OptionsJson, + Required = requirement.Required + }).ToList(), MaterialRequirements = materialRequirements.Select(requirement => new OnboardingPublicMaterialOutput { MaterialTypeId = requirement.MaterialTypeId, @@ -134,7 +146,9 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService throw Oops.Bah("当前任务不可编辑"); } - OnboardingEmployeeInfoValidator.ValidateAndNormalize(input); + var formRequirements = + await OnboardingEmployeeFieldRequirementSnapshot.GetForTask(_db, task.Id); + OnboardingEmployeeInfoValidator.ValidateAndNormalize(input, formRequirements); await _identityValidationService.EnsureNoDuplicate( task.Id, input.EmployeeName, diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs index 7aa41d5..2b6cc26 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs @@ -161,6 +161,11 @@ public class OnboardingTaskService : IOnboardingTaskService CreateTime = DateTime.Now }).ExecuteCommandAsync(); + var employeeFieldRequirements = + await OnboardingEmployeeFieldRequirementSnapshot.BuildFromCurrentDefinitions(_db, taskId); + if (employeeFieldRequirements.Count > 0) + await _db.Insertable(employeeFieldRequirements).ExecuteCommandAsync(); + var contracts = contractConfigs.Select(config => { var template = contractTemplates.First(item => item.Id == config.ContractTemplateId); @@ -295,6 +300,10 @@ public class OnboardingTaskService : IOnboardingTaskService .Where(item => item.TaskId == sourceTask.Id) .OrderBy(item => item.SortCode) .ToListAsync(); + var sourceEmployeeFieldRequirements = + (await OnboardingEmployeeFieldRequirementSnapshot.GetForTask(_db, sourceTask.Id)) + .Where(OnboardingEmployeeFieldRequirementSnapshot.IsEmployeeField) + .ToList(); var sourceContractIds = sourceContracts.Select(item => item.Id).ToList(); var sourceManualVariables = await _db.Queryable() .Where(item => sourceContractIds.Contains(item.TaskContractId) @@ -334,6 +343,7 @@ public class OnboardingTaskService : IOnboardingTaskService sourceTask, sourceContracts, sourceMaterialRequirements, + sourceEmployeeFieldRequirements, initialContractVariables, expiryHours, item.Remark.Trim()); @@ -361,6 +371,7 @@ public class OnboardingTaskService : IOnboardingTaskService ObOnboardingTask sourceTask, List sourceContracts, List sourceMaterialRequirements, + List sourceEmployeeFieldRequirements, List initialContractVariables, int expiryHours, string remark) @@ -405,6 +416,24 @@ public class OnboardingTaskService : IOnboardingTaskService CreateTime = now }).ExecuteCommandAsync(); + if (sourceEmployeeFieldRequirements.Count > 0) + { + var copiedEmployeeFieldRequirements = sourceEmployeeFieldRequirements.Select(source => + new ObTaskFormRequirement + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = taskId, + FieldId = source.FieldId, + FieldCode = source.FieldCode, + FieldName = source.FieldName, + DataType = source.DataType, + OptionsJson = source.OptionsJson, + Required = source.Required, + SortCode = source.SortCode + }).ToList(); + await _db.Insertable(copiedEmployeeFieldRequirements).ExecuteCommandAsync(); + } + var copiedContracts = sourceContracts.Select(source => new ObTaskContract { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), diff --git a/database/migrations/20260914_employee_field_requirement_snapshot.sql b/database/migrations/20260914_employee_field_requirement_snapshot.sql new file mode 100644 index 0000000..c1414ac --- /dev/null +++ b/database/migrations/20260914_employee_field_requirement_snapshot.sql @@ -0,0 +1,28 @@ +-- 入职登记表内置员工字段必填规则的任务快照表。 +-- 可在本地、测试和正式环境重复执行。 + +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `ob_task_form_requirement` ( + `Id` BIGINT NOT NULL COMMENT '主键', + `TaskId` BIGINT NOT NULL COMMENT '入职任务Id', + `FieldId` BIGINT NOT NULL COMMENT '原字段或变量定义Id', + `FieldCode` VARCHAR(100) NOT NULL COMMENT '冻结字段编码', + `FieldName` VARCHAR(200) NOT NULL COMMENT '冻结字段名称', + `DataType` VARCHAR(50) NOT NULL COMMENT '冻结数据类型', + `OptionsJson` JSON NULL COMMENT '冻结选项JSON', + `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必填', + `SortCode` INT NULL COMMENT '显示顺序', + `Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '状态', + `CreateTime` DATETIME(3) NULL COMMENT '创建时间', + `UpdateTime` DATETIME(3) NULL COMMENT '更新时间', + `CreateUserId` BIGINT NULL COMMENT '创建者Id', + `UpdateUserId` BIGINT NULL COMMENT '修改者Id', + `CreateUser` VARCHAR(200) NULL COMMENT '创建人', + `UpdateUser` VARCHAR(200) NULL COMMENT '更新人', + `IsDelete` TINYINT(1) NULL DEFAULT 0 COMMENT '软删除', + `ExtJson` LONGTEXT NULL COMMENT '扩展信息', + PRIMARY KEY (`Id`), + UNIQUE KEY `uk_ob_task_form_requirement` (`TaskId`, `FieldId`), + KEY `idx_ob_task_form_requirement_task_sort` (`TaskId`, `SortCode`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务入职登记字段必填快照'; \ No newline at end of file