namespace SimpleAdmin.Application; /// /// 实现基于入职模板创建任务及冻结配置快照的业务能力。 /// public class OnboardingTaskService : IOnboardingTaskService { private readonly SqlSugarScope _db; private readonly IConfigService _configService; private readonly IOnboardingContractSnapshotService _snapshotService; public OnboardingTaskService( IConfigService configService, IOnboardingContractSnapshotService snapshotService) { _db = DbContext.DB; _configService = configService; _snapshotService = snapshotService; } /// /// 校验公司、岗位和入职模板后创建任务,并冻结合同及材料要求。 /// public async Task Create(OnboardingTaskCreateInput input) { var companyEnabled = await _db.Queryable() .AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable"); if (!companyEnabled) throw Oops.Bah("公司不存在或未启用"); var positionEnabled = await _db.Queryable() .AnyAsync(item => item.Id == input.PositionId && item.CompanyId == input.CompanyId && item.Status == "enable"); if (!positionEnabled) throw Oops.Bah("岗位不存在、未启用或不属于该公司"); var onboardingTemplate = await _db.Queryable() .FirstAsync(item => item.Id == input.OnboardingTemplateId && item.CompanyId == input.CompanyId && item.Status == "enable") ?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司"); var templateSupportsPosition = await _db.Queryable() .AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id && item.PositionId == input.PositionId); if (!templateSupportsPosition) throw Oops.Bah("所选入职模板不适用于该岗位"); var contractConfigs = await _db.Queryable() .Where(item => item.OnboardingTemplateId == onboardingTemplate.Id) .OrderBy(item => item.SortCode) .ToListAsync(); if (contractConfigs.Count == 0) throw Oops.Bah("入职模板尚未配置合同文件"); var materialConfigs = await _db.Queryable() .Where(item => item.OnboardingTemplateId == onboardingTemplate.Id) .OrderBy(item => item.SortCode) .ToListAsync(); var templateIds = contractConfigs.Select(item => item.ContractTemplateId).Distinct().ToList(); var contractTemplates = await _db.Queryable() .Where(item => templateIds.Contains(item.Id)) .ToListAsync(); var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList(); var enabledVersionCount = await _db.Queryable() .CountAsync(item => versionIds.Contains(item.Id) && item.VersionStatus == ContractTemplateVersionStatus.Enabled); if (enabledVersionCount != versionIds.Count) throw Oops.Bah("入职模板包含未启用的合同版本,请先调整模板配置"); var materialIds = materialConfigs.Select(item => item.MaterialTypeId).Distinct().ToList(); var materialTypes = materialIds.Count == 0 ? new List() : await _db.Queryable().Where(item => materialIds.Contains(item.Id)).ToListAsync(); if (materialTypes.Count != materialIds.Count) throw Oops.Bah("入职模板包含不存在的材料类型,请先调整模板配置"); var expiryConfig = await _configService.GetByConfigKey( CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.link.expireHours"); if (expiryConfig == null || !int.TryParse(expiryConfig.ConfigValue, out var expiryHours) || expiryHours <= 0) { throw Oops.Bah("请在系统配置中设置 onboard.link.expireHours(正整数小时)"); } var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); var expiresAt = DateTime.Now.AddHours(expiryHours); var token = Guid.NewGuid().ToString("N"); var task = new ObOnboardingTask { Id = taskId, TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}", CompanyId = input.CompanyId, PositionId = input.PositionId, OnboardingTemplateId = onboardingTemplate.Id, AssignedReviewerUserId = input.AssignedReviewerUserId, LinkExpireAt = expiresAt, TaskStatus = OnboardingTaskStatus.PendingFill, Remark = input.Remark }; ValidateInitialContractDateRanges(input.ContractVariables); await _db.Ado.BeginTranAsync(); try { await _db.Insertable(task).ExecuteCommandAsync(); await _db.Insertable(new ObTaskLink { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, PlainToken = token, TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData( global::System.Text.Encoding.UTF8.GetBytes(token))), ExpiresAt = expiresAt }).ExecuteCommandAsync(); // 初始化空白的员工入职登记表 await _db.Insertable(new ObTaskEmployeeInfo { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, Status = CommonStatusConst.ENABLE, CreateTime = DateTime.Now }).ExecuteCommandAsync(); var contracts = contractConfigs.Select(config => { var template = contractTemplates.First(item => item.Id == config.ContractTemplateId); return new ObTaskContract { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, TemplateId = config.ContractTemplateId, TemplateVersionId = config.ContractTemplateVersionId, ContractName = template.TemplateName, ContractType = template.ContractType, Required = config.Required, PrintCopies = config.PrintCopies }; }).ToList(); await _db.Insertable(contracts).ExecuteCommandAsync(); if (materialConfigs.Count > 0) { var materialSnapshots = materialConfigs.Select(config => { var material = materialTypes.First(item => item.Id == config.MaterialTypeId); return new ObTaskMaterialRequirement { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, MaterialTypeId = material.Id, MaterialCode = material.MaterialCode, MaterialName = material.MaterialName, AllowedExtensions = material.AllowedExtensions, MaxFileCount = material.MaxFileCount, MaxFileSizeMb = material.MaxFileSizeMb, Required = config.Required, SortCode = config.SortCode }; }).ToList(); await _db.Insertable(materialSnapshots).ExecuteCommandAsync(); } await _snapshotService.Build(taskId); await ApplyInitialContractVariables(input.ContractVariables, contracts); await _db.Ado.CommitTranAsync(); } catch { await _db.Ado.RollbackTranAsync(); throw; } return new OnboardingTaskCreatedOutput { TaskId = taskId, TaskNo = task.TaskNo, LinkToken = token, ExpiresAt = expiresAt }; } /// /// 校验并保存任务创建时填写的人事合同变量。 /// private async Task ApplyInitialContractVariables( List inputs, List contracts) { inputs ??= new List(); var versionIds = contracts.Select(item => item.TemplateVersionId).Distinct().ToList(); var definitions = await _db.Queryable() .Where(item => versionIds.Contains(item.TemplateVersionId) && item.HrEditable && (item.VariableType == "hr" || item.VariableType == "contract" || item.VariableType == "company" || item.VariableType == ContractVariableType.Contract || item.VariableType == ContractVariableType.Company || item.VariableCode.StartsWith("hr."))) .ToListAsync(); var duplicateInput = inputs .GroupBy(item => new { item.ContractTemplateVersionId, item.VariableCode }) .FirstOrDefault(group => group.Count() > 1); if (duplicateInput != null) throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交"); foreach (var input in inputs) { var definitionExists = definitions.Any(item => item.TemplateVersionId == input.ContractTemplateVersionId && item.VariableCode == input.VariableCode); if (!definitionExists) throw Oops.Bah($"合同变量“{input.VariableCode}”不存在或不允许人事填写"); } var missingRequired = definitions.FirstOrDefault(definition => { if (!definition.Required) return false; var input = inputs.FirstOrDefault(item => item.ContractTemplateVersionId == definition.TemplateVersionId && item.VariableCode == definition.VariableCode); return string.IsNullOrWhiteSpace(input?.VariableValue ?? definition.DefaultValue); }); if (missingRequired != null) throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”"); if (inputs.Count == 0) return; var contractByVersion = contracts.ToDictionary(item => item.TemplateVersionId); var contractIds = contracts.Select(item => item.Id).ToList(); var rows = await _db.Queryable() .Where(item => contractIds.Contains(item.TaskContractId)) .ToListAsync(); foreach (var input in inputs) { var contract = contractByVersion[input.ContractTemplateVersionId]; var row = rows.FirstOrDefault(item => item.TaskContractId == contract.Id && item.VariableCode == input.VariableCode) ?? throw Oops.Bah($"合同变量快照“{input.VariableCode}”不存在"); row.VariableValue = input.VariableValue; row.SourceType = ContractVariableSourceType.HrInput; row.IsManualOverride = true; } var changedRows = rows.Where(item => item.IsManualOverride).ToList(); if (changedRows.Count > 0) await _db.Updateable(changedRows).ExecuteCommandAsync(); } /// /// 校验人事创建任务时填写的合同日期先后关系。 /// private static void ValidateInitialContractDateRanges( List inputs) { inputs ??= new List(); foreach (var group in inputs.GroupBy(item => item.ContractTemplateVersionId)) { ValidateDateRange(group, "hr.ContractStartDate", "hr.ContractEndDate", "合同"); ValidateDateRange(group, "hr.ProbationStartDate", "hr.ProbationEndDate", "试用期"); } } /// /// 校验同一合同版本内一组开始、结束日期。 /// private static void ValidateDateRange( IEnumerable inputs, string startCode, string endCode, string rangeName) { var startValue = inputs.FirstOrDefault(item => string.Equals(item.VariableCode, startCode, StringComparison.OrdinalIgnoreCase))?.VariableValue; var endValue = inputs.FirstOrDefault(item => string.Equals(item.VariableCode, endCode, StringComparison.OrdinalIgnoreCase))?.VariableValue; if (string.IsNullOrWhiteSpace(startValue) || string.IsNullOrWhiteSpace(endValue)) return; if (!DateTime.TryParse(startValue, out var startDate)) throw Oops.Bah($"{rangeName}开始日期格式不正确"); if (!DateTime.TryParse(endValue, out var endDate)) throw Oops.Bah($"{rangeName}结束日期格式不正确"); if (endDate.Date < startDate.Date) throw Oops.Bah($"{rangeName}结束日期不能早于开始日期"); } }