From a51d45372dbdca3ccd7939f67d53d25c5a181e33 Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Mon, 24 Aug 2026 14:26:11 +0800 Subject: [PATCH] 1 --- .../Audit/Dto/OnboardingAuditInput.cs | 9 + .../Audit/IOnboardingAuditService.cs | 7 + .../Audit/OnboardingAuditService.cs | 129 +++++++++ .../Company/OnboardingCompanyService.cs | 44 ++- .../Dto/OnboardingContractDocumentInput.cs | 7 + .../IOnboardingContractDocumentService.cs | 2 + .../OnboardingContractDocumentService.cs | 97 +++++++ .../IOnboardingContractSnapshotService.cs | 7 + .../OnboardingContractSnapshotService.cs | 82 ++++++ .../OnboardingContractTemplateService.cs | 187 +++++++++--- .../Employee/Dto/OnboardingEmployeeInput.cs | 17 ++ .../Employee/IOnboardingEmployeeService.cs | 7 + .../Employee/OnboardingEmployeeService.cs | 24 ++ .../Dto/OnboardingEntryConfigInput.cs | 68 +++++ .../IOnboardingEntryConfigService.cs | 17 ++ .../OnboardingEntryConfigService.cs | 245 ++++++++++++++++ .../IOnboardingTaskLifecycleService.cs | 2 + .../OnboardingTaskLifecycleService.cs | 90 ++++++ .../Dto/OnboardingMaterialReviewInput.cs | 10 + .../IOnboardingMaterialReviewService.cs | 2 + .../OnboardingMaterialReviewService.cs | 65 +++++ .../Position/OnboardingPositionService.cs | 42 ++- ...OnboardingMaterialUploadValidationInput.cs | 9 + .../Dto/OnboardingPublicSubmissionInput.cs | 38 +++ .../IOnboardingPublicSubmissionService.cs | 10 + .../OnboardingPublicSubmissionService.cs | 265 ++++++++++++++++++ .../IOnboardingPublicTaskService.cs | 8 + .../PublicTask/OnboardingPublicTaskService.cs | 156 +++++++++++ .../Task/Dto/OnboardingTaskPublicInput.cs | 62 +++- .../Onboarding/Task/OnboardingTaskService.cs | 121 +++++++- .../OnboardingTaskContractVariableInput.cs | 43 +++ .../IOnboardingTaskContractVariableService.cs | 7 + .../OnboardingTaskContractVariableService.cs | 139 +++++++++ .../TaskQuery/Dto/OnboardingTaskQueryInput.cs | 17 ++ .../TaskQuery/IOnboardingTaskQueryService.cs | 8 + .../TaskQuery/OnboardingTaskQueryService.cs | 44 +++ .../SimpleAdmin.Application.xml | 26 +- .../Services/Dev/File/FileService.cs | 12 +- .../Services/Dev/File/IFileService.cs | 3 +- .../Onboarding/OnboardingAuditController.cs | 26 ++ .../Onboarding/OnboardingCompanyController.cs | 49 +++- .../OnboardingContractDocumentController.cs | 25 ++ .../OnboardingContractTemplateController.cs | 117 ++++++-- .../OnboardingEmployeeController.cs | 26 ++ .../OnboardingEntryConfigController.cs | 86 ++++++ .../OnboardingMaterialReviewController.cs | 20 ++ .../OnboardingPositionController.cs | 49 +++- .../OnboardingPublicSubmissionController.cs | 67 +++++ .../OnboardingPublicTaskController.cs | 35 +++ ...nboardingTaskContractVariableController.cs | 26 ++ .../Onboarding/OnboardingTaskController.cs | 15 +- .../OnboardingTaskLifecycleController.cs | 26 ++ .../OnboardingTaskQueryController.cs | 26 ++ SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml | 5 + 54 files changed, 2607 insertions(+), 119 deletions(-) create mode 100644 SimpleAdmin.Application/Services/Onboarding/Audit/Dto/OnboardingAuditInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Audit/IOnboardingAuditService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractDocument/Dto/OnboardingContractDocumentInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractDocument/IOnboardingContractDocumentService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Employee/IOnboardingEmployeeService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Lifecycle/IOnboardingTaskLifecycleService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/MaterialReview/Dto/OnboardingMaterialReviewInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/MaterialReview/IOnboardingMaterialReviewService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/MaterialReview/OnboardingMaterialReviewService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicSubmission/IOnboardingPublicSubmissionService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/Dto/OnboardingTaskContractVariableInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/IOnboardingTaskContractVariableService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskQuery/Dto/OnboardingTaskQueryInput.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs create mode 100644 SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingAuditController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractDocumentController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEmployeeController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingMaterialReviewController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskContractVariableController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskLifecycleController.cs create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/Dto/OnboardingAuditInput.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/Dto/OnboardingAuditInput.cs new file mode 100644 index 0000000..edf6937 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Audit/Dto/OnboardingAuditInput.cs @@ -0,0 +1,9 @@ +namespace SimpleAdmin.Application; + +public class OnboardingAuditActionInput +{ + [IdNotNull(ErrorMessage = "任务Id不能为空")] + public long TaskId { get; set; } + + public string AuditOpinion { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/IOnboardingAuditService.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/IOnboardingAuditService.cs new file mode 100644 index 0000000..899ed57 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Audit/IOnboardingAuditService.cs @@ -0,0 +1,7 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingAuditService : ITransient +{ + Task Approve(OnboardingAuditActionInput input); + Task Reject(OnboardingAuditActionInput input); +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs new file mode 100644 index 0000000..84f77b9 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs @@ -0,0 +1,129 @@ +namespace SimpleAdmin.Application; + +public class OnboardingAuditService : IOnboardingAuditService +{ + private readonly ISqlSugarClient _db; + private readonly IConfigService _config; + private readonly IOnboardingContractSnapshotService _snapshotService; + + public OnboardingAuditService( + ISqlSugarClient db, + IConfigService config, + IOnboardingContractSnapshotService snapshotService) + { + _db = db; + _config = config; + _snapshotService = snapshotService; + } + + public async Task Approve(OnboardingAuditActionInput input) + { + var task = await GetAuditableTask(input.TaskId); + var employeeInfo = await _db.Queryable() + .FirstAsync(item => item.TaskId == task.Id) + ?? throw Oops.Bah("候选人资料不存在"); + + var hasMaterialNeedSupplement = await _db.Queryable() + .AnyAsync(item => item.TaskId == task.Id + && item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement); + if (hasMaterialNeedSupplement) + { + throw Oops.Bah("仍有资料需要补充,不能审核通过"); + } + + var hasExistingEmployee = await _db.Queryable() + .AnyAsync(item => item.IdCardNumber == employeeInfo.IdCardNumber || item.Phone == employeeInfo.Phone); + if (hasExistingEmployee) + { + throw Oops.Bah("该身份证号或手机号已存在正式员工"); + } + + var extraValues = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(); + var employee = employeeInfo.Adapt(); + employee.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); + employee.CompanyId = task.CompanyId; + employee.PositionId = task.PositionId; + employee.ExtraInfoJson = global::System.Text.Json.JsonSerializer.Serialize(extraValues); + + await _db.Ado.BeginTranAsync(); + try + { + await _snapshotService.Build(task.Id, employeeInfo); + await _db.Insertable(employee).ExecuteCommandAsync(); + + task.EmployeeId = employee.Id; + task.TaskStatus = OnboardingTaskStatus.Approved; + task.AuditedAt = DateTime.Now; + await _db.Updateable(task).ExecuteCommandAsync(); + await WriteAudit(task, OnboardingAuditAction.Approve, input.AuditOpinion); + await _db.Ado.CommitTranAsync(); + } + catch + { + await _db.Ado.RollbackTranAsync(); + throw; + } + } + + public async Task Reject(OnboardingAuditActionInput input) + { + if (string.IsNullOrWhiteSpace(input.AuditOpinion)) + { + throw Oops.Bah("请填写退回原因"); + } + + var task = await GetAuditableTask(input.TaskId); + task.TaskStatus = OnboardingTaskStatus.Rejected; + task.RejectReason = input.AuditOpinion; + await _db.Updateable(task).ExecuteCommandAsync(); + await WriteAudit(task, OnboardingAuditAction.Reject, input.AuditOpinion); + } + + private async Task GetAuditableTask(long taskId) + { + var task = await _db.Queryable().InSingleAsync(taskId) + ?? throw Oops.Bah("入职任务不存在"); + if (task.TaskStatus != OnboardingTaskStatus.Reviewing) + { + throw Oops.Bah("当前任务不在审核中"); + } + + var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds") + ?? throw Oops.Bah("请配置入职审核人"); + using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue); + var reviewerIds = new List(); + if (configJson.RootElement.TryGetProperty("all", out var allReviewers)) + { + reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (configJson.RootElement.TryGetProperty("company", out var companyReviewers) + && companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers)) + { + reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (!reviewerIds.Contains(UserManager.UserId)) + { + throw Oops.Bah("您不是该入职任务的审核人"); + } + + return task; + } + + private Task WriteAudit(ObOnboardingTask task, string action, string opinion) + { + return _db.Insertable(new ObTaskAudit + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + AuditRound = task.SubmitCount, + AuditAction = action, + AuditorUserId = UserManager.UserId, + AuditOpinion = opinion, + AuditedAt = DateTime.Now + }).ExecuteCommandAsync(); + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs b/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs index e732fff..3a9396f 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Company/OnboardingCompanyService.cs @@ -5,16 +5,27 @@ public class OnboardingCompanyService : DbRepository, IOnboardingComp public async Task> Page(OnboardingCompanyPageInput input) { return await Context.Queryable() - .WhereIF(!string.IsNullOrWhiteSpace(input.CompanyName), it => it.CompanyName.Contains(input.CompanyName) || it.CompanyCode.Contains(input.CompanyName)) + .WhereIF( + !string.IsNullOrWhiteSpace(input.CompanyName), + it => it.CompanyName.Contains(input.CompanyName) || it.CompanyCode.Contains(input.CompanyName)) .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status) .OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}") .OrderBy(it => it.CreateTime) .ToPagedListAsync(input.PageNum, input.PageSize); } - public Task> ListEnabled() => Context.Queryable().Where(it => it.Status == "enable").OrderBy(it => it.CompanyName).ToListAsync(); + public Task> ListEnabled() + { + return Context.Queryable() + .Where(it => it.Status == "enable") + .OrderBy(it => it.CompanyName) + .ToListAsync(); + } - public async Task Detail(BaseIdInput input) => await GetByIdAsync(input.Id) ?? throw Oops.Bah("公司不存在"); + public async Task Detail(BaseIdInput input) + { + return await GetByIdAsync(input.Id) ?? throw Oops.Bah("公司不存在"); + } public async Task Add(OnboardingCompanyAddInput input) { @@ -31,15 +42,32 @@ public class OnboardingCompanyService : DbRepository, IOnboardingComp public async Task Delete(BaseIdListInput input) { - if (input.Ids.Count == 0) return; - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.CompanyId))) throw Oops.Bah("公司下仍有岗位,不能删除"); - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.CompanyId))) throw Oops.Bah("公司已有入职任务,不能删除"); - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.CompanyId))) throw Oops.Bah("公司已有正式员工,不能删除"); + if (input.Ids.Count == 0) + return; + + var hasPositions = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.CompanyId)); + if (hasPositions) + throw Oops.Bah("公司下仍有岗位,不能删除"); + + var hasTasks = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.CompanyId)); + if (hasTasks) + throw Oops.Bah("公司已有入职任务,不能删除"); + + var hasEmployees = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.CompanyId)); + if (hasEmployees) + throw Oops.Bah("公司已有正式员工,不能删除"); + await DeleteByIdsAsync(input.Ids.Cast().ToArray()); } private async Task EnsureCodeUnique(string code, long id) { - if (await Context.Queryable().AnyAsync(it => it.CompanyCode == code && it.Id != id)) throw Oops.Bah("公司编码已存在"); + var exists = await Context.Queryable() + .AnyAsync(it => it.CompanyCode == code && it.Id != id); + if (exists) + throw Oops.Bah("公司编码已存在"); } } diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/Dto/OnboardingContractDocumentInput.cs b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/Dto/OnboardingContractDocumentInput.cs new file mode 100644 index 0000000..7edc8cd --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/Dto/OnboardingContractDocumentInput.cs @@ -0,0 +1,7 @@ +namespace SimpleAdmin.Application; + +public class OnboardingGenerateContractInput +{ + [IdNotNull(ErrorMessage = "任务合同Id不能为空")] + public long TaskContractId { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/IOnboardingContractDocumentService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/IOnboardingContractDocumentService.cs new file mode 100644 index 0000000..b99f1ab --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/IOnboardingContractDocumentService.cs @@ -0,0 +1,2 @@ +namespace SimpleAdmin.Application; +public interface IOnboardingContractDocumentService : ITransient { Task Generate(OnboardingGenerateContractInput input); } diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs new file mode 100644 index 0000000..54dffd8 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs @@ -0,0 +1,97 @@ +namespace SimpleAdmin.Application; + +public class OnboardingContractDocumentService : IOnboardingContractDocumentService +{ + private readonly ISqlSugarClient _db; + private readonly IFileService _fileService; + + public OnboardingContractDocumentService(ISqlSugarClient db, IFileService fileService) + { + _db = db; + _fileService = fileService; + } + + public async Task Generate(OnboardingGenerateContractInput input) + { + var contract = await _db.Queryable().InSingleAsync(input.TaskContractId) + ?? throw Oops.Bah("任务合同不存在"); + var task = await _db.Queryable().InSingleAsync(contract.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + + if (task.TaskStatus != OnboardingTaskStatus.Approved) + throw Oops.Bah("仅审核通过的任务可生成归档合同"); + + if (contract.GeneratedDocxFileId.HasValue) + return contract.GeneratedDocxFileId.Value; + + var version = await _db.Queryable().InSingleAsync(contract.TemplateVersionId) + ?? throw Oops.Bah("合同版本不存在"); + var source = await _db.Queryable().InSingleAsync(version.SourceDocxFileId) + ?? throw Oops.Bah("合同源文件不存在"); + var variables = await _db.Queryable() + .Where(x => x.TaskContractId == contract.Id) + .ToListAsync(); + var sourceResult = await _fileService.Download(new BaseIdInput { Id = source.Id }) + ?? throw Oops.Bah("无法读取合同源文件"); + + await using var sourceStream = sourceResult.FileStream; + using var sourceBytes = new MemoryStream(); + await sourceStream.CopyToAsync(sourceBytes); + + var rendered = ReplaceDocxText( + sourceBytes.ToArray(), + variables.ToDictionary(x => x.VariableCode, x => x.VariableValue ?? string.Empty)); + var name = $"{task.TaskNo}_{contract.ContractName}.docx"; + var fileId = await _fileService.StoreGeneratedFile(source.Engine, name, rendered); + + contract.GeneratedDocxFileId = fileId; + contract.GeneratedAt = DateTime.Now; + await _db.Updateable(contract).ExecuteCommandAsync(); + + return fileId; + } + + private static byte[] ReplaceDocxText(byte[] docx, Dictionary values) + { + using var input = new MemoryStream(docx); + using var archive = new global::System.IO.Compression.ZipArchive( + input, + global::System.IO.Compression.ZipArchiveMode.Read); + using var output = new MemoryStream(); + using var target = new global::System.IO.Compression.ZipArchive( + output, + global::System.IO.Compression.ZipArchiveMode.Create, + true); + + foreach (var entry in archive.Entries) + { + var next = target.CreateEntry(entry.FullName, global::System.IO.Compression.CompressionLevel.Optimal); + using var reader = entry.Open(); + using var writer = next.Open(); + + var isWordTextXml = entry.FullName == "word/document.xml" + || ((entry.FullName.StartsWith("word/header") || entry.FullName.StartsWith("word/footer")) + && entry.FullName.EndsWith(".xml")); + if (!isWordTextXml) + { + reader.CopyTo(writer); + continue; + } + + using var textReader = new StreamReader(reader); + var document = global::System.Xml.Linq.XDocument.Parse(textReader.ReadToEnd()); + var word = global::System.Xml.Linq.XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main"); + + foreach (var node in document.Descendants(word + "t")) + { + foreach (var item in values) + node.Value = node.Value.Replace("{{" + item.Key + "}}", item.Value ?? string.Empty); + } + + using var textWriter = new StreamWriter(writer); + textWriter.Write(document.ToString(global::System.Xml.Linq.SaveOptions.DisableFormatting)); + } + + return output.ToArray(); + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs new file mode 100644 index 0000000..ec62571 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs @@ -0,0 +1,7 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingContractSnapshotService : ITransient +{ + /// 根据任务、公司与候选人资料刷新自动变量;人事已手工填写的变量不会被覆盖。 + Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs new file mode 100644 index 0000000..7e4de73 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs @@ -0,0 +1,82 @@ +namespace SimpleAdmin.Application; + +/// 冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。 +public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService +{ + private readonly ISqlSugarClient _db; + public OnboardingContractSnapshotService(ISqlSugarClient db) => _db = db; + + public async Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null) + { + var task = await _db.Queryable().InSingleAsync(taskId) ?? throw Oops.Bah("入职任务不存在"); + var company = await _db.Queryable().InSingleAsync(task.CompanyId) ?? throw Oops.Bah("入职公司不存在"); + employeeInfo ??= await _db.Queryable().FirstAsync(x => x.TaskId == taskId); + var contracts = await _db.Queryable().Where(x => x.TaskId == taskId).ToListAsync(); + foreach (var contract in contracts) + { + if (contract.GeneratedDocxFileId.HasValue || contract.GeneratedPdfFileId.HasValue) throw Oops.Bah("合同已生成,不能重建变量快照"); + var definitions = await _db.Queryable().Where(x => x.TemplateVersionId == contract.TemplateVersionId).OrderBy(x => x.SortCode).ToListAsync(); + var oldRows = await _db.Queryable().Where(x => x.TaskContractId == contract.Id).ToListAsync(); + var rows = definitions.Select(definition => + { + var old = oldRows.FirstOrDefault(x => x.VariableCode == definition.VariableCode); + var preserveManual = old?.IsManualOverride == true; + return new ObTaskContractVariable + { + Id = old?.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskContractId = contract.Id, + VariableCode = definition.VariableCode, + VariableName = definition.VariableName, + VariableType = definition.VariableType, + DataType = definition.DataType, + VariableValue = preserveManual ? old.VariableValue : ResolveValue(definition, employeeInfo, company, task), + SourceType = preserveManual ? ContractVariableSourceType.HrInput : GetSourceType(definition.VariableType), + IsManualOverride = preserveManual + }; + }).ToList(); + await _db.Deleteable().Where(x => x.TaskContractId == contract.Id).ExecuteCommandAsync(); + if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync(); + } + } + + private static string GetSourceType(string variableType) => variableType switch + { + ContractVariableType.Employee => ContractVariableSourceType.Employee, + ContractVariableType.Company => ContractVariableSourceType.Company, + ContractVariableType.System => ContractVariableSourceType.System, + _ => ContractVariableSourceType.HrInput + }; + + private static string ResolveValue(ObTemplateVariable definition, ObTaskEmployeeInfo info, ObCompany company, ObOnboardingTask task) + { + if (definition.VariableType == ContractVariableType.Employee) + return GetPropertyValue(info, definition.EmployeeField) ?? definition.DefaultValue; + if (definition.VariableType == ContractVariableType.Company) + return GetPropertyValue(company, ResolveCompanyField(definition)) ?? definition.DefaultValue; + if (definition.VariableType == ContractVariableType.System && string.Equals(definition.VariableCode, "TASK_NO", StringComparison.OrdinalIgnoreCase)) + return task.TaskNo; + return definition.DefaultValue; + } + + private static string GetPropertyValue(object source, string propertyName) + { + if (source == null || string.IsNullOrWhiteSpace(propertyName)) return null; + return source.GetType().GetProperty(propertyName, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.IgnoreCase)?.GetValue(source)?.ToString(); + } + + private static string ResolveCompanyField(ObTemplateVariable definition) + { + if (!string.IsNullOrWhiteSpace(definition.EmployeeField)) return definition.EmployeeField; + return definition.VariableCode.ToUpperInvariant() switch + { + "COMPANY_CODE" => nameof(ObCompany.CompanyCode), + "COMPANY_NAME" => nameof(ObCompany.CompanyName), + "LEGAL_NAME" or "COMPANY_LEGAL_NAME" => nameof(ObCompany.LegalName), + "UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode), + "LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative), + "REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress), + "CONTACT_PHONE" => nameof(ObCompany.ContactPhone), + _ => definition.VariableCode + }; + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs index 9b7d133..04bda13 100644 --- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs @@ -1,12 +1,16 @@ namespace SimpleAdmin.Application; -/// 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。 +/// +/// 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。 +/// public class OnboardingContractTemplateService : DbRepository, IOnboardingContractTemplateService { public async Task> Page(OnboardingContractTemplatePageInput input) { return await Context.Queryable() - .WhereIF(!string.IsNullOrWhiteSpace(input.TemplateName), it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName)) + .WhereIF( + !string.IsNullOrWhiteSpace(input.TemplateName), + it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName)) .WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType) .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status) .OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}") @@ -14,7 +18,10 @@ public class OnboardingContractTemplateService : DbRepository Detail(BaseIdInput input) => await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在"); + public async Task Detail(BaseIdInput input) + { + return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在"); + } public async Task Add(OnboardingContractTemplateAddInput input) { @@ -31,46 +38,83 @@ public class OnboardingContractTemplateService : DbRepository().AnyAsync(it => input.Ids.Contains(it.TemplateId))) throw Oops.Bah("模板已有版本,不能删除"); - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.TemplateId))) throw Oops.Bah("模板已配置适用范围,不能删除"); - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.TemplateId))) throw Oops.Bah("模板已生成入职合同,不能删除"); + if (input.Ids.Count == 0) + return; + + var hasVersions = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.TemplateId)); + if (hasVersions) + throw Oops.Bah("模板已有版本,不能删除"); + + var hasScopes = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.TemplateId)); + if (hasScopes) + throw Oops.Bah("模板已配置适用范围,不能删除"); + + var hasContracts = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.TemplateId)); + if (hasContracts) + throw Oops.Bah("模板已生成入职合同,不能删除"); + await DeleteByIdsAsync(input.Ids.Cast().ToArray()); } - private async Task EnsureCodeUnique(string code, long id) - { - if (await Context.Queryable().AnyAsync(it => it.TemplateCode == code && it.Id != id)) throw Oops.Bah("模板编码已存在"); - } public async Task> VersionPage(OnboardingContractVersionPageInput input) { - return await Context.Queryable().Where(it => it.TemplateId == input.TemplateId) + return await Context.Queryable() + .Where(it => it.TemplateId == input.TemplateId) .WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus) - .OrderByDescending(it => it.CreateTime).ToPagedListAsync(input.PageNum, input.PageSize); + .OrderByDescending(it => it.CreateTime) + .ToPagedListAsync(input.PageNum, input.PageSize); } public async Task AddVersion(OnboardingContractVersionAddInput input) { await Detail(new BaseIdInput { Id = input.TemplateId }); - var file = await Context.Queryable().InSingleAsync(input.SourceDocxFileId) ?? throw Oops.Bah("DOCX源文件不存在"); - if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase)) throw Oops.Bah("合同模板仅支持DOCX文件"); - if (await Context.Queryable().AnyAsync(it => it.TemplateId == input.TemplateId && it.VersionNo == input.VersionNo)) throw Oops.Bah("该模板版本号已存在"); + + var file = await Context.Queryable().InSingleAsync(input.SourceDocxFileId) + ?? throw Oops.Bah("DOCX源文件不存在"); + if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase)) + throw Oops.Bah("合同模板仅支持DOCX文件"); + + var versionExists = await Context.Queryable() + .AnyAsync(it => it.TemplateId == input.TemplateId && it.VersionNo == input.VersionNo); + if (versionExists) + throw Oops.Bah("该模板版本号已存在"); + await Context.Insertable(input.Adapt()).ExecuteCommandAsync(); } public async Task SetVersionStatus(OnboardingContractVersionStatusInput input) { - if (input.VersionStatus != ContractTemplateVersionStatus.Enabled && input.VersionStatus != ContractTemplateVersionStatus.Disabled) throw Oops.Bah("版本状态仅支持启用或停用"); - var version = await Context.Queryable().InSingleAsync(input.Id) ?? throw Oops.Bah("模板版本不存在"); - if (input.VersionStatus == ContractTemplateVersionStatus.Enabled && !await Context.Queryable().AnyAsync(it => it.TemplateVersionId == input.Id)) throw Oops.Bah("请先配置模板变量后再启用"); + if (input.VersionStatus != ContractTemplateVersionStatus.Enabled + && input.VersionStatus != ContractTemplateVersionStatus.Disabled) + { + throw Oops.Bah("版本状态仅支持启用或停用"); + } + + var version = await Context.Queryable().InSingleAsync(input.Id) + ?? throw Oops.Bah("模板版本不存在"); + + if (input.VersionStatus == ContractTemplateVersionStatus.Enabled) + { + var hasVariables = await Context.Queryable() + .AnyAsync(it => it.TemplateVersionId == input.Id); + if (!hasVariables) + throw Oops.Bah("请先配置模板变量后再启用"); + } + version.VersionStatus = input.VersionStatus; await Context.Updateable(version).ExecuteCommandAsync(); } public async Task> VariablePage(OnboardingTemplateVariablePageInput input) { - return await Context.Queryable().Where(it => it.TemplateVersionId == input.TemplateVersionId) - .OrderBy(it => it.SortCode).OrderBy(it => it.VariableCode).ToPagedListAsync(input.PageNum, input.PageSize); + return await Context.Queryable() + .Where(it => it.TemplateVersionId == input.TemplateVersionId) + .OrderBy(it => it.SortCode) + .OrderBy(it => it.VariableCode) + .ToPagedListAsync(input.PageNum, input.PageSize); } public async Task AddVariable(OnboardingTemplateVariableAddInput input) @@ -82,8 +126,11 @@ public class OnboardingContractTemplateService : DbRepository().InSingleAsync(input.Id) ?? throw Oops.Bah("模板变量不存在"); - if (variable.TemplateVersionId != input.TemplateVersionId) throw Oops.Bah("不允许变更变量所属版本"); + var variable = await Context.Queryable().InSingleAsync(input.Id) + ?? throw Oops.Bah("模板变量不存在"); + if (variable.TemplateVersionId != input.TemplateVersionId) + throw Oops.Bah("不允许变更变量所属版本"); + await EnsureVersionDraft(input.TemplateVersionId); await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, input.Id); await Context.Updateable(input.Adapt()).ExecuteCommandAsync(); @@ -91,28 +138,28 @@ public class OnboardingContractTemplateService : DbRepository().Where(it => input.Ids.Contains(it.Id)).Select(it => it.TemplateVersionId).ToListAsync(); - foreach (var versionId in versionIds.Distinct()) await EnsureVersionDraft(versionId); + if (input.Ids.Count == 0) + return; + + var versionIds = await Context.Queryable() + .Where(it => input.Ids.Contains(it.Id)) + .Select(it => it.TemplateVersionId) + .ToListAsync(); + + foreach (var versionId in versionIds.Distinct()) + await EnsureVersionDraft(versionId); + await Context.Deleteable().In(input.Ids).ExecuteCommandAsync(); } - private async Task EnsureVersionDraft(long versionId) - { - var version = await Context.Queryable().InSingleAsync(versionId) ?? throw Oops.Bah("模板版本不存在"); - if (version.VersionStatus != ContractTemplateVersionStatus.Draft) throw Oops.Bah("只有草稿版本可维护变量"); - } - - private async Task EnsureVariableUnique(long versionId, string code, long id) - { - if (await Context.Queryable().AnyAsync(it => it.TemplateVersionId == versionId && it.VariableCode == code && it.Id != id)) throw Oops.Bah("该版本变量编码已存在"); - } public async Task> ScopePage(OnboardingContractScopePageInput input) + public async Task> ScopePage(OnboardingContractScopePageInput input) { return await Context.Queryable() .WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value) .WhereIF(input.PositionId.HasValue, it => it.PositionId == input.PositionId.Value) .WhereIF(input.TemplateId.HasValue, it => it.TemplateId == input.TemplateId.Value) - .OrderBy(it => it.SortCode).ToPagedListAsync(input.PageNum, input.PageSize); + .OrderBy(it => it.SortCode) + .ToPagedListAsync(input.PageNum, input.PageSize); } public async Task AddScope(OnboardingContractScopeAddInput input) @@ -124,7 +171,10 @@ public class OnboardingContractTemplateService : DbRepository().AnyAsync(it => it.Id == input.Id)) throw Oops.Bah("合同适用范围不存在"); + var scopeExists = await Context.Queryable().AnyAsync(it => it.Id == input.Id); + if (!scopeExists) + throw Oops.Bah("合同适用范围不存在"); + await EnsureScopeValid(input.CompanyId, input.PositionId, input.TemplateId, input.TemplateVersionId); await EnsureScopeUnique(input.CompanyId, input.PositionId, input.TemplateVersionId, input.Id); await Context.Updateable(input.Adapt()).ExecuteCommandAsync(); @@ -132,18 +182,65 @@ public class OnboardingContractTemplateService : DbRepository 0) await Context.Deleteable().In(input.Ids).ExecuteCommandAsync(); + if (input.Ids.Count == 0) + return; + + await Context.Deleteable().In(input.Ids).ExecuteCommandAsync(); + } + + private async Task EnsureCodeUnique(string code, long id) + { + var exists = await Context.Queryable() + .AnyAsync(it => it.TemplateCode == code && it.Id != id); + if (exists) + throw Oops.Bah("模板编码已存在"); + } + + private async Task EnsureVersionDraft(long versionId) + { + var version = await Context.Queryable().InSingleAsync(versionId) + ?? throw Oops.Bah("模板版本不存在"); + if (version.VersionStatus != ContractTemplateVersionStatus.Draft) + throw Oops.Bah("只有草稿版本可维护变量"); + } + + private async Task EnsureVariableUnique(long versionId, string code, long id) + { + var exists = await Context.Queryable() + .AnyAsync(it => it.TemplateVersionId == versionId && it.VariableCode == code && it.Id != id); + if (exists) + throw Oops.Bah("该版本变量编码已存在"); } private async Task EnsureScopeValid(long companyId, long? positionId, long templateId, long versionId) { - if (!await Context.Queryable().AnyAsync(it => it.Id == companyId && it.Status == "enable")) throw Oops.Bah("所属公司不存在或未启用"); - if (positionId.HasValue && !await Context.Queryable().AnyAsync(it => it.Id == positionId.Value && it.CompanyId == companyId && it.Status == "enable")) throw Oops.Bah("岗位不存在、未启用或不属于该公司"); - var version = await Context.Queryable().InSingleAsync(versionId) ?? throw Oops.Bah("合同版本不存在"); - if (version.TemplateId != templateId || version.VersionStatus != ContractTemplateVersionStatus.Enabled) throw Oops.Bah("只能配置该模板的已启用合同版本"); + var companyExists = await Context.Queryable() + .AnyAsync(it => it.Id == companyId && it.Status == "enable"); + if (!companyExists) + throw Oops.Bah("所属公司不存在或未启用"); + + if (positionId.HasValue) + { + var positionExists = await Context.Queryable() + .AnyAsync(it => it.Id == positionId.Value && it.CompanyId == companyId && it.Status == "enable"); + if (!positionExists) + throw Oops.Bah("岗位不存在、未启用或不属于该公司"); + } + + var version = await Context.Queryable().InSingleAsync(versionId) + ?? throw Oops.Bah("合同版本不存在"); + if (version.TemplateId != templateId || version.VersionStatus != ContractTemplateVersionStatus.Enabled) + throw Oops.Bah("只能配置该模板的已启用合同版本"); } private async Task EnsureScopeUnique(long companyId, long? positionId, long versionId, long id) { - if (await Context.Queryable().AnyAsync(it => it.CompanyId == companyId && it.PositionId == positionId && it.TemplateVersionId == versionId && it.Id != id)) throw Oops.Bah("该公司、岗位和合同版本的适用范围已存在"); - }} + var exists = await Context.Queryable() + .AnyAsync(it => it.CompanyId == companyId + && it.PositionId == positionId + && it.TemplateVersionId == versionId + && it.Id != id); + if (exists) + throw Oops.Bah("该公司、岗位和合同版本的适用范围已存在"); + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs b/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs new file mode 100644 index 0000000..9d636d2 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Employee/Dto/OnboardingEmployeeInput.cs @@ -0,0 +1,17 @@ +namespace SimpleAdmin.Application; + +public class OnboardingEmployeePageInput : BasePageInput +{ + public string EmployeeName { get; set; } + public string Phone { get; set; } + public long? CompanyId { get; set; } + public long? PositionId { get; set; } +} + +public class OnboardingEmployeeDetailOutput +{ + public ObEmployee Employee { get; set; } + public ObOnboardingTask Task { get; set; } + public List Contracts { get; set; } = new List(); + public List Audits { get; set; } = new List(); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/Employee/IOnboardingEmployeeService.cs b/SimpleAdmin.Application/Services/Onboarding/Employee/IOnboardingEmployeeService.cs new file mode 100644 index 0000000..c2a6129 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Employee/IOnboardingEmployeeService.cs @@ -0,0 +1,7 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingEmployeeService : ITransient +{ + Task> Page(OnboardingEmployeePageInput input); + Task Detail(BaseIdInput input); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs b/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs new file mode 100644 index 0000000..06630a3 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Employee/OnboardingEmployeeService.cs @@ -0,0 +1,24 @@ +namespace SimpleAdmin.Application; + +/// 仅展示审核通过后创建的正式员工档案;入职资料的来源任务可追溯。 +public class OnboardingEmployeeService : IOnboardingEmployeeService +{ + private readonly ISqlSugarClient _db; + public OnboardingEmployeeService(ISqlSugarClient db) => _db = db; + + public Task> Page(OnboardingEmployeePageInput input) => _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.EmployeeName), x => x.EmployeeName.Contains(input.EmployeeName) || x.IdCardNumber.Contains(input.EmployeeName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), x => x.Phone.Contains(input.Phone)) + .WhereIF(input.CompanyId.HasValue, x => x.CompanyId == input.CompanyId.Value) + .WhereIF(input.PositionId.HasValue, x => x.PositionId == input.PositionId.Value) + .OrderByDescending(x => x.CreateTime).ToPagedListAsync(input.PageNum, input.PageSize); + + public async Task Detail(BaseIdInput input) + { + var employee = await _db.Queryable().InSingleAsync(input.Id) ?? throw Oops.Bah("正式员工不存在"); + var task = await _db.Queryable().FirstAsync(x => x.EmployeeId == employee.Id); + var contracts = task == null ? new List() : await _db.Queryable().Where(x => x.TaskId == task.Id).ToListAsync(); + var audits = task == null ? new List() : await _db.Queryable().Where(x => x.TaskId == task.Id).OrderByDescending(x => x.AuditedAt).ToListAsync(); + return new OnboardingEmployeeDetailOutput { Employee = employee, Task = task, Contracts = contracts, Audits = audits }; + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs new file mode 100644 index 0000000..60b5367 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs @@ -0,0 +1,68 @@ +namespace SimpleAdmin.Application; + +public class OnboardingFormFieldSaveInput +{ + public long? Id { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段编码不能为空")] + public string FieldCode { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "字段名称不能为空")] + public string FieldName { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "数据类型不能为空")] + public string DataType { get; set; } + + public string OptionsJson { get; set; } + public string ValidationRule { get; set; } + public string Placeholder { get; set; } + public string Status { get; set; } = "enable"; +} + +public class OnboardingMaterialTypeSaveInput +{ + public long? Id { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料编码不能为空")] + public string MaterialCode { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "资料名称不能为空")] + public string MaterialName { get; set; } + + public string AllowedExtensions { get; set; } + public int MaxFileCount { get; set; } = 1; + public int? MaxFileSizeMb { get; set; } + public string Status { get; set; } = "enable"; +} + +public class OnboardingFormRequirementSaveInput +{ + public long? Id { get; set; } + + [IdNotNull(ErrorMessage = "公司Id不能为空")] + public long CompanyId { get; set; } + + public long? PositionId { get; set; } + + [IdNotNull(ErrorMessage = "字段Id不能为空")] + public long FieldId { get; set; } + + public bool Required { get; set; } + public int? SortCode { get; set; } +} + +public class OnboardingMaterialRequirementSaveInput +{ + public long? Id { get; set; } + + [IdNotNull(ErrorMessage = "公司Id不能为空")] + public long CompanyId { get; set; } + + public long? PositionId { get; set; } + + [IdNotNull(ErrorMessage = "资料类型Id不能为空")] + public long MaterialTypeId { get; set; } + + public bool Required { get; set; } + public int? SortCode { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs new file mode 100644 index 0000000..fe6bcf8 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs @@ -0,0 +1,17 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingEntryConfigService : ITransient +{ + Task> ListFormFields(); + Task SaveFormField(OnboardingFormFieldSaveInput input); + Task DeleteFormFields(BaseIdListInput input); + Task> ListMaterialTypes(); + Task SaveMaterialType(OnboardingMaterialTypeSaveInput input); + Task DeleteMaterialTypes(BaseIdListInput input); + Task> ListFormRequirements(long companyId); + Task SaveFormRequirement(OnboardingFormRequirementSaveInput input); + Task DeleteFormRequirements(BaseIdListInput input); + Task> ListMaterialRequirements(long companyId); + Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input); + Task DeleteMaterialRequirements(BaseIdListInput input); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs new file mode 100644 index 0000000..89bf0d7 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs @@ -0,0 +1,245 @@ +namespace SimpleAdmin.Application; + +/// +/// 维护员工端动态字段、材料类型及公司岗位要求。 +/// +public class OnboardingEntryConfigService : IOnboardingEntryConfigService +{ + private readonly ISqlSugarClient _db; + + public OnboardingEntryConfigService(ISqlSugarClient db) + { + _db = db; + } + + public Task> ListFormFields() + { + return _db.Queryable() + .OrderBy(x => x.FieldCode) + .ToListAsync(); + } + + public async Task SaveFormField(OnboardingFormFieldSaveInput input) + { + var fieldCodeExists = await _db.Queryable() + .AnyAsync(x => x.FieldCode == input.FieldCode && x.Id != input.Id); + if (fieldCodeExists) + throw Oops.Bah("字段编码已存在"); + + var entity = new ObFormField + { + Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + FieldCode = input.FieldCode, + FieldName = input.FieldName, + DataType = input.DataType, + OptionsJson = input.OptionsJson, + ValidationRule = input.ValidationRule, + Placeholder = input.Placeholder, + Status = input.Status + }; + + if (input.Id.HasValue) + { + var exists = await _db.Queryable().AnyAsync(x => x.Id == input.Id.Value); + if (!exists) + throw Oops.Bah("字段不存在"); + + await _db.Updateable(entity).ExecuteCommandAsync(); + return; + } + + await _db.Insertable(entity).ExecuteCommandAsync(); + } + + public async Task DeleteFormFields(BaseIdListInput input) + { + if (input.Ids.Count == 0) + return; + + var isReferenced = await _db.Queryable() + .AnyAsync(x => input.Ids.Contains(x.FieldId)); + if (isReferenced) + throw Oops.Bah("字段已被填写要求引用,不能删除"); + + await _db.Deleteable().In(input.Ids).ExecuteCommandAsync(); + } + + public Task> ListMaterialTypes() + { + return _db.Queryable() + .OrderBy(x => x.MaterialCode) + .ToListAsync(); + } + + public async Task SaveMaterialType(OnboardingMaterialTypeSaveInput input) + { + if (input.MaxFileCount <= 0) + throw Oops.Bah("最大文件数量必须大于0"); + + var materialCodeExists = await _db.Queryable() + .AnyAsync(x => x.MaterialCode == input.MaterialCode && x.Id != input.Id); + if (materialCodeExists) + throw Oops.Bah("资料编码已存在"); + + var entity = new ObMaterialType + { + Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + MaterialCode = input.MaterialCode, + MaterialName = input.MaterialName, + AllowedExtensions = input.AllowedExtensions, + MaxFileCount = input.MaxFileCount, + MaxFileSizeMb = input.MaxFileSizeMb, + Status = input.Status + }; + + if (input.Id.HasValue) + { + var exists = await _db.Queryable().AnyAsync(x => x.Id == input.Id.Value); + if (!exists) + throw Oops.Bah("资料类型不存在"); + + await _db.Updateable(entity).ExecuteCommandAsync(); + return; + } + + await _db.Insertable(entity).ExecuteCommandAsync(); + } + + public async Task DeleteMaterialTypes(BaseIdListInput input) + { + if (input.Ids.Count == 0) + return; + + var isReferenced = await _db.Queryable() + .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); + if (isReferenced) + throw Oops.Bah("资料类型已被资料要求引用,不能删除"); + + var hasUploads = await _db.Queryable() + .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId)); + if (hasUploads) + throw Oops.Bah("资料类型已有上传记录,不能删除"); + + await _db.Deleteable().In(input.Ids).ExecuteCommandAsync(); + } + + public Task> ListFormRequirements(long companyId) + { + return _db.Queryable() + .Where(x => x.CompanyId == companyId) + .OrderBy(x => x.SortCode) + .ToListAsync(); + } + + public async Task SaveFormRequirement(OnboardingFormRequirementSaveInput input) + { + await EnsureCompanyAndPosition(input.CompanyId, input.PositionId); + + var fieldExists = await _db.Queryable() + .AnyAsync(x => x.Id == input.FieldId && x.Status == "enable"); + if (!fieldExists) + throw Oops.Bah("动态字段不存在或未启用"); + + var requirementExists = await _db.Queryable() + .AnyAsync(x => x.CompanyId == input.CompanyId + && x.PositionId == input.PositionId + && x.FieldId == input.FieldId + && x.Id != input.Id); + if (requirementExists) + throw Oops.Bah("该公司、岗位和字段的要求已存在"); + + var entity = new ObFormRequirement + { + Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + CompanyId = input.CompanyId, + PositionId = input.PositionId, + FieldId = input.FieldId, + Required = input.Required, + SortCode = input.SortCode + }; + + if (input.Id.HasValue) + { + await _db.Updateable(entity).ExecuteCommandAsync(); + return; + } + + await _db.Insertable(entity).ExecuteCommandAsync(); + } + + public Task DeleteFormRequirements(BaseIdListInput input) + { + if (input.Ids.Count == 0) + return Task.CompletedTask; + + return _db.Deleteable().In(input.Ids).ExecuteCommandAsync(); + } + + public Task> ListMaterialRequirements(long companyId) + { + return _db.Queryable() + .Where(x => x.CompanyId == companyId) + .OrderBy(x => x.SortCode) + .ToListAsync(); + } + + public async Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input) + { + await EnsureCompanyAndPosition(input.CompanyId, input.PositionId); + + var materialTypeExists = await _db.Queryable() + .AnyAsync(x => x.Id == input.MaterialTypeId && x.Status == "enable"); + if (!materialTypeExists) + throw Oops.Bah("资料类型不存在或未启用"); + + var requirementExists = await _db.Queryable() + .AnyAsync(x => x.CompanyId == input.CompanyId + && x.PositionId == input.PositionId + && x.MaterialTypeId == input.MaterialTypeId + && x.Id != input.Id); + if (requirementExists) + throw Oops.Bah("该公司、岗位和资料类型的要求已存在"); + + var entity = new ObMaterialRequirement + { + Id = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + CompanyId = input.CompanyId, + PositionId = input.PositionId, + MaterialTypeId = input.MaterialTypeId, + Required = input.Required, + SortCode = input.SortCode + }; + + if (input.Id.HasValue) + { + await _db.Updateable(entity).ExecuteCommandAsync(); + return; + } + + await _db.Insertable(entity).ExecuteCommandAsync(); + } + + public Task DeleteMaterialRequirements(BaseIdListInput input) + { + if (input.Ids.Count == 0) + return Task.CompletedTask; + + return _db.Deleteable().In(input.Ids).ExecuteCommandAsync(); + } + + private async Task EnsureCompanyAndPosition(long companyId, long? positionId) + { + var companyExists = await _db.Queryable() + .AnyAsync(x => x.Id == companyId && x.Status == "enable"); + if (!companyExists) + throw Oops.Bah("公司不存在或未启用"); + + if (!positionId.HasValue) + return; + + var positionExists = await _db.Queryable() + .AnyAsync(x => x.Id == positionId.Value && x.CompanyId == companyId && x.Status == "enable"); + if (!positionExists) + throw Oops.Bah("岗位不存在、未启用或不属于该公司"); + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Lifecycle/IOnboardingTaskLifecycleService.cs b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/IOnboardingTaskLifecycleService.cs new file mode 100644 index 0000000..9a99104 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/IOnboardingTaskLifecycleService.cs @@ -0,0 +1,2 @@ +namespace SimpleAdmin.Application; +public interface IOnboardingTaskLifecycleService : ITransient { Task Archive(OnboardingAuditActionInput input); Task Revoke(OnboardingAuditActionInput input); } diff --git a/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs new file mode 100644 index 0000000..b15e638 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/Lifecycle/OnboardingTaskLifecycleService.cs @@ -0,0 +1,90 @@ +namespace SimpleAdmin.Application; + +public class OnboardingTaskLifecycleService : IOnboardingTaskLifecycleService +{ + private readonly ISqlSugarClient _db; + private readonly IConfigService _config; + + public OnboardingTaskLifecycleService(ISqlSugarClient db, IConfigService config) + { + _db = db; + _config = config; + } + + public async Task Archive(OnboardingAuditActionInput input) + { + var task = await Authorize(input.TaskId); + if (task.TaskStatus != OnboardingTaskStatus.Approved) + { + throw Oops.Bah("仅审核通过任务可归档"); + } + + task.TaskStatus = OnboardingTaskStatus.Archived; + task.ArchivedAt = DateTime.Now; + await _db.Updateable(task).ExecuteCommandAsync(); + await Log(task, OnboardingAuditAction.Archive, input.AuditOpinion); + } + + public async Task Revoke(OnboardingAuditActionInput input) + { + var task = await Authorize(input.TaskId); + if (task.TaskStatus == OnboardingTaskStatus.Approved || task.TaskStatus == OnboardingTaskStatus.Archived) + { + throw Oops.Bah("已通过或已归档任务不能撤销"); + } + + task.TaskStatus = OnboardingTaskStatus.Revoked; + await _db.Updateable(task).ExecuteCommandAsync(); + await _db.Updateable() + .SetColumns(item => new ObTaskLink + { + LinkStatus = OnboardingLinkStatus.Revoked, + RevokedAt = DateTime.Now + }) + .Where(item => item.TaskId == task.Id && item.LinkStatus == OnboardingLinkStatus.Active) + .ExecuteCommandAsync(); + await Log(task, OnboardingAuditAction.Revoke, input.AuditOpinion); + } + + private async Task Authorize(long taskId) + { + var task = await _db.Queryable().InSingleAsync(taskId) + ?? throw Oops.Bah("入职任务不存在"); + var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds") + ?? throw Oops.Bah("请配置入职审核人"); + + using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue); + var reviewerIds = new List(); + if (configJson.RootElement.TryGetProperty("all", out var allReviewers)) + { + reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (configJson.RootElement.TryGetProperty("company", out var companyReviewers) + && companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers)) + { + reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (!reviewerIds.Contains(UserManager.UserId)) + { + throw Oops.Bah("您不是该入职任务的审核人"); + } + + return task; + } + + private Task Log(ObOnboardingTask task, string action, string opinion) + { + return _db.Insertable(new ObTaskAudit + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + AuditRound = task.SubmitCount, + AuditAction = action, + AuditorUserId = UserManager.UserId, + AuditOpinion = opinion, + AuditedAt = DateTime.Now + }).ExecuteCommandAsync(); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/MaterialReview/Dto/OnboardingMaterialReviewInput.cs b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/Dto/OnboardingMaterialReviewInput.cs new file mode 100644 index 0000000..de6241d --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/Dto/OnboardingMaterialReviewInput.cs @@ -0,0 +1,10 @@ +namespace SimpleAdmin.Application; + +public class OnboardingMaterialReviewInput +{ + [IdNotNull(ErrorMessage = "任务资料Id不能为空")] + public long TaskMaterialId { get; set; } + + public string ReviewStatus { get; set; } + public string ReviewRemark { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/MaterialReview/IOnboardingMaterialReviewService.cs b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/IOnboardingMaterialReviewService.cs new file mode 100644 index 0000000..1820e3d --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/IOnboardingMaterialReviewService.cs @@ -0,0 +1,2 @@ +namespace SimpleAdmin.Application; +public interface IOnboardingMaterialReviewService : ITransient { Task Review(OnboardingMaterialReviewInput input); } diff --git a/SimpleAdmin.Application/Services/Onboarding/MaterialReview/OnboardingMaterialReviewService.cs b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/OnboardingMaterialReviewService.cs new file mode 100644 index 0000000..04daeec --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/MaterialReview/OnboardingMaterialReviewService.cs @@ -0,0 +1,65 @@ +namespace SimpleAdmin.Application; + +public class OnboardingMaterialReviewService : IOnboardingMaterialReviewService +{ + private readonly ISqlSugarClient _db; + private readonly IConfigService _config; + + public OnboardingMaterialReviewService(ISqlSugarClient db, IConfigService config) + { + _db = db; + _config = config; + } + + public async Task Review(OnboardingMaterialReviewInput input) + { + if (input.ReviewStatus != TaskMaterialReviewStatus.Passed + && input.ReviewStatus != TaskMaterialReviewStatus.NeedSupplement) + { + throw Oops.Bah("资料审核状态不正确"); + } + + if (input.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement + && string.IsNullOrWhiteSpace(input.ReviewRemark)) + { + throw Oops.Bah("请填写需补充原因"); + } + + var material = await _db.Queryable().InSingleAsync(input.TaskMaterialId) + ?? throw Oops.Bah("任务资料不存在"); + var task = await _db.Queryable().InSingleAsync(material.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + if (task.TaskStatus != OnboardingTaskStatus.Reviewing) + { + throw Oops.Bah("当前任务不在审核中"); + } + + await EnsureReviewer(task); + material.ReviewStatus = input.ReviewStatus; + material.ReviewRemark = input.ReviewRemark; + await _db.Updateable(material).ExecuteCommandAsync(); + } + + private async Task EnsureReviewer(ObOnboardingTask task) + { + var config = await _config.GetByConfigKey(CateGoryConst.CONFIG_BIZ_DEFINE, "onboard.reviewer.userIds") + ?? throw Oops.Bah("请配置入职审核人"); + using var configJson = global::System.Text.Json.JsonDocument.Parse(config.ConfigValue); + var reviewerIds = new List(); + if (configJson.RootElement.TryGetProperty("all", out var allReviewers)) + { + reviewerIds.AddRange(allReviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (configJson.RootElement.TryGetProperty("company", out var companyReviewers) + && companyReviewers.TryGetProperty(task.CompanyId.ToString(), out var reviewers)) + { + reviewerIds.AddRange(reviewers.EnumerateArray().Select(item => item.GetInt64())); + } + + if (!reviewerIds.Contains(UserManager.UserId)) + { + throw Oops.Bah("您不是该入职任务的审核人"); + } + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs b/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs index 4cbdf7f..134f484 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Position/OnboardingPositionService.cs @@ -6,16 +6,27 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo { return await Context.Queryable() .WhereIF(input.CompanyId.HasValue, it => it.CompanyId == input.CompanyId.Value) - .WhereIF(!string.IsNullOrWhiteSpace(input.PositionName), it => it.PositionName.Contains(input.PositionName) || it.PositionCode.Contains(input.PositionName)) + .WhereIF( + !string.IsNullOrWhiteSpace(input.PositionName), + it => it.PositionName.Contains(input.PositionName) || it.PositionCode.Contains(input.PositionName)) .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status) .OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}") .OrderBy(it => it.PositionName) .ToPagedListAsync(input.PageNum, input.PageSize); } - public Task> ListEnabled(long companyId) => Context.Queryable().Where(it => it.CompanyId == companyId && it.Status == "enable").OrderBy(it => it.PositionName).ToListAsync(); + public Task> ListEnabled(long companyId) + { + return Context.Queryable() + .Where(it => it.CompanyId == companyId && it.Status == "enable") + .OrderBy(it => it.PositionName) + .ToListAsync(); + } - public async Task Detail(BaseIdInput input) => await GetByIdAsync(input.Id) ?? throw Oops.Bah("岗位不存在"); + public async Task Detail(BaseIdInput input) + { + return await GetByIdAsync(input.Id) ?? throw Oops.Bah("岗位不存在"); + } public async Task Add(OnboardingPositionAddInput input) { @@ -34,19 +45,34 @@ public class OnboardingPositionService : DbRepository, IOnboardingPo public async Task Delete(BaseIdListInput input) { - if (input.Ids.Count == 0) return; - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.PositionId))) throw Oops.Bah("岗位已有入职任务,不能删除"); - if (await Context.Queryable().AnyAsync(it => input.Ids.Contains(it.PositionId))) throw Oops.Bah("岗位已有正式员工,不能删除"); + if (input.Ids.Count == 0) + return; + + var hasTasks = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.PositionId)); + if (hasTasks) + throw Oops.Bah("岗位已有入职任务,不能删除"); + + var hasEmployees = await Context.Queryable() + .AnyAsync(it => input.Ids.Contains(it.PositionId)); + if (hasEmployees) + throw Oops.Bah("岗位已有正式员工,不能删除"); + await DeleteByIdsAsync(input.Ids.Cast().ToArray()); } private async Task EnsureCompanyExists(long companyId) { - if (!await Context.Queryable().AnyAsync(it => it.Id == companyId)) throw Oops.Bah("所属公司不存在或已删除"); + var exists = await Context.Queryable().AnyAsync(it => it.Id == companyId); + if (!exists) + throw Oops.Bah("所属公司不存在或已删除"); } private async Task EnsureCodeUnique(long companyId, string code, long id) { - if (await Context.Queryable().AnyAsync(it => it.CompanyId == companyId && it.PositionCode == code && it.Id != id)) throw Oops.Bah("该公司下岗位编码已存在"); + var exists = await Context.Queryable() + .AnyAsync(it => it.CompanyId == companyId && it.PositionCode == code && it.Id != id); + if (exists) + throw Oops.Bah("该公司下岗位编码已存在"); } } diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs new file mode 100644 index 0000000..511ed08 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingMaterialUploadValidationInput.cs @@ -0,0 +1,9 @@ +namespace SimpleAdmin.Application; + +public class OnboardingMaterialUploadValidationInput +{ + public string Token { get; set; } + public long MaterialTypeId { get; set; } + public string FileName { get; set; } + public long FileLength { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs new file mode 100644 index 0000000..b117b70 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/Dto/OnboardingPublicSubmissionInput.cs @@ -0,0 +1,38 @@ +namespace SimpleAdmin.Application; + +public class OnboardingDynamicValueInput +{ + public long FieldId { get; set; } + public string FieldValue { get; set; } +} + +public class OnboardingSaveDynamicValuesInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + public List Values { get; set; } = new(); +} + +public class OnboardingSaveMaterialInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + public long MaterialTypeId { get; set; } + public long FileId { get; set; } +} + +public class OnboardingConfirmContractInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + public long TaskContractId { get; set; } +} + +public class OnboardingSubmitInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/IOnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/IOnboardingPublicSubmissionService.cs new file mode 100644 index 0000000..c7e7c00 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/IOnboardingPublicSubmissionService.cs @@ -0,0 +1,10 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingPublicSubmissionService : ITransient +{ + Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input); + Task SaveMaterial(OnboardingSaveMaterialInput input); + Task ConfirmContract(OnboardingConfirmContractInput input); + Task Submit(OnboardingSubmitInput input); + Task ValidateMaterialUpload(OnboardingMaterialUploadValidationInput input); +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs new file mode 100644 index 0000000..49104a0 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs @@ -0,0 +1,265 @@ +namespace SimpleAdmin.Application; + +public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionService +{ + private readonly ISqlSugarClient _db; + private readonly IOnboardingContractSnapshotService _snapshotService; + + public OnboardingPublicSubmissionService( + ISqlSugarClient db, + IOnboardingContractSnapshotService snapshotService) + { + _db = db; + _snapshotService = snapshotService; + } + + public async Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input) + { + var task = await GetEditableTask(input.Token); + var requirements = await GetEffectiveFormRequirements(task); + var fieldIds = input.Values.Select(item => item.FieldId).Distinct().ToList(); + var hasInapplicableField = fieldIds.Any(id => requirements.All(requirement => requirement.FieldId != id)); + if (hasInapplicableField) + throw Oops.Bah("填写字段不适用于当前公司或岗位"); + + var fields = await _db.Queryable() + .Where(field => fieldIds.Contains(field.Id) && field.Status == "enable") + .ToListAsync(); + foreach (var value in input.Values) + { + var field = fields.FirstOrDefault(item => item.Id == value.FieldId) + ?? throw Oops.Bah("填写字段不存在或已停用"); + var old = await _db.Queryable() + .FirstAsync(item => item.TaskId == task.Id && item.FieldId == field.Id); + var row = new ObTaskFormValue + { + Id = old?.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + FieldId = field.Id, + FieldCode = field.FieldCode, + FieldName = field.FieldName, + FieldValue = value.FieldValue, + SubmitRound = task.SubmitCount + 1 + }; + + if (old == null) + { + await _db.Insertable(row).ExecuteCommandAsync(); + } + else + { + await _db.Updateable(row).ExecuteCommandAsync(); + } + } + } + + public async Task SaveMaterial(OnboardingSaveMaterialInput input) + { + var task = await GetEditableTask(input.Token); + var requirements = await GetEffectiveMaterialRequirements(task); + var materialIsApplicable = requirements.Any(requirement => requirement.MaterialTypeId == input.MaterialTypeId); + if (!materialIsApplicable) + throw Oops.Bah("资料类型不适用于当前公司或岗位"); + + var material = await _db.Queryable() + .FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable") + ?? throw Oops.Bah("资料类型不存在或已停用"); + var fileExists = await _db.Queryable().AnyAsync(item => item.Id == input.FileId); + if (!fileExists) + throw Oops.Bah("上传文件不存在"); + + var materialCount = await _db.Queryable() + .CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId); + if (materialCount >= material.MaxFileCount) + throw Oops.Bah("该资料已达到最大上传数量"); + + await _db.Insertable(new ObTaskMaterial + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + MaterialTypeId = material.Id, + MaterialName = material.MaterialName, + FileId = input.FileId, + SubmitRound = task.SubmitCount + 1, + UploadedAt = DateTime.Now + }).ExecuteCommandAsync(); + } + + public async Task ConfirmContract(OnboardingConfirmContractInput input) + { + var task = await GetEditableTask(input.Token); + var contract = await _db.Queryable() + .FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == task.Id) + ?? throw Oops.Bah("合同不存在"); + + contract.ReadAt ??= DateTime.Now; + contract.ConfirmedAt = DateTime.Now; + contract.ContractStatus = TaskContractStatus.Confirmed; + await _db.Updateable(contract).ExecuteCommandAsync(); + } + + public async Task Submit(OnboardingSubmitInput input) + { + var task = await GetEditableTask(input.Token); + var info = await _db.Queryable() + .FirstAsync(item => item.TaskId == task.Id); + var basicInfoIncomplete = info == null + || string.IsNullOrWhiteSpace(info.EmployeeName) + || string.IsNullOrWhiteSpace(info.Phone) + || string.IsNullOrWhiteSpace(info.IdCardNumber); + if (basicInfoIncomplete) + throw Oops.Bah("请先完整填写基本信息"); + + await _snapshotService.Build(task.Id, info); + + var requiredFields = (await GetEffectiveFormRequirements(task)) + .Where(item => item.Required) + .Select(item => item.FieldId) + .ToList(); + var values = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(); + var hasMissingRequiredField = requiredFields.Any( + id => values.All(item => item.FieldId != id || string.IsNullOrWhiteSpace(item.FieldValue))); + if (hasMissingRequiredField) + throw Oops.Bah("仍有必填动态信息未填写"); + + var requiredMaterials = (await GetEffectiveMaterialRequirements(task)) + .Where(item => item.Required) + .Select(item => item.MaterialTypeId) + .ToList(); + var materials = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(); + var hasMissingRequiredMaterial = requiredMaterials.Any( + id => materials.All(item => item.MaterialTypeId != id)); + if (hasMissingRequiredMaterial) + throw Oops.Bah("仍有必传资料未上传"); + + await EnsureRequiredContractVariables(task); + + var hasUnconfirmedRequiredContract = await _db.Queryable() + .AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null); + if (hasUnconfirmedRequiredContract) + throw Oops.Bah("请确认阅读全部必签合同"); + + task.TaskStatus = OnboardingTaskStatus.Reviewing; + task.SubmitCount++; + task.LastSubmittedAt = DateTime.Now; + task.RejectReason = null; + await _db.Updateable(task).ExecuteCommandAsync(); + + await _db.Insertable(new ObTaskAudit + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = task.Id, + AuditRound = task.SubmitCount, + AuditAction = OnboardingAuditAction.Submit, + AuditOpinion = "员工提交入职资料", + SnapshotJson = global::System.Text.Json.JsonSerializer.Serialize(new { info, values, materials }), + AuditedAt = DateTime.Now + }).ExecuteCommandAsync(); + } + + public async Task ValidateMaterialUpload(OnboardingMaterialUploadValidationInput input) + { + var task = await GetEditableTask(input.Token); + var requirements = await GetEffectiveMaterialRequirements(task); + var materialIsApplicable = requirements.Any(requirement => requirement.MaterialTypeId == input.MaterialTypeId); + if (!materialIsApplicable) + throw Oops.Bah("资料类型不适用于当前公司或岗位"); + + var material = await _db.Queryable() + .FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable") + ?? throw Oops.Bah("资料类型不存在或已停用"); + var suffix = Path.GetExtension(input.FileName).TrimStart('.').ToLowerInvariant(); + var allowedExtensions = (material.AllowedExtensions ?? string.Empty) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(item => item.TrimStart('.').ToLowerInvariant()); + var suffixNotAllowed = allowedExtensions.Any() && !allowedExtensions.Contains(suffix); + if (suffixNotAllowed) + throw Oops.Bah("该资料不允许上传此文件类型"); + + var fileTooLarge = material.MaxFileSizeMb.HasValue + && input.FileLength > material.MaxFileSizeMb.Value * 1024L * 1024L; + if (fileTooLarge) + throw Oops.Bah("文件超过允许的大小"); + + var materialCount = await _db.Queryable() + .CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId); + if (materialCount >= material.MaxFileCount) + throw Oops.Bah("该资料已达到最大上传数量"); + } + + private async Task EnsureRequiredContractVariables(ObOnboardingTask task) + { + var contracts = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(); + var contractIds = contracts.Select(contract => contract.Id).ToList(); + var templateVersionIds = contracts.Select(contract => contract.TemplateVersionId).ToList(); + var variables = await _db.Queryable() + .Where(item => contractIds.Contains(item.TaskContractId)) + .ToListAsync(); + var definitions = await _db.Queryable() + .Where(item => templateVersionIds.Contains(item.TemplateVersionId) && item.Required) + .ToListAsync(); + var missing = contracts + .SelectMany(contract => definitions + .Where(definition => definition.TemplateVersionId == contract.TemplateVersionId) + .Select(definition => new { contract, definition })) + .FirstOrDefault(item => string.IsNullOrWhiteSpace( + variables.FirstOrDefault(variable => variable.TaskContractId == item.contract.Id + && variable.VariableCode == item.definition.VariableCode) + ?.VariableValue)); + if (missing != null) + throw Oops.Bah($"合同“{missing.contract.ContractName}”的必填变量“{missing.definition.VariableName}”尚未填写"); + } + + private async Task> GetEffectiveFormRequirements(ObOnboardingTask task) + { + var requirements = await _db.Queryable() + .Where(item => item.CompanyId == task.CompanyId + && (item.PositionId == null || item.PositionId == task.PositionId)) + .ToListAsync(); + + return requirements + .GroupBy(item => item.FieldId) + .Select(group => group.FirstOrDefault(item => item.PositionId == task.PositionId) ?? group.First()) + .ToList(); + } + + private async Task> GetEffectiveMaterialRequirements(ObOnboardingTask task) + { + var requirements = await _db.Queryable() + .Where(item => item.CompanyId == task.CompanyId + && (item.PositionId == null || item.PositionId == task.PositionId)) + .ToListAsync(); + + return requirements + .GroupBy(item => item.MaterialTypeId) + .Select(group => group.FirstOrDefault(item => item.PositionId == task.PositionId) ?? group.First()) + .ToList(); + } + + private async Task GetEditableTask(string token) + { + var hash = Convert.ToHexString( + global::System.Security.Cryptography.SHA256.HashData( + global::System.Text.Encoding.UTF8.GetBytes(token))); + var link = await _db.Queryable().FirstAsync(item => item.TokenHash == hash) + ?? throw Oops.Bah("链接不存在"); + var linkUnavailable = link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now; + if (linkUnavailable) + throw Oops.Bah("链接已失效"); + + var task = await _db.Queryable().InSingleAsync(link.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + var taskNotEditable = task.TaskStatus != OnboardingTaskStatus.PendingFill + && task.TaskStatus != OnboardingTaskStatus.Rejected; + if (taskNotEditable) + throw Oops.Bah("当前任务不可编辑"); + + return task; + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs new file mode 100644 index 0000000..3941de1 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/IOnboardingPublicTaskService.cs @@ -0,0 +1,8 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingPublicTaskService : ITransient +{ + Task Detail(OnboardingTaskPublicAccessInput input); + Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input); + Task GetContractSourceFileId(OnboardingPublicContractSourceInput input); +} diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs new file mode 100644 index 0000000..efb41e2 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs @@ -0,0 +1,156 @@ +namespace SimpleAdmin.Application; + +/// +/// 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。 +/// +public class OnboardingPublicTaskService : IOnboardingPublicTaskService +{ + private readonly ISqlSugarClient _db; + private readonly IOnboardingContractSnapshotService _snapshotService; + + public OnboardingPublicTaskService( + ISqlSugarClient db, + IOnboardingContractSnapshotService snapshotService) + { + _db = db; + _snapshotService = snapshotService; + } + + public async Task Detail(OnboardingTaskPublicAccessInput input) + { + var link = await GetActiveLink(input.Token); + var task = await _db.Queryable().InSingleAsync(link.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + + link.AccessCount++; + link.LastAccessedAt = DateTime.Now; + await _db.Updateable(link).ExecuteCommandAsync(); + + var formRequirements = await GetEffectiveFormRequirements(task); + var materialRequirements = await GetEffectiveMaterialRequirements(task); + var fields = await _db.Queryable() + .Where(x => formRequirements.Select(r => r.FieldId).Contains(x.Id) && x.Status == "enable") + .ToListAsync(); + var materialTypes = await _db.Queryable() + .Where(x => materialRequirements.Select(r => r.MaterialTypeId).Contains(x.Id) && x.Status == "enable") + .ToListAsync(); + + return new OnboardingTaskPublicDetailOutput + { + Task = task, + EmployeeInfo = await _db.Queryable().FirstAsync(it => it.TaskId == task.Id), + Contracts = await _db.Queryable() + .Where(it => it.TaskId == task.Id) + .ToListAsync(), + DynamicValues = await _db.Queryable() + .Where(x => x.TaskId == task.Id) + .ToListAsync(), + Materials = await _db.Queryable() + .Where(x => x.TaskId == task.Id) + .ToListAsync(), + FormFields = formRequirements + .OrderBy(x => x.SortCode) + .Select(requirement => fields.FirstOrDefault(field => field.Id == requirement.FieldId)) + .Where(field => field != null) + .Select(field => new OnboardingPublicFormFieldOutput + { + FieldId = field.Id, + FieldCode = field.FieldCode, + FieldName = field.FieldName, + DataType = field.DataType, + OptionsJson = field.OptionsJson, + Required = formRequirements.First(requirement => requirement.FieldId == field.Id).Required + }) + .ToList(), + MaterialRequirements = materialRequirements + .OrderBy(x => x.SortCode) + .Select(requirement => materialTypes.FirstOrDefault(material => material.Id == requirement.MaterialTypeId)) + .Where(material => material != null) + .Select(material => new OnboardingPublicMaterialOutput + { + MaterialTypeId = material.Id, + MaterialName = material.MaterialName, + AllowedExtensions = material.AllowedExtensions, + MaxFileCount = material.MaxFileCount, + Required = materialRequirements.First(requirement => requirement.MaterialTypeId == material.Id).Required + }) + .ToList() + }; + } + + public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input) + { + var link = await GetActiveLink(input.Token); + var task = await _db.Queryable().InSingleAsync(link.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + if (task.TaskStatus != OnboardingTaskStatus.PendingFill && task.TaskStatus != OnboardingTaskStatus.Rejected) + throw Oops.Bah("当前任务不可编辑"); + + var old = await _db.Queryable().FirstAsync(it => it.TaskId == task.Id); + var entity = input.Adapt(); + entity.TaskId = task.Id; + + if (old == null) + { + entity.Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); + await _db.Insertable(entity).ExecuteCommandAsync(); + } + else + { + entity.Id = old.Id; + await _db.Updateable(entity).ExecuteCommandAsync(); + } + + await _snapshotService.Build(task.Id, entity); + } + + public async Task GetContractSourceFileId(OnboardingPublicContractSourceInput input) + { + var link = await GetActiveLink(input.Token); + var contract = await _db.Queryable() + .FirstAsync(x => x.Id == input.TaskContractId && x.TaskId == link.TaskId) + ?? throw Oops.Bah("合同不存在"); + var version = await _db.Queryable().InSingleAsync(contract.TemplateVersionId) + ?? throw Oops.Bah("合同模板版本不存在"); + + return version.SourceDocxFileId; + } + + private async Task> GetEffectiveFormRequirements(ObOnboardingTask task) + { + var requirements = await _db.Queryable() + .Where(x => x.CompanyId == task.CompanyId && (x.PositionId == null || x.PositionId == task.PositionId)) + .ToListAsync(); + + return requirements + .GroupBy(x => x.FieldId) + .Select(group => group.FirstOrDefault(x => x.PositionId == task.PositionId) ?? group.First()) + .ToList(); + } + + private async Task> GetEffectiveMaterialRequirements(ObOnboardingTask task) + { + var requirements = await _db.Queryable() + .Where(x => x.CompanyId == task.CompanyId && (x.PositionId == null || x.PositionId == task.PositionId)) + .ToListAsync(); + + return requirements + .GroupBy(x => x.MaterialTypeId) + .Select(group => group.FirstOrDefault(x => x.PositionId == task.PositionId) ?? group.First()) + .ToList(); + } + + private async Task GetActiveLink(string token) + { + var hash = Convert.ToHexString( + global::System.Security.Cryptography.SHA256.HashData( + global::System.Text.Encoding.UTF8.GetBytes(token))); + var link = await _db.Queryable().FirstAsync(it => it.TokenHash == hash) + ?? throw Oops.Bah("链接不存在"); + + if (link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now) + throw Oops.Bah("链接已失效"); + + return link; + } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs index 69521b7..ace1cf7 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskPublicInput.cs @@ -1,11 +1,61 @@ namespace SimpleAdmin.Application; -public class OnboardingTaskPublicAccessInput { [global::System.ComponentModel.DataAnnotations.Required] public string Token { get; set; } } +public class OnboardingTaskPublicAccessInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } +} + +public class OnboardingPublicContractSourceInput +{ + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + [IdNotNull(ErrorMessage = "任务合同Id不能为空")] + public long TaskContractId { get; set; } +} + public class OnboardingTaskPublicSaveInput : ObTaskEmployeeInfo { - [global::System.ComponentModel.DataAnnotations.Required] public string Token { get; set; } - [global::System.ComponentModel.DataAnnotations.Required] public new string EmployeeName { get; set; } - [global::System.ComponentModel.DataAnnotations.Required] public new string Phone { get; set; } - [global::System.ComponentModel.DataAnnotations.Required] public new string IdCardNumber { get; set; } + [global::System.ComponentModel.DataAnnotations.Required] + public string Token { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required] + public new string EmployeeName { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required] + public new string Phone { get; set; } + + [global::System.ComponentModel.DataAnnotations.Required] + public new string IdCardNumber { get; set; } +} + +public class OnboardingPublicFormFieldOutput +{ + public long FieldId { get; set; } + public string FieldCode { get; set; } + public string FieldName { get; set; } + public string DataType { get; set; } + public string OptionsJson { get; set; } + public bool Required { get; set; } +} + +public class OnboardingPublicMaterialOutput +{ + public long MaterialTypeId { get; set; } + public string MaterialName { get; set; } + public string AllowedExtensions { get; set; } + public int MaxFileCount { get; set; } + public bool Required { get; set; } +} + +public class OnboardingTaskPublicDetailOutput +{ + public ObOnboardingTask Task { get; set; } + public ObTaskEmployeeInfo EmployeeInfo { get; set; } + public List Contracts { get; set; } + public List DynamicValues { get; set; } + public List FormFields { get; set; } + public List MaterialRequirements { get; set; } + public List Materials { get; set; } } -public class OnboardingTaskPublicDetailOutput { public ObOnboardingTask Task { get; set; } public ObTaskEmployeeInfo EmployeeInfo { get; set; } public List Contracts { get; set; } } diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs index bbf093d..b8e57e2 100644 --- a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs +++ b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs @@ -4,31 +4,124 @@ public class OnboardingTaskService : IOnboardingTaskService { private readonly ISqlSugarClient _db; private readonly IConfigService _configService; - public OnboardingTaskService(ISqlSugarClient db, IConfigService configService) { _db = db; _configService = configService; } + private readonly IOnboardingContractSnapshotService _snapshotService; + + public OnboardingTaskService( + ISqlSugarClient db, + IConfigService configService, + IOnboardingContractSnapshotService snapshotService) + { + _db = db; + _configService = configService; + _snapshotService = snapshotService; + } public async Task Create(OnboardingTaskCreateInput input) { - if (!await _db.Queryable().AnyAsync(it => it.Id == input.CompanyId && it.Status == "enable")) throw Oops.Bah("公司不存在或未启用"); - if (!await _db.Queryable().AnyAsync(it => it.Id == input.PositionId && it.CompanyId == input.CompanyId && it.Status == "enable")) throw Oops.Bah("岗位不存在、未启用或不属于该公司"); - var scopes = await _db.Queryable().Where(it => it.CompanyId == input.CompanyId && (it.PositionId == null || it.PositionId == input.PositionId)).ToListAsync(); - if (scopes.Count == 0) throw Oops.Bah("该公司和岗位尚未配置合同适用范围"); + 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 scopes = await _db.Queryable() + .Where(item => item.CompanyId == input.CompanyId + && (item.PositionId == null || item.PositionId == input.PositionId)) + .ToListAsync(); + + // 同一版本同时存在“全公司”和“指定岗位”范围时,岗位专属配置优先,避免同一合同生成两次。 + scopes = scopes + .GroupBy(item => item.TemplateVersionId) + .Select(group => group.FirstOrDefault(item => item.PositionId == input.PositionId) ?? group.First()) + .ToList(); + if (scopes.Count == 0) + { + throw Oops.Bah("该公司和岗位尚未配置合同适用范围"); + } + var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); - 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 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 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, AssignedReviewerUserId = input.AssignedReviewerUserId, LinkExpireAt = expiresAt, TaskStatus = OnboardingTaskStatus.PendingFill, Remark = input.Remark }; + var task = new ObOnboardingTask + { + Id = taskId, + TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}", + CompanyId = input.CompanyId, + PositionId = input.PositionId, + AssignedReviewerUserId = input.AssignedReviewerUserId, + LinkExpireAt = expiresAt, + TaskStatus = OnboardingTaskStatus.PendingFill, + Remark = input.Remark + }; + await _db.Ado.BeginTranAsync(); try { await _db.Insertable(task).ExecuteCommandAsync(); - await _db.Insertable(new ObTaskLink { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(global::System.Text.Encoding.UTF8.GetBytes(token))), ExpiresAt = expiresAt }).ExecuteCommandAsync(); - var templateIds = scopes.Select(it => it.TemplateId).Distinct().ToList(); - var templates = await _db.Queryable().Where(it => templateIds.Contains(it.Id)).ToListAsync(); - await _db.Insertable(scopes.Select(it => new ObTaskContract { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TaskId = taskId, TemplateId = it.TemplateId, TemplateVersionId = it.TemplateVersionId, ContractName = templates.First(x => x.Id == it.TemplateId).TemplateName, ContractType = templates.First(x => x.Id == it.TemplateId).ContractType, Required = it.Required }).ToList()).ExecuteCommandAsync(); + await _db.Insertable(new ObTaskLink + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = taskId, + TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData( + global::System.Text.Encoding.UTF8.GetBytes(token))), + ExpiresAt = expiresAt + }).ExecuteCommandAsync(); + + var templateIds = scopes.Select(item => item.TemplateId).Distinct().ToList(); + var templates = await _db.Queryable() + .Where(item => templateIds.Contains(item.Id)) + .ToListAsync(); + var contracts = scopes.Select(scope => + { + var template = templates.First(item => item.Id == scope.TemplateId); + return new ObTaskContract + { + Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), + TaskId = taskId, + TemplateId = scope.TemplateId, + TemplateVersionId = scope.TemplateVersionId, + ContractName = template.TemplateName, + ContractType = template.ContractType, + Required = scope.Required + }; + }).ToList(); + + await _db.Insertable(contracts).ExecuteCommandAsync(); + await _snapshotService.Build(taskId); await _db.Ado.CommitTranAsync(); } - catch { await _db.Ado.RollbackTranAsync(); throw; } - return new OnboardingTaskCreatedOutput { TaskId = taskId, TaskNo = task.TaskNo, LinkToken = token, ExpiresAt = expiresAt }; + catch + { + await _db.Ado.RollbackTranAsync(); + throw; + } + + return new OnboardingTaskCreatedOutput + { + TaskId = taskId, + TaskNo = task.TaskNo, + LinkToken = token, + ExpiresAt = expiresAt + }; } } diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/Dto/OnboardingTaskContractVariableInput.cs b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/Dto/OnboardingTaskContractVariableInput.cs new file mode 100644 index 0000000..4c8819a --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/Dto/OnboardingTaskContractVariableInput.cs @@ -0,0 +1,43 @@ +namespace SimpleAdmin.Application; + +public class OnboardingTaskContractVariableQueryInput +{ + [IdNotNull(ErrorMessage = "入职任务Id不能为空")] + public long TaskId { get; set; } +} + +public class OnboardingTaskContractVariableSaveInput +{ + [IdNotNull(ErrorMessage = "任务合同Id不能为空")] + public long TaskContractId { get; set; } + [global::System.ComponentModel.DataAnnotations.Required(ErrorMessage = "变量不能为空")] + public List Values { get; set; } = new List(); +} + +public class OnboardingTaskContractVariableValueInput +{ + [IdNotNull(ErrorMessage = "变量Id不能为空")] + public long Id { get; set; } + public string VariableValue { get; set; } +} + +public class OnboardingTaskContractVariableOutput +{ + public long TaskContractId { get; set; } + public string ContractName { get; set; } + public string ContractType { get; set; } + public List Variables { get; set; } = new List(); +} + +public class OnboardingTaskContractVariableItemOutput +{ + public long Id { get; set; } + public string VariableCode { get; set; } + public string VariableName { get; set; } + public string VariableType { get; set; } + public string DataType { get; set; } + public string VariableValue { get; set; } + public bool Required { get; set; } + public bool HrEditable { get; set; } + public bool Editable { get; set; } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/IOnboardingTaskContractVariableService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/IOnboardingTaskContractVariableService.cs new file mode 100644 index 0000000..b1ce963 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/IOnboardingTaskContractVariableService.cs @@ -0,0 +1,7 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingTaskContractVariableService : ITransient +{ + Task> List(OnboardingTaskContractVariableQueryInput input); + Task Save(OnboardingTaskContractVariableSaveInput input); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs new file mode 100644 index 0000000..5fe51fc --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs @@ -0,0 +1,139 @@ +namespace SimpleAdmin.Application; + +/// +/// 管理端在链接发送前填写合同薪资、补贴等人事变量。 +/// +public class OnboardingTaskContractVariableService : IOnboardingTaskContractVariableService +{ + private readonly ISqlSugarClient _db; + + public OnboardingTaskContractVariableService(ISqlSugarClient db) + => _db = db; + + public async Task> List( + OnboardingTaskContractVariableQueryInput input) + { + var task = await _db.Queryable() + .InSingleAsync(input.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + + var contracts = await _db.Queryable() + .Where(x => x.TaskId == task.Id) + .ToListAsync(); + + var contractIds = contracts.Select(x => x.Id).ToList(); + var templateVersionIds = contracts.Select(x => x.TemplateVersionId).ToList(); + + var variables = await _db.Queryable() + .Where(x => contractIds.Contains(x.TaskContractId)) + .ToListAsync(); + + var definitions = await _db.Queryable() + .Where(x => templateVersionIds.Contains(x.TemplateVersionId)) + .ToListAsync(); + + return contracts.Select(contract => new OnboardingTaskContractVariableOutput + { + TaskContractId = contract.Id, + ContractName = contract.ContractName, + ContractType = contract.ContractType, + + Variables = variables + .Where(variable => variable.TaskContractId == contract.Id) + .OrderBy(variable => variable.VariableCode) + .Select(variable => + { + var definition = definitions.FirstOrDefault(x => + x.TemplateVersionId == contract.TemplateVersionId && + x.VariableCode == variable.VariableCode); + + var hrEditable = definition?.HrEditable == true; + + return new OnboardingTaskContractVariableItemOutput + { + Id = variable.Id, + VariableCode = variable.VariableCode, + VariableName = variable.VariableName, + VariableType = variable.VariableType, + DataType = variable.DataType, + VariableValue = variable.VariableValue, + Required = definition?.Required == true, + HrEditable = hrEditable, + Editable = + variable.VariableType == ContractVariableType.Contract || + (variable.VariableType == ContractVariableType.Company && hrEditable) + }; + }) + .ToList() + }).ToList(); + } + + public async Task Save(OnboardingTaskContractVariableSaveInput input) + { + var contract = await _db.Queryable() + .InSingleAsync(input.TaskContractId) + ?? throw Oops.Bah("任务合同不存在"); + + var task = await _db.Queryable() + .InSingleAsync(contract.TaskId) + ?? throw Oops.Bah("入职任务不存在"); + + if (task.TaskStatus != OnboardingTaskStatus.PendingFill && + task.TaskStatus != OnboardingTaskStatus.Rejected) + { + throw Oops.Bah("任务已提交审核,不能再修改合同参数"); + } + + var valueIds = input.Values + .Select(x => x.Id) + .Distinct() + .ToList(); + + var rows = await _db.Queryable() + .Where(x => + x.TaskContractId == contract.Id && + valueIds.Contains(x.Id)) + .ToListAsync(); + + if (rows.Count != valueIds.Count) + { + throw Oops.Bah("包含不属于该合同的变量"); + } + + var variableCodes = rows + .Select(x => x.VariableCode) + .ToList(); + + var definitions = await _db.Queryable() + .Where(x => + x.TemplateVersionId == contract.TemplateVersionId && + variableCodes.Contains(x.VariableCode)) + .ToListAsync(); + + foreach (var row in rows) + { + var definition = definitions.FirstOrDefault(x => + x.VariableCode == row.VariableCode) + ?? throw Oops.Bah("合同变量定义不存在"); + + if (row.VariableType != ContractVariableType.Contract && + !(row.VariableType == ContractVariableType.Company && + definition.HrEditable)) + { + throw Oops.Bah($"变量“{row.VariableName}”不允许人事手工修改"); + } + + var inputValue = input.Values.First(x => x.Id == row.Id); + + row.VariableValue = inputValue.VariableValue; + row.SourceType = ContractVariableSourceType.HrInput; + row.IsManualOverride = true; + } + + if (rows.Count > 0) + { + await _db.Updateable(rows) + .ExecuteCommandAsync(); + } + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/Dto/OnboardingTaskQueryInput.cs b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/Dto/OnboardingTaskQueryInput.cs new file mode 100644 index 0000000..2847fe2 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/Dto/OnboardingTaskQueryInput.cs @@ -0,0 +1,17 @@ +namespace SimpleAdmin.Application; + +public class OnboardingTaskPageInput : BasePageInput +{ + public string TaskNo { get; set; } + public string TaskStatus { get; set; } + public long? CompanyId { get; set; } +} + +public class OnboardingTaskDetailOutput +{ + public ObOnboardingTask Task { get; set; } + public ObTaskEmployeeInfo EmployeeInfo { get; set; } + public List Contracts { get; set; } + public List Materials { get; set; } + public List Audits { get; set; } +} diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs new file mode 100644 index 0000000..a24af85 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/IOnboardingTaskQueryService.cs @@ -0,0 +1,8 @@ +namespace SimpleAdmin.Application; + +public interface IOnboardingTaskQueryService : ITransient +{ + Task> Page(OnboardingTaskPageInput input); + + Task Detail(BaseIdInput input); +} \ No newline at end of file diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs new file mode 100644 index 0000000..b791910 --- /dev/null +++ b/SimpleAdmin.Application/Services/Onboarding/TaskQuery/OnboardingTaskQueryService.cs @@ -0,0 +1,44 @@ +namespace SimpleAdmin.Application; + +public class OnboardingTaskQueryService : IOnboardingTaskQueryService +{ + private readonly ISqlSugarClient _db; + + public OnboardingTaskQueryService(ISqlSugarClient db) + { + _db = db; + } + + public Task> Page(OnboardingTaskPageInput input) + { + return _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo)) + .WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus), item => item.TaskStatus == input.TaskStatus) + .WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value) + .OrderByDescending(item => item.CreateTime) + .ToPagedListAsync(input.PageNum, input.PageSize); + } + + public async Task Detail(BaseIdInput input) + { + var task = await _db.Queryable().InSingleAsync(input.Id) + ?? throw Oops.Bah("入职任务不存在"); + + return new OnboardingTaskDetailOutput + { + Task = task, + EmployeeInfo = await _db.Queryable() + .FirstAsync(item => item.TaskId == task.Id), + Contracts = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(), + Materials = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .ToListAsync(), + Audits = await _db.Queryable() + .Where(item => item.TaskId == task.Id) + .OrderByDescending(item => item.AuditedAt) + .ToListAsync() + }; + } +} \ No newline at end of file diff --git a/SimpleAdmin.Application/SimpleAdmin.Application.xml b/SimpleAdmin.Application/SimpleAdmin.Application.xml index 5277779..a06d95c 100644 --- a/SimpleAdmin.Application/SimpleAdmin.Application.xml +++ b/SimpleAdmin.Application/SimpleAdmin.Application.xml @@ -99,8 +99,32 @@ 系统配置 + + 根据任务、公司与候选人资料刷新自动变量;人事已手工填写的变量不会被覆盖。 + + + 冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。 + - 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。 + + 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。 + + + + 仅展示审核通过后创建的正式员工档案;入职资料的来源任务可追溯。 + + + + 维护员工端动态字段、材料类型及公司岗位要求。 + + + + + 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。 + + + + 管理端在链接发送前填写合同薪资、补贴等人事变量。 diff --git a/SimpleAdmin.System/Services/Dev/File/FileService.cs b/SimpleAdmin.System/Services/Dev/File/FileService.cs index 621457d..a93cfa2 100644 --- a/SimpleAdmin.System/Services/Dev/File/FileService.cs +++ b/SimpleAdmin.System/Services/Dev/File/FileService.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 // // SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: // 1.请不要删除和修改根目录下的LICENSE文件。 @@ -100,6 +100,16 @@ public class FileService : DbRepository, IFileService return null; } + public async Task StoreGeneratedFile(string engine, string fileName, byte[] content) + { + await using var stream = new MemoryStream(content); + IFormFile file = new FormFile(stream, 0, content.Length, "file", fileName) + { + Headers = new HeaderDictionary(), + ContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }; + return await StorageFile(engine, file); + } #region 方法 /// diff --git a/SimpleAdmin.System/Services/Dev/File/IFileService.cs b/SimpleAdmin.System/Services/Dev/File/IFileService.cs index 1a57632..d5bc210 100644 --- a/SimpleAdmin.System/Services/Dev/File/IFileService.cs +++ b/SimpleAdmin.System/Services/Dev/File/IFileService.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 +// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 // // SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: // 1.请不要删除和修改根目录下的LICENSE文件。 @@ -60,4 +60,5 @@ public interface IFileService : ITransient /// 文件 /// Task UploadFile(string engine, IFormFile file); + Task StoreGeneratedFile(string engine, string fileName, byte[] content); } diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingAuditController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingAuditController.cs new file mode 100644 index 0000000..773269c --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingAuditController.cs @@ -0,0 +1,26 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职审核")] +[Route("biz/onboarding/audit")] +[RolePermission] +public class OnboardingAuditController +{ + private readonly IOnboardingAuditService _service; + + public OnboardingAuditController(IOnboardingAuditService service) + { + _service = service; + } + + [HttpPost("approve")] + public async Task Approve([FromBody] OnboardingAuditActionInput input) + { + await _service.Approve(input); + } + + [HttpPost("reject")] + public async Task Reject([FromBody] OnboardingAuditActionInput input) + { + await _service.Reject(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingCompanyController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingCompanyController.cs index 75276c2..56e707c 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingCompanyController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingCompanyController.cs @@ -6,12 +6,45 @@ namespace SimpleAdmin.Web.Core; public class OnboardingCompanyController { private readonly IOnboardingCompanyService _service; - public OnboardingCompanyController(IOnboardingCompanyService service) => _service = service; - [HttpGet("page")] public async Task Page([FromQuery] OnboardingCompanyPageInput input) => await _service.Page(input); - [HttpGet("list-enabled")] public async Task ListEnabled() => await _service.ListEnabled(); - [HttpGet("detail")] public async Task Detail([FromQuery] BaseIdInput input) => await _service.Detail(input); - [HttpPost("add")] public async Task Add([FromBody] OnboardingCompanyAddInput input) => await _service.Add(input); - [HttpPost("edit")] public async Task Edit([FromBody] OnboardingCompanyEditInput input) => await _service.Edit(input); - [HttpPost("delete")] public async Task Delete([FromBody] BaseIdListInput input) => await _service.Delete(input); -} + public OnboardingCompanyController(IOnboardingCompanyService service) + { + _service = service; + } + + [HttpGet("page")] + public async Task Page([FromQuery] OnboardingCompanyPageInput input) + { + return await _service.Page(input); + } + + [HttpGet("list-enabled")] + public async Task ListEnabled() + { + return await _service.ListEnabled(); + } + + [HttpGet("detail")] + public async Task Detail([FromQuery] BaseIdInput input) + { + return await _service.Detail(input); + } + + [HttpPost("add")] + public async Task Add([FromBody] OnboardingCompanyAddInput input) + { + await _service.Add(input); + } + + [HttpPost("edit")] + public async Task Edit([FromBody] OnboardingCompanyEditInput input) + { + await _service.Edit(input); + } + + [HttpPost("delete")] + public async Task Delete([FromBody] BaseIdListInput input) + { + await _service.Delete(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractDocumentController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractDocumentController.cs new file mode 100644 index 0000000..0ddbcea --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractDocumentController.cs @@ -0,0 +1,25 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职合同文件")] +[Route("biz/onboarding/contract-document")] +[RolePermission] +public class OnboardingContractDocumentController +{ + private readonly IOnboardingContractDocumentService _service; + + public OnboardingContractDocumentController( + IOnboardingContractDocumentService service) + { + _service = service; + } + + /// + /// 生成入职合同文件 + /// + [HttpPost("generate")] + public async Task Generate( + [FromBody] OnboardingGenerateContractInput input) + { + return await _service.Generate(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs index c158e3e..922c843 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs @@ -6,20 +6,105 @@ namespace SimpleAdmin.Web.Core; public class OnboardingContractTemplateController { private readonly IOnboardingContractTemplateService _service; - public OnboardingContractTemplateController(IOnboardingContractTemplateService service) => _service = service; - [HttpGet("page")] public async Task Page([FromQuery] OnboardingContractTemplatePageInput input) => await _service.Page(input); - [HttpGet("detail")] public async Task Detail([FromQuery] BaseIdInput input) => await _service.Detail(input); - [HttpPost("add")] public async Task Add([FromBody] OnboardingContractTemplateAddInput input) => await _service.Add(input); - [HttpPost("edit")] public async Task Edit([FromBody] OnboardingContractTemplateEditInput input) => await _service.Edit(input); - [HttpPost("delete")] public async Task Delete([FromBody] BaseIdListInput input) => await _service.Delete(input); - [HttpGet("version/page")] public async Task VersionPage([FromQuery] OnboardingContractVersionPageInput input) => await _service.VersionPage(input); - [HttpPost("version/add")] public async Task AddVersion([FromBody] OnboardingContractVersionAddInput input) => await _service.AddVersion(input); - [HttpPost("version/set-status")] public async Task SetVersionStatus([FromBody] OnboardingContractVersionStatusInput input) => await _service.SetVersionStatus(input); - [HttpGet("variable/page")] public async Task VariablePage([FromQuery] OnboardingTemplateVariablePageInput input) => await _service.VariablePage(input); - [HttpPost("variable/add")] public async Task AddVariable([FromBody] OnboardingTemplateVariableAddInput input) => await _service.AddVariable(input); - [HttpPost("variable/edit")] public async Task EditVariable([FromBody] OnboardingTemplateVariableEditInput input) => await _service.EditVariable(input); - [HttpPost("variable/delete")] public async Task DeleteVariable([FromBody] BaseIdListInput input) => await _service.DeleteVariable(input); [HttpGet("scope/page")] public async Task ScopePage([FromQuery] OnboardingContractScopePageInput input) => await _service.ScopePage(input); - [HttpPost("scope/add")] public async Task AddScope([FromBody] OnboardingContractScopeAddInput input) => await _service.AddScope(input); - [HttpPost("scope/edit")] public async Task EditScope([FromBody] OnboardingContractScopeEditInput input) => await _service.EditScope(input); - [HttpPost("scope/delete")] public async Task DeleteScope([FromBody] BaseIdListInput input) => await _service.DeleteScope(input);} + public OnboardingContractTemplateController(IOnboardingContractTemplateService service) + { + _service = service; + } + + [HttpGet("page")] + public async Task Page([FromQuery] OnboardingContractTemplatePageInput input) + { + return await _service.Page(input); + } + + [HttpGet("detail")] + public async Task Detail([FromQuery] BaseIdInput input) + { + return await _service.Detail(input); + } + + [HttpPost("add")] + public async Task Add([FromBody] OnboardingContractTemplateAddInput input) + { + await _service.Add(input); + } + + [HttpPost("edit")] + public async Task Edit([FromBody] OnboardingContractTemplateEditInput input) + { + await _service.Edit(input); + } + + [HttpPost("delete")] + public async Task Delete([FromBody] BaseIdListInput input) + { + await _service.Delete(input); + } + + [HttpGet("version/page")] + public async Task VersionPage([FromQuery] OnboardingContractVersionPageInput input) + { + return await _service.VersionPage(input); + } + + [HttpPost("version/add")] + public async Task AddVersion([FromBody] OnboardingContractVersionAddInput input) + { + await _service.AddVersion(input); + } + + [HttpPost("version/set-status")] + public async Task SetVersionStatus([FromBody] OnboardingContractVersionStatusInput input) + { + await _service.SetVersionStatus(input); + } + + [HttpGet("variable/page")] + public async Task VariablePage([FromQuery] OnboardingTemplateVariablePageInput input) + { + return await _service.VariablePage(input); + } + + [HttpPost("variable/add")] + public async Task AddVariable([FromBody] OnboardingTemplateVariableAddInput input) + { + await _service.AddVariable(input); + } + + [HttpPost("variable/edit")] + public async Task EditVariable([FromBody] OnboardingTemplateVariableEditInput input) + { + await _service.EditVariable(input); + } + + [HttpPost("variable/delete")] + public async Task DeleteVariable([FromBody] BaseIdListInput input) + { + await _service.DeleteVariable(input); + } + + [HttpGet("scope/page")] + public async Task ScopePage([FromQuery] OnboardingContractScopePageInput input) + { + return await _service.ScopePage(input); + } + + [HttpPost("scope/add")] + public async Task AddScope([FromBody] OnboardingContractScopeAddInput input) + { + await _service.AddScope(input); + } + + [HttpPost("scope/edit")] + public async Task EditScope([FromBody] OnboardingContractScopeEditInput input) + { + await _service.EditScope(input); + } + + [HttpPost("scope/delete")] + public async Task DeleteScope([FromBody] BaseIdListInput input) + { + await _service.DeleteScope(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEmployeeController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEmployeeController.cs new file mode 100644 index 0000000..d4cca36 --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEmployeeController.cs @@ -0,0 +1,26 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职正式员工档案")] +[Route("biz/onboarding/employee")] +[RolePermission] +public class OnboardingEmployeeController +{ + private readonly IOnboardingEmployeeService _service; + + public OnboardingEmployeeController(IOnboardingEmployeeService service) + { + _service = service; + } + + [HttpGet("page")] + public async Task Page([FromQuery] OnboardingEmployeePageInput input) + { + return await _service.Page(input); + } + + [HttpGet("detail")] + public async Task Detail([FromQuery] BaseIdInput input) + { + return await _service.Detail(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs new file mode 100644 index 0000000..7a86bdc --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs @@ -0,0 +1,86 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职填写与材料配置")] +[Route("biz/onboarding/entry-config")] +[RolePermission] +public class OnboardingEntryConfigController +{ + private readonly IOnboardingEntryConfigService _service; + + public OnboardingEntryConfigController(IOnboardingEntryConfigService service) + { + _service = service; + } + + [HttpGet("form-field/list")] + public async Task ListFormFields() + { + return await _service.ListFormFields(); + } + + [HttpPost("form-field/save")] + public async Task SaveFormField([FromBody] OnboardingFormFieldSaveInput input) + { + await _service.SaveFormField(input); + } + + [HttpPost("form-field/delete")] + public async Task DeleteFormFields([FromBody] BaseIdListInput input) + { + await _service.DeleteFormFields(input); + } + + [HttpGet("material-type/list")] + public async Task ListMaterialTypes() + { + return await _service.ListMaterialTypes(); + } + + [HttpPost("material-type/save")] + public async Task SaveMaterialType([FromBody] OnboardingMaterialTypeSaveInput input) + { + await _service.SaveMaterialType(input); + } + + [HttpPost("material-type/delete")] + public async Task DeleteMaterialTypes([FromBody] BaseIdListInput input) + { + await _service.DeleteMaterialTypes(input); + } + + [HttpGet("form-requirement/list")] + public async Task ListFormRequirements([FromQuery] long companyId) + { + return await _service.ListFormRequirements(companyId); + } + + [HttpPost("form-requirement/save")] + public async Task SaveFormRequirement([FromBody] OnboardingFormRequirementSaveInput input) + { + await _service.SaveFormRequirement(input); + } + + [HttpPost("form-requirement/delete")] + public async Task DeleteFormRequirements([FromBody] BaseIdListInput input) + { + await _service.DeleteFormRequirements(input); + } + + [HttpGet("material-requirement/list")] + public async Task ListMaterialRequirements([FromQuery] long companyId) + { + return await _service.ListMaterialRequirements(companyId); + } + + [HttpPost("material-requirement/save")] + public async Task SaveMaterialRequirement([FromBody] OnboardingMaterialRequirementSaveInput input) + { + await _service.SaveMaterialRequirement(input); + } + + [HttpPost("material-requirement/delete")] + public async Task DeleteMaterialRequirements([FromBody] BaseIdListInput input) + { + await _service.DeleteMaterialRequirements(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingMaterialReviewController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingMaterialReviewController.cs new file mode 100644 index 0000000..1753c30 --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingMaterialReviewController.cs @@ -0,0 +1,20 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职资料审核")] +[Route("biz/onboarding/material")] +[RolePermission] +public class OnboardingMaterialReviewController +{ + private readonly IOnboardingMaterialReviewService _service; + + public OnboardingMaterialReviewController(IOnboardingMaterialReviewService service) + { + _service = service; + } + + [HttpPost("review")] + public async Task Review([FromBody] OnboardingMaterialReviewInput input) + { + await _service.Review(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPositionController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPositionController.cs index 3967867..0d49894 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPositionController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPositionController.cs @@ -6,12 +6,45 @@ namespace SimpleAdmin.Web.Core; public class OnboardingPositionController { private readonly IOnboardingPositionService _service; - public OnboardingPositionController(IOnboardingPositionService service) => _service = service; - [HttpGet("page")] public async Task Page([FromQuery] OnboardingPositionPageInput input) => await _service.Page(input); - [HttpGet("list-enabled")] public async Task ListEnabled([FromQuery] long companyId) => await _service.ListEnabled(companyId); - [HttpGet("detail")] public async Task Detail([FromQuery] BaseIdInput input) => await _service.Detail(input); - [HttpPost("add")] public async Task Add([FromBody] OnboardingPositionAddInput input) => await _service.Add(input); - [HttpPost("edit")] public async Task Edit([FromBody] OnboardingPositionEditInput input) => await _service.Edit(input); - [HttpPost("delete")] public async Task Delete([FromBody] BaseIdListInput input) => await _service.Delete(input); -} + public OnboardingPositionController(IOnboardingPositionService service) + { + _service = service; + } + + [HttpGet("page")] + public async Task Page([FromQuery] OnboardingPositionPageInput input) + { + return await _service.Page(input); + } + + [HttpGet("list-enabled")] + public async Task ListEnabled([FromQuery] long companyId) + { + return await _service.ListEnabled(companyId); + } + + [HttpGet("detail")] + public async Task Detail([FromQuery] BaseIdInput input) + { + return await _service.Detail(input); + } + + [HttpPost("add")] + public async Task Add([FromBody] OnboardingPositionAddInput input) + { + await _service.Add(input); + } + + [HttpPost("edit")] + public async Task Edit([FromBody] OnboardingPositionEditInput input) + { + await _service.Edit(input); + } + + [HttpPost("delete")] + public async Task Delete([FromBody] BaseIdListInput input) + { + await _service.Delete(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs new file mode 100644 index 0000000..dd05155 --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicSubmissionController.cs @@ -0,0 +1,67 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "员工入职提交")] +[Route("open/onboarding")] +[AllowAnonymous] +public class OnboardingPublicSubmissionController +{ + private readonly IOnboardingPublicSubmissionService _service; + private readonly IFileService _fileService; + + public OnboardingPublicSubmissionController(IOnboardingPublicSubmissionService service, IFileService fileService) + { + _service = service; + _fileService = fileService; + } + + [HttpPost("save-dynamic-values")] + public async Task SaveDynamicValues([FromBody] OnboardingSaveDynamicValuesInput input) + { + await _service.SaveDynamicValues(input); + } + + [HttpPost("save-material")] + public async Task SaveMaterial([FromBody] OnboardingSaveMaterialInput input) + { + await _service.SaveMaterial(input); + } + + [HttpPost("confirm-contract")] + public async Task ConfirmContract([FromBody] OnboardingConfirmContractInput input) + { + await _service.ConfirmContract(input); + } + + [HttpPost("submit")] + public async Task Submit([FromBody] OnboardingSubmitInput input) + { + await _service.Submit(input); + } + + [HttpPost("upload-material")] + [Consumes("multipart/form-data")] + [RequestSizeLimit(20 * 1024 * 1024)] + public async Task UploadMaterial([FromForm] string token, [FromForm] long materialTypeId, IFormFile file) + { + if (file == null || file.Length == 0) + { + throw new ArgumentException("请选择需要上传的文件"); + } + + await _service.ValidateMaterialUpload(new OnboardingMaterialUploadValidationInput + { + Token = token, + MaterialTypeId = materialTypeId, + FileName = file.FileName, + FileLength = file.Length + }); + + var fileId = await _fileService.UploadFile(SysDictConst.FILE_ENGINE_LOCAL, file); + await _service.SaveMaterial(new OnboardingSaveMaterialInput + { + Token = token, + MaterialTypeId = materialTypeId, + FileId = fileId + }); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs new file mode 100644 index 0000000..595656d --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs @@ -0,0 +1,35 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "员工入职填写")] +[Route("open/onboarding")] +[AllowAnonymous] +public class OnboardingPublicTaskController +{ + private readonly IOnboardingPublicTaskService _service; + private readonly IFileService _fileService; + + public OnboardingPublicTaskController(IOnboardingPublicTaskService service, IFileService fileService) + { + _service = service; + _fileService = fileService; + } + + [HttpPost("detail")] + public async Task Detail([FromBody] OnboardingTaskPublicAccessInput input) + { + return await _service.Detail(input); + } + + [HttpPost("save-employee-info")] + public async Task SaveEmployeeInfo([FromBody] OnboardingTaskPublicSaveInput input) + { + await _service.SaveEmployeeInfo(input); + } + + [HttpGet("contract-source")] + public async Task ContractSource([FromQuery] OnboardingPublicContractSourceInput input) + { + var fileId = await _service.GetContractSourceFileId(input); + return await _fileService.Download(new BaseIdInput { Id = fileId }); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskContractVariableController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskContractVariableController.cs new file mode 100644 index 0000000..dadbd79 --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskContractVariableController.cs @@ -0,0 +1,26 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职任务合同参数")] +[Route("biz/onboarding/task-contract-variable")] +[RolePermission] +public class OnboardingTaskContractVariableController +{ + private readonly IOnboardingTaskContractVariableService _service; + + public OnboardingTaskContractVariableController(IOnboardingTaskContractVariableService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task List([FromQuery] OnboardingTaskContractVariableQueryInput input) + { + return await _service.List(input); + } + + [HttpPost("save")] + public async Task Save([FromBody] OnboardingTaskContractVariableSaveInput input) + { + await _service.Save(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskController.cs index 66430c3..7a1d049 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskController.cs @@ -6,6 +6,15 @@ namespace SimpleAdmin.Web.Core; public class OnboardingTaskController { private readonly IOnboardingTaskService _service; - public OnboardingTaskController(IOnboardingTaskService service) => _service = service; - [HttpPost("create")] public async Task Create([FromBody] OnboardingTaskCreateInput input) => await _service.Create(input); -} + + public OnboardingTaskController(IOnboardingTaskService service) + { + _service = service; + } + + [HttpPost("create")] + public async Task Create([FromBody] OnboardingTaskCreateInput input) + { + return await _service.Create(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskLifecycleController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskLifecycleController.cs new file mode 100644 index 0000000..9c32aab --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskLifecycleController.cs @@ -0,0 +1,26 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职任务归档")] +[Route("biz/onboarding/task")] +[RolePermission] +public class OnboardingTaskLifecycleController +{ + private readonly IOnboardingTaskLifecycleService _service; + + public OnboardingTaskLifecycleController(IOnboardingTaskLifecycleService service) + { + _service = service; + } + + [HttpPost("archive")] + public async Task Archive([FromBody] OnboardingAuditActionInput input) + { + await _service.Archive(input); + } + + [HttpPost("revoke")] + public async Task Revoke([FromBody] OnboardingAuditActionInput input) + { + await _service.Revoke(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs new file mode 100644 index 0000000..e0b6eeb --- /dev/null +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTaskQueryController.cs @@ -0,0 +1,26 @@ +namespace SimpleAdmin.Web.Core; + +[ApiDescriptionSettings("Application", Tag = "入职任务查询")] +[Route("biz/onboarding/task")] +[RolePermission] +public class OnboardingTaskQueryController +{ + private readonly IOnboardingTaskQueryService _service; + + public OnboardingTaskQueryController(IOnboardingTaskQueryService service) + { + _service = service; + } + + [HttpGet("page")] + public async Task Page([FromQuery] OnboardingTaskPageInput input) + { + return await _service.Page(input); + } + + [HttpGet("detail")] + public async Task Detail([FromQuery] BaseIdInput input) + { + return await _service.Detail(input); + } +} \ No newline at end of file diff --git a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml index 9804993..df8e209 100644 --- a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml +++ b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml @@ -26,6 +26,11 @@ 模拟 Configure + + + 生成入职合同文件 + + 机构管理控制器