From 085bf5e2ab9961c98ee7df389ac72a327647f968 Mon Sep 17 00:00:00 2001
From: fuenmao <980740792@qq.com>
Date: Tue, 25 Aug 2026 10:20:53 +0800
Subject: [PATCH] =?UTF-8?q?=E9=80=BB=E8=BE=91=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Entity/Onboarding/ObOnboardingTask.cs | 5 +
.../Entity/Onboarding/ObOnboardingTemplate.cs | 29 +
.../ObOnboardingTemplateContract.cs | 39 +
.../ObOnboardingTemplateFormField.cs | 29 +
.../ObOnboardingTemplateMaterial.cs | 29 +
.../ObOnboardingTemplatePosition.cs | 19 +
.../Entity/Onboarding/ObTaskContract.cs | 5 +
.../Onboarding/ObTaskFormRequirement.cs | 49 ++
.../Onboarding/ObTaskMaterialRequirement.cs | 54 ++
.../Entity/Onboarding/OnboardingStatus.cs | 4 +
.../Audit/OnboardingAuditService.cs | 8 +-
.../OnboardingContractDocumentService.cs | 88 +-
.../IOnboardingContractSnapshotService.cs | 2 +-
.../OnboardingContractSnapshotService.cs | 191 +++-
.../ContractTemplate/DocxTemplateProcessor.cs | 160 ++++
.../Dto/OnboardingContractScopeInput.cs | 73 --
.../IOnboardingContractTemplateService.cs | 40 +-
.../OnboardingContractTemplateService.cs | 218 ++---
.../Dto/OnboardingEntryConfigInput.cs | 76 --
.../IOnboardingEntryConfigService.cs | 37 +-
.../OnboardingEntryConfigService.cs | 154 +---
.../Dto/OnboardingTemplateInput.cs | 213 +++++
.../IOnboardingTemplateService.cs | 32 +
.../OnboardingTemplateService.cs | 431 +++++++++
.../OnboardingPublicSubmissionService.cs | 151 ++--
.../PublicTask/OnboardingPublicTaskService.cs | 135 ++-
.../Task/Dto/OnboardingTaskInput.cs | 32 +
.../Onboarding/Task/OnboardingTaskService.cs | 198 ++++-
.../OnboardingTaskContractVariableService.cs | 9 +
.../SimpleAdmin.Application.xml | 829 ++++++++++++------
.../OnboardingContractTemplateController.cs | 58 +-
.../OnboardingEntryConfigController.cs | 58 +-
.../OnboardingPublicTaskController.cs | 2 +-
.../OnboardingTemplateController.cs | 62 ++
SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml | 96 +-
.../20260824_onboarding_template_refactor.sql | 364 ++++++++
36 files changed, 2770 insertions(+), 1209 deletions(-)
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateContract.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateFormField.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateMaterial.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplatePosition.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObTaskFormRequirement.cs
create mode 100644 SimpleAdmin.Application/Entity/Onboarding/ObTaskMaterialRequirement.cs
create mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs
delete mode 100644 SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractScopeInput.cs
create mode 100644 SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/Dto/OnboardingTemplateInput.cs
create mode 100644 SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
create mode 100644 SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
create mode 100644 SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTemplateController.cs
create mode 100644 database/migrations/20260824_onboarding_template_refactor.sql
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
index b372944..dd0cf51 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTask.cs
@@ -37,6 +37,11 @@ public class ObOnboardingTask : BaseEntity
///
public long PositionId { get; set; }
+ ///
+ /// 创建任务时锁定的入职模板 Id。
+ ///
+ public long OnboardingTemplateId { get; set; }
+
///
/// 创建任务时指定的审核人用户 Id。
///
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
new file mode 100644
index 0000000..082c800
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplate.cs
@@ -0,0 +1,29 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板,统一组织公司、岗位、合同、补充字段和上传材料。
+///
+[SugarTable("ob_onboarding_template", TableDescription = "入职模板")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObOnboardingTemplate : BaseEntity
+{
+ ///
+ /// 入职模板业务编码。
+ ///
+ public string TemplateCode { get; set; }
+
+ ///
+ /// 入职模板显示名称。
+ ///
+ public string TemplateName { get; set; }
+
+ ///
+ /// 所属入职业务公司 Id。
+ ///
+ public long CompanyId { get; set; }
+
+ ///
+ /// 模板用途和适用范围说明。
+ ///
+ public string Description { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateContract.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateContract.cs
new file mode 100644
index 0000000..7d7d2b8
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateContract.cs
@@ -0,0 +1,39 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板需要使用的合同文件及锁定版本。
+///
+[SugarTable("ob_onboarding_template_contract", TableDescription = "入职模板合同配置")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObOnboardingTemplateContract : BaseEntity
+{
+ ///
+ /// 入职模板 Id。
+ ///
+ public long OnboardingTemplateId { get; set; }
+
+ ///
+ /// 合同模板主档 Id。
+ ///
+ public long ContractTemplateId { get; set; }
+
+ ///
+ /// 锁定的合同模板版本 Id。
+ ///
+ public long ContractTemplateVersionId { get; set; }
+
+ ///
+ /// 员工是否必须查看并确认该合同。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 合同打印份数。
+ ///
+ public int PrintCopies { get; set; } = 1;
+
+ ///
+ /// 显示和生成顺序。
+ ///
+ public int? SortCode { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateFormField.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateFormField.cs
new file mode 100644
index 0000000..13683bc
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateFormField.cs
@@ -0,0 +1,29 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板需要员工填写的动态补充字段。
+///
+[SugarTable("ob_onboarding_template_form_field", TableDescription = "入职模板补充字段")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObOnboardingTemplateFormField : BaseEntity
+{
+ ///
+ /// 入职模板 Id。
+ ///
+ public long OnboardingTemplateId { get; set; }
+
+ ///
+ /// 动态补充字段 Id。
+ ///
+ public long FieldId { get; set; }
+
+ ///
+ /// 员工是否必须填写。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 手机端显示顺序。
+ ///
+ public int? SortCode { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateMaterial.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateMaterial.cs
new file mode 100644
index 0000000..b97725f
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplateMaterial.cs
@@ -0,0 +1,29 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板需要员工上传的材料配置。
+///
+[SugarTable("ob_onboarding_template_material", TableDescription = "入职模板上传材料")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObOnboardingTemplateMaterial : BaseEntity
+{
+ ///
+ /// 入职模板 Id。
+ ///
+ public long OnboardingTemplateId { get; set; }
+
+ ///
+ /// 上传材料类型 Id。
+ ///
+ public long MaterialTypeId { get; set; }
+
+ ///
+ /// 员工是否必须上传。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 手机端显示顺序。
+ ///
+ public int? SortCode { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplatePosition.cs b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplatePosition.cs
new file mode 100644
index 0000000..99805ef
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObOnboardingTemplatePosition.cs
@@ -0,0 +1,19 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板适用岗位关系。
+///
+[SugarTable("ob_onboarding_template_position", TableDescription = "入职模板适用岗位")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObOnboardingTemplatePosition : BaseEntity
+{
+ ///
+ /// 入职模板 Id。
+ ///
+ public long OnboardingTemplateId { get; set; }
+
+ ///
+ /// 适用岗位 Id。
+ ///
+ public long PositionId { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObTaskContract.cs b/SimpleAdmin.Application/Entity/Onboarding/ObTaskContract.cs
index 2abb9cc..0d1d126 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/ObTaskContract.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObTaskContract.cs
@@ -47,6 +47,11 @@ public class ObTaskContract : BaseEntity
///
public bool Required { get; set; }
+ ///
+ /// 任务创建时冻结的合同打印份数。
+ ///
+ public int PrintCopies { get; set; } = 1;
+
///
/// 任务合同当前状态。
///
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObTaskFormRequirement.cs b/SimpleAdmin.Application/Entity/Onboarding/ObTaskFormRequirement.cs
new file mode 100644
index 0000000..1666e28
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObTaskFormRequirement.cs
@@ -0,0 +1,49 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职任务创建时冻结的动态补充字段要求。
+///
+[SugarTable("ob_task_form_requirement", TableDescription = "任务补充字段快照")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObTaskFormRequirement : BaseEntity
+{
+ ///
+ /// 入职任务 Id。
+ ///
+ public long TaskId { get; set; }
+
+ ///
+ /// 原动态字段 Id。
+ ///
+ public long FieldId { get; set; }
+
+ ///
+ /// 冻结的字段业务编码。
+ ///
+ public string FieldCode { get; set; }
+
+ ///
+ /// 冻结的字段显示名称。
+ ///
+ public string FieldName { get; set; }
+
+ ///
+ /// 冻结的数据类型。
+ ///
+ public string DataType { get; set; }
+
+ ///
+ /// 冻结的选项 JSON。
+ ///
+ public string OptionsJson { get; set; }
+
+ ///
+ /// 是否必须填写。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 手机端显示顺序。
+ ///
+ public int? SortCode { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/ObTaskMaterialRequirement.cs b/SimpleAdmin.Application/Entity/Onboarding/ObTaskMaterialRequirement.cs
new file mode 100644
index 0000000..459f902
--- /dev/null
+++ b/SimpleAdmin.Application/Entity/Onboarding/ObTaskMaterialRequirement.cs
@@ -0,0 +1,54 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职任务创建时冻结的上传材料要求。
+///
+[SugarTable("ob_task_material_requirement", TableDescription = "任务上传材料快照")]
+[Tenant(SqlSugarConst.DB_DEFAULT)]
+public class ObTaskMaterialRequirement : BaseEntity
+{
+ ///
+ /// 入职任务 Id。
+ ///
+ public long TaskId { get; set; }
+
+ ///
+ /// 原材料类型 Id。
+ ///
+ public long MaterialTypeId { get; set; }
+
+ ///
+ /// 冻结的材料编码。
+ ///
+ public string MaterialCode { get; set; }
+
+ ///
+ /// 冻结的材料名称。
+ ///
+ public string MaterialName { get; set; }
+
+ ///
+ /// 冻结的允许扩展名。
+ ///
+ public string AllowedExtensions { get; set; }
+
+ ///
+ /// 冻结的最大文件数量。
+ ///
+ public int MaxFileCount { get; set; }
+
+ ///
+ /// 冻结的单文件大小上限,单位 MB。
+ ///
+ public int? MaxFileSizeMb { get; set; }
+
+ ///
+ /// 是否必须上传。
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// 手机端显示顺序。
+ ///
+ public int? SortCode { get; set; }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs b/SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs
index a015bd8..dd9bc6e 100644
--- a/SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs
+++ b/SimpleAdmin.Application/Entity/Onboarding/OnboardingStatus.cs
@@ -139,6 +139,10 @@ public static class TaskContractStatus
///
public static class ContractVariableType
{
+ ///
+ /// 导入模板时尚未确认来源的变量。
+ ///
+ public const string Unresolved = "unresolved";
///
/// 值来源于候选人或正式员工信息。
///
diff --git a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
index a5c3f4e..e807c7c 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Audit/OnboardingAuditService.cs
@@ -28,12 +28,12 @@ public class OnboardingAuditService : IOnboardingAuditService
.FirstAsync(item => item.TaskId == task.Id)
?? throw Oops.Bah("候选人资料不存在");
- var hasMaterialNeedSupplement = await _db.Queryable()
+ var hasUnapprovedMaterial = await _db.Queryable()
.AnyAsync(item => item.TaskId == task.Id
- && item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
- if (hasMaterialNeedSupplement)
+ && item.ReviewStatus != TaskMaterialReviewStatus.Passed);
+ if (hasUnapprovedMaterial)
{
- throw Oops.Bah("仍有资料需要补充,不能审核通过");
+ throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务");
}
// 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
index 4771350..bea8ef2 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
///
-/// 实现入职合同文件生成业务能力。
+/// 实现入职合同预览和归档 DOCX 文件生成能力。
///
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
{
@@ -15,7 +15,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
}
///
- /// 根据已锁定的模板版本和变量快照生成任务合同 DOCX 文件。
+ /// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
///
public async Task Generate(OnboardingGenerateContractInput input)
{
@@ -23,10 +23,12 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
?? throw Oops.Bah("任务合同不存在");
var task = await _db.Queryable().InSingleAsync(contract.TaskId)
?? throw Oops.Bah("入职任务不存在");
-
- if (task.TaskStatus != OnboardingTaskStatus.Approved)
- throw Oops.Bah("仅审核通过的任务可生成归档合同");
-
+ var statusAllowsGeneration = task.TaskStatus == OnboardingTaskStatus.PendingFill
+ || task.TaskStatus == OnboardingTaskStatus.Rejected
+ || task.TaskStatus == OnboardingTaskStatus.Reviewing
+ || task.TaskStatus == OnboardingTaskStatus.Approved;
+ if (!statusAllowsGeneration)
+ throw Oops.Bah("当前任务状态不能生成合同");
if (contract.GeneratedDocxFileId.HasValue)
return contract.GeneratedDocxFileId.Value;
@@ -34,20 +36,31 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
?? 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)
+ var definitions = await _db.Queryable()
+ .Where(item => item.TemplateVersionId == version.Id)
.ToListAsync();
+ var variables = await _db.Queryable()
+ .Where(item => item.TaskContractId == contract.Id)
+ .ToListAsync();
+ var missing = definitions
+ .Where(item => item.Required)
+ .FirstOrDefault(definition => string.IsNullOrWhiteSpace(
+ variables.FirstOrDefault(variable => variable.VariableCode == definition.VariableCode)?.VariableValue));
+ if (missing != null)
+ throw Oops.Bah($"合同变量“{missing.VariableName}”尚未填写,暂时不能预览合同");
+
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(
+ var rendered = DocxTemplateProcessor.Render(
sourceBytes.ToArray(),
- variables.ToDictionary(x => x.VariableCode, x => x.VariableValue ?? string.Empty));
+ variables.ToDictionary(
+ item => item.VariableCode,
+ item => item.VariableValue ?? string.Empty,
+ StringComparer.OrdinalIgnoreCase));
var name = $"{task.TaskNo}_{contract.ContractName}.docx";
var fileId = await _fileService.StoreGeneratedFile(
SysDictConst.FILE_ENGINE_ALIYUN_OSS,
@@ -56,53 +69,10 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
contract.GeneratedDocxFileId = fileId;
contract.GeneratedAt = DateTime.Now;
+ contract.ContractStatus = task.TaskStatus == OnboardingTaskStatus.Approved
+ ? TaskContractStatus.OfflinePendingSign
+ : TaskContractStatus.PendingConfirm;
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);
-
- // DOCX 本质是 ZIP 包,仅替换正文、页眉和页脚 XML 中的文本节点,其他资源原样复制。
- 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();
- }
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs
index b05e6fc..0621a55 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/IOnboardingContractSnapshotService.cs
@@ -9,5 +9,5 @@ public interface IOnboardingContractSnapshotService : ITransient
///
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
///
- Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null);
+ Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null, bool invalidateGeneratedPreview = false);
}
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
index ebfa2f3..66ac864 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractSnapshot/OnboardingContractSnapshotService.cs
@@ -1,32 +1,53 @@
namespace SimpleAdmin.Application;
-/// 冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。
///
-/// 实现任务合同变量快照业务能力。
+/// 构建任务合同变量快照,合同生成阶段只读取本快照。
///
public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService
{
private readonly SqlSugarScope _db;
- public OnboardingContractSnapshotService() => _db = DbContext.DB;
+
+ public OnboardingContractSnapshotService()
+ {
+ _db = DbContext.DB;
+ }
///
- /// 根据任务、公司和候选人资料构建或刷新合同变量快照。
+ /// 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。
///
- public async Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null)
+ public async Task Build(
+ long taskId,
+ ObTaskEmployeeInfo employeeInfo = null,
+ bool invalidateGeneratedPreview = false)
{
- 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();
+ 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(item => item.TaskId == taskId);
+ var dynamicValues = await _db.Queryable()
+ .Where(item => item.TaskId == taskId)
+ .ToListAsync();
+ var dynamicValueMap = dynamicValues
+ .GroupBy(item => item.FieldCode, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(group => group.Key, group => group.Last().FieldValue, StringComparer.OrdinalIgnoreCase);
+ var contracts = await _db.Queryable()
+ .Where(item => item.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 definitions = await _db.Queryable()
+ .Where(item => item.TemplateVersionId == contract.TemplateVersionId)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync();
+ var oldRows = await _db.Queryable()
+ .Where(item => item.TaskContractId == contract.Id)
+ .ToListAsync();
var rows = definitions.Select(definition =>
{
- var old = oldRows.FirstOrDefault(x => x.VariableCode == definition.VariableCode);
- // 人事手工填写的工资、补贴等合同参数优先级最高,刷新自动变量时不能覆盖。
+ var old = oldRows.FirstOrDefault(item => item.VariableCode == definition.VariableCode);
var preserveManual = old?.IsManualOverride == true;
return new ObTaskContractVariable
{
@@ -36,54 +57,136 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
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),
+ VariableValue = preserveManual
+ ? old.VariableValue
+ : ResolveValue(definition, employeeInfo, dynamicValueMap, 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();
+
+ await _db.Deleteable()
+ .Where(item => item.TaskContractId == contract.Id)
+ .ExecuteCommandAsync();
+ if (rows.Count > 0)
+ await _db.Insertable(rows).ExecuteCommandAsync();
+
+ if (invalidateGeneratedPreview && task.TaskStatus is OnboardingTaskStatus.PendingFill or OnboardingTaskStatus.Rejected)
+ {
+ contract.GeneratedDocxFileId = null;
+ contract.GeneratedPdfFileId = null;
+ contract.GeneratedAt = null;
+ contract.ReadAt = null;
+ contract.ConfirmedAt = null;
+ contract.ContractStatus = TaskContractStatus.PendingGenerate;
+ await _db.Updateable(contract).ExecuteCommandAsync();
+ }
}
}
- private static string GetSourceType(string variableType) => variableType switch
+ private static string GetSourceType(string variableType)
{
- ContractVariableType.Employee => ContractVariableSourceType.Employee,
- ContractVariableType.Company => ContractVariableSourceType.Company,
- ContractVariableType.System => ContractVariableSourceType.System,
- _ => ContractVariableSourceType.HrInput
- };
+ return 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)
+ private static string ResolveValue(
+ ObTemplateVariable definition,
+ ObTaskEmployeeInfo employeeInfo,
+ IReadOnlyDictionary dynamicValues,
+ ObCompany company,
+ ObOnboardingTask task)
{
+ var sourceField = GetSourceField(definition);
if (definition.VariableType == ContractVariableType.Employee)
- return GetPropertyValue(info, definition.EmployeeField) ?? definition.DefaultValue;
+ {
+ var fixedValue = GetPropertyValue(employeeInfo, ResolveEmployeeField(sourceField), definition.FormatRule);
+ if (!string.IsNullOrWhiteSpace(fixedValue))
+ return fixedValue;
+ return dynamicValues.TryGetValue(sourceField, out var dynamicValue)
+ ? dynamicValue
+ : 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 GetPropertyValue(company, ResolveCompanyField(sourceField), definition.FormatRule) ?? definition.DefaultValue;
+
+ if (definition.VariableType == ContractVariableType.System)
+ {
+ return sourceField.ToUpperInvariant() switch
+ {
+ "TASKNO" or "TASK_NO" or "CONTRACTNO" or "CONTRACT_NO" => task.TaskNo,
+ "CURRENTDATE" or "CURRENT_DATE" or "SIGNDATE" or "SIGN_DATE" => DateTime.Now.ToString("yyyy年MM月dd日"),
+ _ => definition.DefaultValue
+ };
+ }
+
return definition.DefaultValue;
}
- private static string GetPropertyValue(object source, string propertyName)
+ private static string GetSourceField(ObTemplateVariable definition)
{
- 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();
+ if (!string.IsNullOrWhiteSpace(definition.EmployeeField))
+ return definition.EmployeeField;
+ var separatorIndex = definition.VariableCode?.IndexOf('.') ?? -1;
+ return separatorIndex >= 0
+ ? definition.VariableCode[(separatorIndex + 1)..]
+ : definition.VariableCode;
}
- private static string ResolveCompanyField(ObTemplateVariable definition)
+ private static string GetPropertyValue(object source, string propertyName, string formatRule = null)
{
- if (!string.IsNullOrWhiteSpace(definition.EmployeeField)) return definition.EmployeeField;
- return definition.VariableCode.ToUpperInvariant() switch
+ if (source == null || string.IsNullOrWhiteSpace(propertyName))
+ return null;
+
+ var value = source.GetType()
+ .GetProperty(
+ propertyName,
+ global::System.Reflection.BindingFlags.Public
+ | global::System.Reflection.BindingFlags.Instance
+ | global::System.Reflection.BindingFlags.IgnoreCase)
+ ?.GetValue(source);
+ if (value == null)
+ return null;
+
+ if (value is DateTime dateTime)
+ return dateTime.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule);
+ if (value is DateTimeOffset dateTimeOffset)
+ return dateTimeOffset.ToString(string.IsNullOrWhiteSpace(formatRule) ? "yyyy年MM月dd日" : formatRule);
+ if (!string.IsNullOrWhiteSpace(formatRule) && value is IFormattable formattable)
+ return formattable.ToString(formatRule, null);
+ return value.ToString();
+ }
+
+ private static string ResolveEmployeeField(string sourceField)
+ {
+ return sourceField.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
+ "FULLNAME" or "FULL_NAME" or "NAME" => nameof(ObTaskEmployeeInfo.EmployeeName),
+ "MOBILE" or "MOBILEPHONE" or "MOBILE_PHONE" => nameof(ObTaskEmployeeInfo.Phone),
+ "IDNUMBER" or "ID_NUMBER" or "IDENTITYNUMBER" => nameof(ObTaskEmployeeInfo.IdCardNumber),
+ _ => sourceField
};
}
-}
+ private static string ResolveCompanyField(string sourceField)
+ {
+ return sourceField.ToUpperInvariant() switch
+ {
+ "COMPANYCODE" or "COMPANY_CODE" => nameof(ObCompany.CompanyCode),
+ "COMPANYNAME" or "COMPANY_NAME" => nameof(ObCompany.CompanyName),
+ "LEGALNAME" or "LEGAL_NAME" or "COMPANYLEGALNAME" => nameof(ObCompany.LegalName),
+ "UNIFIEDSOCIALCREDITCODE" or "UNIFIED_SOCIAL_CREDIT_CODE" => nameof(ObCompany.UnifiedSocialCreditCode),
+ "LEGALREPRESENTATIVE" or "LEGAL_REPRESENTATIVE" => nameof(ObCompany.LegalRepresentative),
+ "REGISTEREDADDRESS" or "REGISTERED_ADDRESS" => nameof(ObCompany.RegisteredAddress),
+ "CONTACTPHONE" or "CONTACT_PHONE" => nameof(ObCompany.ContactPhone),
+ _ => sourceField
+ };
+ }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs
new file mode 100644
index 0000000..95bedae
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs
@@ -0,0 +1,160 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 负责从 DOCX 中识别占位符并在不依赖 Word 进程的情况下替换文本节点。
+///
+public static class DocxTemplateProcessor
+{
+ private static readonly global::System.Text.RegularExpressions.Regex PlaceholderRegex = new(
+ @"\{\{\s*(?[A-Za-z][A-Za-z0-9_.-]*)\s*\}\}",
+ global::System.Text.RegularExpressions.RegexOptions.Compiled);
+
+ private static readonly global::System.Xml.Linq.XNamespace WordNamespace =
+ "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+
+ ///
+ /// 扫描正文、表格、页眉和页脚,返回去重后的变量编码。
+ ///
+ public static List ExtractVariableCodes(byte[] docx)
+ {
+ using var input = new MemoryStream(docx);
+ using var archive = new global::System.IO.Compression.ZipArchive(
+ input,
+ global::System.IO.Compression.ZipArchiveMode.Read);
+ var codes = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var entry in archive.Entries.Where(entry => IsWordTextXml(entry.FullName)))
+ {
+ using var stream = entry.Open();
+ var document = global::System.Xml.Linq.XDocument.Load(stream);
+ foreach (var paragraph in document.Descendants(WordNamespace + "p"))
+ {
+ var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value));
+ foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text))
+ codes.Add(match.Groups["code"].Value.Trim());
+ }
+ }
+
+ return codes.OrderBy(code => code).ToList();
+ }
+
+ ///
+ /// 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。
+ ///
+ public static byte[] Render(byte[] docx, IReadOnlyDictionary 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 sourceStream = entry.Open();
+ using var targetStream = next.Open();
+ if (!IsWordTextXml(entry.FullName))
+ {
+ sourceStream.CopyTo(targetStream);
+ continue;
+ }
+
+ var document = global::System.Xml.Linq.XDocument.Load(sourceStream);
+ foreach (var paragraph in document.Descendants(WordNamespace + "p"))
+ ReplaceParagraphVariables(paragraph, values);
+ document.Save(targetStream, global::System.Xml.Linq.SaveOptions.DisableFormatting);
+ }
+
+ target.Dispose();
+ return output.ToArray();
+ }
+
+ private static void ReplaceParagraphVariables(
+ global::System.Xml.Linq.XElement paragraph,
+ IReadOnlyDictionary values)
+ {
+ var replacements = 0;
+ while (replacements < 1000)
+ {
+ var nodes = paragraph.Descendants(WordNamespace + "t").ToList();
+ var text = string.Concat(nodes.Select(node => node.Value));
+ var match = PlaceholderRegex.Matches(text)
+ .Cast()
+ .LastOrDefault(item => values.ContainsKey(item.Groups["code"].Value.Trim()));
+ if (match == null)
+ return;
+
+ var code = match.Groups["code"].Value.Trim();
+ ReplaceRange(nodes, match.Index, match.Length, values[code] ?? string.Empty);
+ replacements++;
+ }
+
+ throw Oops.Bah("合同模板单个段落中的变量数量异常");
+ }
+
+ private static void ReplaceRange(
+ List nodes,
+ int start,
+ int length,
+ string replacement)
+ {
+ var end = start + length;
+ var offset = 0;
+ global::System.Xml.Linq.XElement startNode = null;
+ global::System.Xml.Linq.XElement endNode = null;
+ var startIndex = 0;
+ var endIndex = 0;
+
+ foreach (var node in nodes)
+ {
+ var nextOffset = offset + node.Value.Length;
+ if (startNode == null && start >= offset && start < nextOffset)
+ {
+ startNode = node;
+ startIndex = start - offset;
+ }
+
+ if (end > offset && end <= nextOffset)
+ {
+ endNode = node;
+ endIndex = end - offset;
+ break;
+ }
+
+ offset = nextOffset;
+ }
+
+ if (startNode == null || endNode == null)
+ return;
+
+ if (ReferenceEquals(startNode, endNode))
+ {
+ startNode.Value = startNode.Value[..startIndex]
+ + replacement
+ + startNode.Value[endIndex..];
+ return;
+ }
+
+ var startNodePosition = nodes.IndexOf(startNode);
+ var endNodePosition = nodes.IndexOf(endNode);
+ startNode.Value = startNode.Value[..startIndex] + replacement;
+ for (var index = startNodePosition + 1; index < endNodePosition; index++)
+ nodes[index].Value = string.Empty;
+ endNode.Value = endNode.Value[endIndex..];
+ }
+
+ private static bool IsWordTextXml(string entryName)
+ {
+ return entryName == "word/document.xml"
+ || ((entryName.StartsWith("word/header", StringComparison.OrdinalIgnoreCase)
+ || entryName.StartsWith("word/footer", StringComparison.OrdinalIgnoreCase))
+ && entryName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));
+ }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractScopeInput.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractScopeInput.cs
deleted file mode 100644
index ca4a1d3..0000000
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/Dto/OnboardingContractScopeInput.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-namespace SimpleAdmin.Application;
-
-///
-/// 合同适用范围分页查询参数。
-///
-public class OnboardingContractScopePageInput : BasePageInput
-{
- ///
- /// 入职业务公司 Id。
- ///
- public long? CompanyId { get; set; }
-
- ///
- /// 岗位 Id;为空时表示公司级配置。
- ///
- public long? PositionId { get; set; }
-
- ///
- /// 合同模板 Id。
- ///
- public long? TemplateId { get; set; }
-}
-
-///
-/// 新增合同适用范围参数。
-///
-public class OnboardingContractScopeAddInput
-{
- ///
- /// 入职业务公司 Id。
- ///
- [IdNotNull(ErrorMessage = "公司Id不能为空")]
- public long CompanyId { get; set; }
-
- ///
- /// 岗位 Id;为空时表示公司级配置。
- ///
- public long? PositionId { get; set; }
-
- ///
- /// 合同模板 Id。
- ///
- [IdNotNull(ErrorMessage = "模板Id不能为空")]
- public long TemplateId { get; set; }
-
- ///
- /// 合同模板版本 Id。
- ///
- [IdNotNull(ErrorMessage = "模板版本Id不能为空")]
- public long TemplateVersionId { get; set; }
-
- ///
- /// 是否必填、必传或必签。
- ///
- public bool Required { get; set; } = true;
-
- ///
- /// 显示排序码。
- ///
- public int? SortCode { get; set; }
-}
-
-///
-/// 编辑合同适用范围参数。
-///
-public class OnboardingContractScopeEditInput : OnboardingContractScopeAddInput
-{
- ///
- /// 业务数据 Id。
- ///
- [IdNotNull(ErrorMessage = "适用范围Id不能为空")]
- public long Id { get; set; }
-}
diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
index 30f7dea..e3d32dd 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/IOnboardingContractTemplateService.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
///
-/// 定义合同模板、版本、变量和适用范围业务能力。
+/// 定义合同模板、DOCX 版本和自动识别变量的管理能力。
///
public interface IOnboardingContractTemplateService : ITransient
{
@@ -9,59 +9,49 @@ public interface IOnboardingContractTemplateService : ITransient
/// 分页查询合同模板主档案。
///
Task> Page(OnboardingContractTemplatePageInput input);
+
///
/// 查询合同模板详情。
///
Task Detail(BaseIdInput input);
+
///
/// 新增合同模板主档案。
///
Task Add(OnboardingContractTemplateAddInput input);
+
///
/// 编辑合同模板主档案。
///
Task Edit(OnboardingContractTemplateEditInput input);
+
///
- /// 删除未被版本、适用范围或任务合同引用的模板。
+ /// 删除未被版本、入职模板或任务合同引用的模板。
///
Task Delete(BaseIdListInput input);
+
///
/// 分页查询指定合同模板的版本。
///
Task> VersionPage(OnboardingContractVersionPageInput input);
+
///
- /// 新增合同模板 DOCX 版本。
+ /// 新增合同模板 DOCX 版本并自动识别变量。
///
Task AddVersion(OnboardingContractVersionAddInput input);
+
///
/// 启用或停用合同模板版本。
///
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
+
///
- /// 分页查询指定模板版本的变量定义。
+ /// 分页查询指定模板版本自动识别出的变量。
///
Task> VariablePage(OnboardingTemplateVariablePageInput input);
+
///
- /// 为草稿模板版本新增变量。
- ///
- Task AddVariable(OnboardingTemplateVariableAddInput input);
- ///
- /// 编辑草稿模板版本中的变量。
+ /// 编辑并确认草稿模板版本中的变量定义。
///
Task EditVariable(OnboardingTemplateVariableEditInput input);
- ///
- /// 删除草稿模板版本中的变量。
- ///
- Task DeleteVariable(BaseIdListInput input); Task> ScopePage(OnboardingContractScopePageInput input);
- ///
- /// 新增公司或岗位的合同适用范围。
- ///
- Task AddScope(OnboardingContractScopeAddInput input);
- ///
- /// 编辑合同适用范围。
- ///
- Task EditScope(OnboardingContractScopeEditInput input);
- ///
- /// 删除合同适用范围。
- ///
- Task DeleteScope(BaseIdListInput input);}
+}
\ 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 41b70d7..9ce9d7b 100644
--- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
@@ -5,6 +5,12 @@ namespace SimpleAdmin.Application;
///
public class OnboardingContractTemplateService : DbRepository, IOnboardingContractTemplateService
{
+ private readonly IFileService _fileService;
+
+ public OnboardingContractTemplateService(IFileService fileService)
+ {
+ _fileService = fileService;
+ }
///
/// 分页查询合同模板主档案。
///
@@ -64,7 +70,12 @@ public class OnboardingContractTemplateService : DbRepository()
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
if (hasScopes)
- throw Oops.Bah("模板已配置适用范围,不能删除");
+ throw Oops.Bah("模板已配置旧版适用范围,不能删除");
+
+ var hasOnboardingTemplates = await Context.Queryable()
+ .AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
+ if (hasOnboardingTemplates)
+ throw Oops.Bah("模板已被入职模板引用,不能删除");
var hasContracts = await Context.Queryable()
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
@@ -92,18 +103,45 @@ public class OnboardingContractTemplateService : DbRepository().InSingleAsync(input.SourceDocxFileId)
- ?? throw Oops.Bah("DOCX源文件不存在");
+ ?? throw Oops.Bah("DOCX 源文件不存在");
if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase))
- throw Oops.Bah("合同模板仅支持DOCX文件");
+ throw Oops.Bah("合同模板仅支持 DOCX 文件");
var versionExists = await Context.Queryable()
- .AnyAsync(it => it.TemplateId == input.TemplateId && it.VersionNo == input.VersionNo);
+ .AnyAsync(item => item.TemplateId == input.TemplateId && item.VersionNo == input.VersionNo);
if (versionExists)
throw Oops.Bah("该模板版本号已存在");
- await Context.Insertable(input.Adapt()).ExecuteCommandAsync();
+ var sourceResult = await _fileService.Download(new BaseIdInput { Id = file.Id })
+ ?? throw Oops.Bah("无法读取 DOCX 源文件");
+ await using var sourceStream = sourceResult.FileStream;
+ using var sourceBytes = new MemoryStream();
+ await sourceStream.CopyToAsync(sourceBytes);
+ var bytes = sourceBytes.ToArray();
+ var variableCodes = DocxTemplateProcessor.ExtractVariableCodes(bytes);
+ var versionId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
+ var version = input.Adapt();
+ version.Id = versionId;
+ version.VersionStatus = ContractTemplateVersionStatus.Draft;
+ version.FileHash = string.IsNullOrWhiteSpace(input.FileHash)
+ ? Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(bytes))
+ : input.FileHash;
+ var variables = variableCodes.Select((code, index) => BuildDetectedVariable(versionId, code, index)).ToList();
+
+ await Context.Ado.BeginTranAsync();
+ try
+ {
+ await Context.Insertable(version).ExecuteCommandAsync();
+ if (variables.Count > 0)
+ await Context.Insertable(variables).ExecuteCommandAsync();
+ await Context.Ado.CommitTranAsync();
+ }
+ catch
+ {
+ await Context.Ado.RollbackTranAsync();
+ throw;
+ }
}
///
@@ -122,10 +160,11 @@ public class OnboardingContractTemplateService : DbRepository()
- .AnyAsync(it => it.TemplateVersionId == input.Id);
- if (!hasVariables)
- throw Oops.Bah("请先配置模板变量后再启用");
+ var hasUnresolvedVariables = await Context.Queryable()
+ .AnyAsync(item => item.TemplateVersionId == input.Id
+ && item.VariableType == ContractVariableType.Unresolved);
+ if (hasUnresolvedVariables)
+ throw Oops.Bah("仍有未识别来源的合同变量,请逐项确认后再启用");
}
version.VersionStatus = input.VersionStatus;
@@ -143,19 +182,8 @@ public class OnboardingContractTemplateService : DbRepository it.VariableCode)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
-
///
- /// 为草稿模板版本新增变量。
- ///
- public async Task AddVariable(OnboardingTemplateVariableAddInput input)
- {
- await EnsureVersionDraft(input.TemplateVersionId);
- await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, 0);
- await Context.Insertable(input.Adapt()).ExecuteCommandAsync();
- }
-
- ///
- /// 编辑草稿模板版本中的变量。
+ /// 编辑草稿模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
///
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
{
@@ -163,77 +191,61 @@ public class OnboardingContractTemplateService : DbRepository()).ExecuteCommandAsync();
+ var entity = input.Adapt();
+ entity.VariableCode = variable.VariableCode;
+ await Context.Updateable(entity).ExecuteCommandAsync();
}
- ///
- /// 删除草稿模板版本中的变量。
- ///
- public async Task DeleteVariable(BaseIdListInput input)
+ private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, int index)
{
- if (input.Ids.Count == 0)
- return;
+ var separatorIndex = code.IndexOf('.');
+ var prefix = separatorIndex > 0 ? code[..separatorIndex].ToLowerInvariant() : string.Empty;
+ var sourceField = separatorIndex > 0 ? code[(separatorIndex + 1)..] : code;
+ var variableType = prefix switch
+ {
+ "employee" => ContractVariableType.Employee,
+ "company" => ContractVariableType.Company,
+ "hr" => ContractVariableType.Contract,
+ "system" => ContractVariableType.System,
+ _ => ContractVariableType.Unresolved
+ };
- 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();
+ return new ObTemplateVariable
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ TemplateVersionId = versionId,
+ VariableCode = code,
+ VariableName = sourceField,
+ VariableType = variableType,
+ DataType = InferDataType(sourceField),
+ EmployeeField = variableType is ContractVariableType.Employee or ContractVariableType.Company
+ ? sourceField
+ : null,
+ Required = true,
+ HrEditable = variableType == ContractVariableType.Contract,
+ SortCode = (index + 1) * 10,
+ Remark = variableType == ContractVariableType.Unresolved ? "导入时未识别变量来源,请管理员确认" : null
+ };
}
- ///
- /// 分页查询合同适用范围。
- ///
- public async Task> ScopePage(OnboardingContractScopePageInput input)
+ private static string InferDataType(string sourceField)
{
- 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);
- }
+ var normalized = sourceField.ToLowerInvariant();
+ if (normalized.Contains("date") || normalized.EndsWith("at"))
+ return "date";
+ if (normalized.Contains("salary")
+ || normalized.Contains("amount")
+ || normalized.Contains("wage")
+ || normalized.Contains("copies"))
+ {
+ return "decimal";
+ }
- ///
- /// 新增公司或岗位的合同适用范围。
- ///
- public async Task AddScope(OnboardingContractScopeAddInput input)
- {
- await EnsureScopeValid(input.CompanyId, input.PositionId, input.TemplateId, input.TemplateVersionId);
- await EnsureScopeUnique(input.CompanyId, input.PositionId, input.TemplateVersionId, 0);
- await Context.Insertable(input.Adapt()).ExecuteCommandAsync();
- }
-
- ///
- /// 编辑合同适用范围。
- ///
- public async Task EditScope(OnboardingContractScopeEditInput input)
- {
- 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();
- }
-
- ///
- /// 删除合同适用范围。
- ///
- public async Task DeleteScope(BaseIdListInput input)
- {
- if (input.Ids.Count == 0)
- return;
-
- await Context.Deleteable().In(input.Ids).ExecuteCommandAsync();
+ return "string";
}
private async Task EnsureCodeUnique(string code, long id)
@@ -251,44 +263,4 @@ public class OnboardingContractTemplateService : DbRepository()
- .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)
- {
- 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)
- {
- 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/EntryConfig/Dto/OnboardingEntryConfigInput.cs b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs
index cacfc21..62cb02c 100644
--- a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/Dto/OnboardingEntryConfigInput.cs
@@ -91,79 +91,3 @@ public class OnboardingMaterialTypeSaveInput
///
public string Status { get; set; } = "enable";
}
-
-///
-/// 保存公司岗位动态字段要求参数。
-///
-public class OnboardingFormRequirementSaveInput
-{
- ///
- /// 业务数据 Id。
- ///
- public long? Id { get; set; }
-
- ///
- /// 入职业务公司 Id。
- ///
- [IdNotNull(ErrorMessage = "公司Id不能为空")]
- public long CompanyId { get; set; }
-
- ///
- /// 岗位 Id;为空时表示公司级配置。
- ///
- public long? PositionId { get; set; }
-
- ///
- /// 动态字段 Id。
- ///
- [IdNotNull(ErrorMessage = "字段Id不能为空")]
- public long FieldId { get; set; }
-
- ///
- /// 是否必填、必传或必签。
- ///
- public bool Required { get; set; }
-
- ///
- /// 显示排序码。
- ///
- public int? SortCode { get; set; }
-}
-
-///
-/// 保存公司岗位材料要求参数。
-///
-public class OnboardingMaterialRequirementSaveInput
-{
- ///
- /// 业务数据 Id。
- ///
- public long? Id { get; set; }
-
- ///
- /// 入职业务公司 Id。
- ///
- [IdNotNull(ErrorMessage = "公司Id不能为空")]
- public long CompanyId { get; set; }
-
- ///
- /// 岗位 Id;为空时表示公司级配置。
- ///
- public long? PositionId { get; set; }
-
- ///
- /// 材料类型 Id。
- ///
- [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
index 5bb40b3..24b3d75 100644
--- a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/IOnboardingEntryConfigService.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
///
-/// 定义动态字段、材料类型及公司岗位要求配置业务能力。
+/// 定义员工动态补充字段与上传材料类型的基础配置能力。
///
public interface IOnboardingEntryConfigService : ITransient
{
@@ -9,48 +9,29 @@ 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
index 6c3977c..dd67379 100644
--- a/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/EntryConfig/OnboardingEntryConfigService.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
///
-/// 维护员工端动态字段、材料类型及公司岗位要求。
+/// 维护员工端动态补充字段和材料类型;与公司、岗位的组合关系由入职模板统一管理。
///
public class OnboardingEntryConfigService : IOnboardingEntryConfigService
{
@@ -92,8 +92,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
var isReferenced = await _db.Queryable()
.AnyAsync(x => input.Ids.Contains(x.FieldId));
- if (isReferenced)
- throw Oops.Bah("字段已被填写要求引用,不能删除");
+ var isReferencedByTemplate = await _db.Queryable()
+ .AnyAsync(x => input.Ids.Contains(x.FieldId));
+ if (isReferenced || isReferencedByTemplate)
+ throw Oops.Bah("字段已被入职模板引用,不能删除");
await _db.Deleteable().In(input.Ids).ExecuteCommandAsync();
}
@@ -155,8 +157,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
var isReferenced = await _db.Queryable()
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
- if (isReferenced)
- throw Oops.Bah("资料类型已被资料要求引用,不能删除");
+ var isReferencedByTemplate = await _db.Queryable()
+ .AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
+ if (isReferenced || isReferencedByTemplate)
+ throw Oops.Bah("资料类型已被入职模板引用,不能删除");
var hasUploads = await _db.Queryable()
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
@@ -165,142 +169,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
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("岗位不存在、未启用或不属于该公司");
- }
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/Dto/OnboardingTemplateInput.cs b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/Dto/OnboardingTemplateInput.cs
new file mode 100644
index 0000000..22037bc
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/Dto/OnboardingTemplateInput.cs
@@ -0,0 +1,213 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板分页查询参数。
+///
+public class OnboardingTemplatePageInput : BasePageInput
+{
+ ///
+ /// 模板编码或名称关键词。
+ ///
+ public string Keyword { get; set; }
+
+ ///
+ /// 所属公司 Id。
+ ///
+ public long? CompanyId { get; set; }
+
+ ///
+ /// 适用岗位 Id。
+ ///
+ public long? PositionId { get; set; }
+
+ ///
+ /// 启用状态。
+ ///
+ public string Status { get; set; }
+}
+
+///
+/// 保存入职模板参数。
+///
+public class OnboardingTemplateSaveInput
+{
+ ///
+ /// 编辑时传入的入职模板 Id。
+ ///
+ public long? Id { get; set; }
+
+ ///
+ /// 入职模板业务编码。
+ ///
+ public string TemplateCode { get; set; }
+
+ ///
+ /// 入职模板名称。
+ ///
+ public string TemplateName { get; set; }
+
+ ///
+ /// 所属公司 Id。
+ ///
+ public long CompanyId { get; set; }
+
+ ///
+ /// 适用岗位 Id 列表。
+ ///
+ public List PositionIds { get; set; } = new();
+
+ ///
+ /// 合同配置列表。
+ ///
+ public List Contracts { get; set; } = new();
+
+ ///
+ /// 动态补充字段列表。
+ ///
+ public List FormFields { get; set; } = new();
+
+ ///
+ /// 上传材料列表。
+ ///
+ public List Materials { get; set; } = new();
+
+ ///
+ /// 模板状态。
+ ///
+ public string Status { get; set; } = "enable";
+
+ ///
+ /// 模板说明。
+ ///
+ public string Description { get; set; }
+}
+
+///
+/// 入职模板合同配置参数。
+///
+public class OnboardingTemplateContractInput
+{
+ ///
+ /// 合同模板 Id。
+ ///
+ public long ContractTemplateId { get; set; }
+
+ ///
+ /// 合同模板版本 Id。
+ ///
+ public long ContractTemplateVersionId { get; set; }
+
+ ///
+ /// 是否必须查看确认。
+ ///
+ public bool Required { get; set; } = true;
+
+ ///
+ /// 打印份数。
+ ///
+ public int PrintCopies { get; set; } = 1;
+
+ ///
+ /// 排序码。
+ ///
+ public int? SortCode { get; set; }
+}
+
+///
+/// 入职模板补充字段参数。
+///
+public class OnboardingTemplateFormFieldInput
+{
+ ///
+ /// 动态字段 Id。
+ ///
+ public long FieldId { get; set; }
+
+ ///
+ /// 是否必填。
+ ///
+ public bool Required { get; set; } = true;
+
+ ///
+ /// 排序码。
+ ///
+ public int? SortCode { get; set; }
+}
+
+///
+/// 入职模板上传材料参数。
+///
+public class OnboardingTemplateMaterialInput
+{
+ ///
+ /// 材料类型 Id。
+ ///
+ public long MaterialTypeId { get; set; }
+
+ ///
+ /// 是否必传。
+ ///
+ public bool Required { get; set; } = true;
+
+ ///
+ /// 排序码。
+ ///
+ public int? SortCode { get; set; }
+}
+
+///
+/// 入职模板列表输出。
+///
+public class OnboardingTemplateOutput : ObOnboardingTemplate
+{
+ ///
+ /// 所属公司名称。
+ ///
+ public string CompanyName { get; set; }
+
+ ///
+ /// 适用岗位 Id 列表。
+ ///
+ public List PositionIds { get; set; } = new();
+
+ ///
+ /// 适用岗位名称列表。
+ ///
+ public List PositionNames { get; set; } = new();
+
+ ///
+ /// 合同配置数量。
+ ///
+ public int ContractCount { get; set; }
+
+ ///
+ /// 动态补充字段数量。
+ ///
+ public int FormFieldCount { get; set; }
+
+ ///
+ /// 上传材料数量。
+ ///
+ public int MaterialCount { get; set; }
+}
+
+///
+/// 入职模板详情输出。
+///
+public class OnboardingTemplateDetailOutput : OnboardingTemplateOutput
+{
+ ///
+ /// 合同配置明细。
+ ///
+ public List Contracts { get; set; } = new();
+
+ ///
+ /// 动态补充字段配置明细。
+ ///
+ public List FormFields { get; set; } = new();
+
+ ///
+ /// 上传材料配置明细。
+ ///
+ public List Materials { get; set; } = new();
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
new file mode 100644
index 0000000..c148d40
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/IOnboardingTemplateService.cs
@@ -0,0 +1,32 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 入职模板聚合配置服务。
+///
+public interface IOnboardingTemplateService
+{
+ ///
+ /// 分页查询入职模板。
+ ///
+ Task> Page(OnboardingTemplatePageInput input);
+
+ ///
+ /// 查询入职模板详情。
+ ///
+ Task Detail(BaseIdInput input);
+
+ ///
+ /// 新增或编辑入职模板及全部关联配置。
+ ///
+ Task Save(OnboardingTemplateSaveInput input);
+
+ ///
+ /// 删除未被任务使用的入职模板。
+ ///
+ Task Delete(BaseIdListInput input);
+
+ ///
+ /// 查询公司岗位可使用的入职模板。
+ ///
+ Task> ListEnabled(long companyId, long positionId);
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
new file mode 100644
index 0000000..8cedffa
--- /dev/null
+++ b/SimpleAdmin.Application/Services/Onboarding/OnboardingTemplate/OnboardingTemplateService.cs
@@ -0,0 +1,431 @@
+namespace SimpleAdmin.Application;
+
+///
+/// 实现入职模板聚合配置及公司岗位匹配能力。
+///
+public class OnboardingTemplateService : IOnboardingTemplateService
+{
+ private readonly SqlSugarScope _db;
+
+ public OnboardingTemplateService()
+ {
+ _db = DbContext.DB;
+ }
+
+ ///
+ public async Task> Page(OnboardingTemplatePageInput input)
+ {
+ var query = _db.Queryable()
+ .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword),
+ item => item.TemplateCode.Contains(input.Keyword) || item.TemplateName.Contains(input.Keyword))
+ .WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
+ .WhereIF(!string.IsNullOrWhiteSpace(input.Status), item => item.Status == input.Status);
+
+ if (input.PositionId.HasValue)
+ {
+ var templateIds = await _db.Queryable()
+ .Where(item => item.PositionId == input.PositionId.Value)
+ .Select(item => item.OnboardingTemplateId)
+ .ToListAsync();
+ query = query.Where(item => templateIds.Contains(item.Id));
+ }
+
+ var page = await query
+ .OrderByDescending(item => item.CreateTime)
+ .ToPagedListAsync(input.PageNum, input.PageSize);
+ var rows = await BuildOutputs(page.List.ToList());
+
+ return new SqlSugarPagedList
+ {
+ PageNum = page.PageNum,
+ PageSize = page.PageSize,
+ Total = page.Total,
+ Pages = page.Pages,
+ HasPrevPages = page.HasPrevPages,
+ HasNextPages = page.HasNextPages,
+ List = rows
+ };
+ }
+
+ ///
+ public async Task Detail(BaseIdInput input)
+ {
+ var template = await _db.Queryable().InSingleAsync(input.Id)
+ ?? throw Oops.Bah("入职模板不存在");
+ var summary = (await BuildOutputs(new List { template })).Single();
+
+ return new OnboardingTemplateDetailOutput
+ {
+ Id = summary.Id,
+ TemplateCode = summary.TemplateCode,
+ TemplateName = summary.TemplateName,
+ CompanyId = summary.CompanyId,
+ CompanyName = summary.CompanyName,
+ Description = summary.Description,
+ Status = summary.Status,
+ CreateTime = summary.CreateTime,
+ UpdateTime = summary.UpdateTime,
+ PositionIds = summary.PositionIds,
+ PositionNames = summary.PositionNames,
+ ContractCount = summary.ContractCount,
+ FormFieldCount = summary.FormFieldCount,
+ MaterialCount = summary.MaterialCount,
+ Contracts = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == template.Id)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync(),
+ FormFields = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == template.Id)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync(),
+ Materials = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == template.Id)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync()
+ };
+ }
+
+ ///
+ public async Task Save(OnboardingTemplateSaveInput input)
+ {
+ NormalizeAndValidateInput(input);
+ await ValidateReferences(input);
+
+ var templateId = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
+ var codeExists = await _db.Queryable()
+ .AnyAsync(item => item.TemplateCode == input.TemplateCode && item.Id != templateId);
+ if (codeExists)
+ throw Oops.Bah("入职模板编码已存在");
+
+ var entity = new ObOnboardingTemplate
+ {
+ Id = templateId,
+ TemplateCode = input.TemplateCode.Trim(),
+ TemplateName = input.TemplateName.Trim(),
+ CompanyId = input.CompanyId,
+ Description = input.Description,
+ Status = input.Status
+ };
+
+ await _db.Ado.BeginTranAsync();
+ try
+ {
+ if (input.Id.HasValue)
+ {
+ var exists = await _db.Queryable().AnyAsync(item => item.Id == templateId);
+ if (!exists)
+ throw Oops.Bah("入职模板不存在");
+ await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
+ await DeleteRelations(templateId);
+ }
+ else
+ {
+ await _db.Insertable(entity).ExecuteCommandAsync();
+ }
+
+ await InsertRelations(templateId, input);
+ await _db.Ado.CommitTranAsync();
+ return templateId;
+ }
+ catch
+ {
+ await _db.Ado.RollbackTranAsync();
+ throw;
+ }
+ }
+
+ ///
+ public async Task Delete(BaseIdListInput input)
+ {
+ if (input.Ids.Count == 0)
+ return;
+
+ var isUsed = await _db.Queryable()
+ .AnyAsync(item => input.Ids.Contains(item.OnboardingTemplateId));
+ if (isUsed)
+ throw Oops.Bah("入职模板已生成任务,不能删除,可以改为停用");
+
+ await _db.Ado.BeginTranAsync();
+ try
+ {
+ foreach (var id in input.Ids)
+ await DeleteRelations(id);
+ await _db.Deleteable().In(input.Ids).ExecuteCommandAsync();
+ await _db.Ado.CommitTranAsync();
+ }
+ catch
+ {
+ await _db.Ado.RollbackTranAsync();
+ throw;
+ }
+ }
+
+ ///
+ public async Task> ListEnabled(long companyId, long positionId)
+ {
+ var templateIds = await _db.Queryable()
+ .Where(item => item.PositionId == positionId)
+ .Select(item => item.OnboardingTemplateId)
+ .ToListAsync();
+ var templates = await _db.Queryable()
+ .Where(item => item.CompanyId == companyId
+ && item.Status == "enable"
+ && templateIds.Contains(item.Id))
+ .OrderBy(item => item.TemplateName)
+ .ToListAsync();
+ return await BuildOutputs(templates);
+ }
+
+ private static void NormalizeAndValidateInput(OnboardingTemplateSaveInput input)
+ {
+ input.TemplateCode = input.TemplateCode?.Trim();
+ input.TemplateName = input.TemplateName?.Trim();
+ input.PositionIds = input.PositionIds?.Distinct().ToList() ?? new List();
+ input.Contracts = input.Contracts?
+ .GroupBy(item => item.ContractTemplateVersionId)
+ .Select(group => group.First())
+ .ToList() ?? new List();
+ input.FormFields = input.FormFields?
+ .GroupBy(item => item.FieldId)
+ .Select(group => group.First())
+ .ToList() ?? new List();
+ input.Materials = input.Materials?
+ .GroupBy(item => item.MaterialTypeId)
+ .Select(group => group.First())
+ .ToList() ?? new List();
+
+ if (string.IsNullOrWhiteSpace(input.TemplateCode) || string.IsNullOrWhiteSpace(input.TemplateName))
+ throw Oops.Bah("请填写入职模板编码和名称");
+ if (input.CompanyId <= 0)
+ throw Oops.Bah("请选择所属公司");
+ if (input.PositionIds.Count == 0)
+ throw Oops.Bah("请至少选择一个适用岗位");
+ if (input.Contracts.Count == 0)
+ throw Oops.Bah("请至少选择一份合同文件");
+ if (input.Status != "enable" && input.Status != "disable")
+ throw Oops.Bah("入职模板状态不正确");
+ if (input.Contracts.Any(item => item.PrintCopies <= 0))
+ throw Oops.Bah("合同打印份数必须大于 0");
+ }
+
+ private async Task ValidateReferences(OnboardingTemplateSaveInput input)
+ {
+ var companyEnabled = await _db.Queryable()
+ .AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
+ if (!companyEnabled)
+ throw Oops.Bah("所属公司不存在或未启用");
+
+ var positionCount = await _db.Queryable()
+ .CountAsync(item => input.PositionIds.Contains(item.Id)
+ && item.CompanyId == input.CompanyId
+ && item.Status == "enable");
+ if (positionCount != input.PositionIds.Count)
+ throw Oops.Bah("存在不属于该公司或未启用的岗位");
+
+ var versionIds = input.Contracts.Select(item => item.ContractTemplateVersionId).ToList();
+ var versions = await _db.Queryable()
+ .Where(item => versionIds.Contains(item.Id))
+ .ToListAsync();
+ if (versions.Count != versionIds.Count
+ || input.Contracts.Any(contract => versions.All(version => version.Id != contract.ContractTemplateVersionId
+ || version.TemplateId != contract.ContractTemplateId)))
+ {
+ throw Oops.Bah("合同模板与版本不匹配");
+ }
+
+ if (input.Status == "enable")
+ {
+ if (versions.Any(item => item.VersionStatus != ContractTemplateVersionStatus.Enabled))
+ throw Oops.Bah("启用入职模板前,请先启用全部合同版本");
+
+ var variables = await _db.Queryable()
+ .Where(item => versionIds.Contains(item.TemplateVersionId))
+ .ToListAsync();
+ var validTypes = new[]
+ {
+ ContractVariableType.Employee,
+ ContractVariableType.Company,
+ ContractVariableType.Contract,
+ ContractVariableType.System
+ };
+ if (variables.Any(item => !validTypes.Contains(item.VariableType)))
+ throw Oops.Bah("合同中仍有未确认来源的变量,请先完成变量映射");
+ }
+
+ var fieldIds = input.FormFields.Select(item => item.FieldId).ToList();
+ var enabledFields = fieldIds.Count == 0
+ ? new List()
+ : await _db.Queryable()
+ .Where(item => fieldIds.Contains(item.Id) && item.Status == "enable")
+ .ToListAsync();
+ if (enabledFields.Count != fieldIds.Count)
+ throw Oops.Bah("存在未启用或不存在的动态补充字段");
+
+ if (input.Status == "enable")
+ {
+ var variables = await _db.Queryable()
+ .Where(item => versionIds.Contains(item.TemplateVersionId)
+ && item.VariableType == ContractVariableType.Employee)
+ .ToListAsync();
+ var availableFields = new HashSet(
+ typeof(ObTaskEmployeeInfo).GetProperties().Select(property => property.Name),
+ StringComparer.OrdinalIgnoreCase);
+ foreach (var fieldCode in enabledFields.Select(item => item.FieldCode))
+ availableFields.Add(fieldCode);
+
+ var missingMapping = variables.FirstOrDefault(variable =>
+ !availableFields.Contains(ResolveEmployeeVariableField(variable)));
+ if (missingMapping != null)
+ {
+ throw Oops.Bah(
+ $"合同员工变量“{missingMapping.VariableCode}”没有对应的固定字段或模板补充字段,请先完成字段映射");
+ }
+ }
+
+ var materialIds = input.Materials.Select(item => item.MaterialTypeId).ToList();
+ if (materialIds.Count > 0)
+ {
+ var materialCount = await _db.Queryable()
+ .CountAsync(item => materialIds.Contains(item.Id) && item.Status == "enable");
+ if (materialCount != materialIds.Count)
+ throw Oops.Bah("存在未启用或不存在的上传材料类型");
+ }
+ }
+
+ private static string ResolveEmployeeVariableField(ObTemplateVariable variable)
+ {
+ var sourceField = variable.EmployeeField;
+ if (string.IsNullOrWhiteSpace(sourceField))
+ {
+ var separatorIndex = variable.VariableCode?.IndexOf('.') ?? -1;
+ sourceField = separatorIndex >= 0
+ ? variable.VariableCode[(separatorIndex + 1)..]
+ : variable.VariableCode;
+ }
+
+ return sourceField?.ToUpperInvariant() switch
+ {
+ "FULLNAME" or "FULL_NAME" or "NAME" => nameof(ObTaskEmployeeInfo.EmployeeName),
+ "MOBILE" or "MOBILEPHONE" or "MOBILE_PHONE" => nameof(ObTaskEmployeeInfo.Phone),
+ "IDNUMBER" or "ID_NUMBER" or "IDENTITYNUMBER" => nameof(ObTaskEmployeeInfo.IdCardNumber),
+ _ => sourceField
+ };
+ }
+ private async Task InsertRelations(long templateId, OnboardingTemplateSaveInput input)
+ {
+ var positions = input.PositionIds.Select(positionId => new ObOnboardingTemplatePosition
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ OnboardingTemplateId = templateId,
+ PositionId = positionId
+ }).ToList();
+ await _db.Insertable(positions).ExecuteCommandAsync();
+
+ var contracts = input.Contracts.Select(item => new ObOnboardingTemplateContract
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ OnboardingTemplateId = templateId,
+ ContractTemplateId = item.ContractTemplateId,
+ ContractTemplateVersionId = item.ContractTemplateVersionId,
+ Required = item.Required,
+ PrintCopies = item.PrintCopies,
+ SortCode = item.SortCode
+ }).ToList();
+ await _db.Insertable(contracts).ExecuteCommandAsync();
+
+ if (input.FormFields.Count > 0)
+ {
+ var fields = input.FormFields.Select(item => new ObOnboardingTemplateFormField
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ OnboardingTemplateId = templateId,
+ FieldId = item.FieldId,
+ Required = item.Required,
+ SortCode = item.SortCode
+ }).ToList();
+ await _db.Insertable(fields).ExecuteCommandAsync();
+ }
+
+ if (input.Materials.Count > 0)
+ {
+ var materials = input.Materials.Select(item => new ObOnboardingTemplateMaterial
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ OnboardingTemplateId = templateId,
+ MaterialTypeId = item.MaterialTypeId,
+ Required = item.Required,
+ SortCode = item.SortCode
+ }).ToList();
+ await _db.Insertable(materials).ExecuteCommandAsync();
+ }
+ }
+
+ private async Task DeleteRelations(long templateId)
+ {
+ await _db.Deleteable()
+ .Where(item => item.OnboardingTemplateId == templateId)
+ .ExecuteCommandAsync();
+ await _db.Deleteable()
+ .Where(item => item.OnboardingTemplateId == templateId)
+ .ExecuteCommandAsync();
+ await _db.Deleteable()
+ .Where(item => item.OnboardingTemplateId == templateId)
+ .ExecuteCommandAsync();
+ await _db.Deleteable()
+ .Where(item => item.OnboardingTemplateId == templateId)
+ .ExecuteCommandAsync();
+ }
+
+ private async Task> BuildOutputs(List templates)
+ {
+ if (templates.Count == 0)
+ return new List();
+
+ var templateIds = templates.Select(item => item.Id).ToList();
+ var companyIds = templates.Select(item => item.CompanyId).Distinct().ToList();
+ var companies = await _db.Queryable()
+ .Where(item => companyIds.Contains(item.Id))
+ .ToListAsync();
+ var relations = await _db.Queryable()
+ .Where(item => templateIds.Contains(item.OnboardingTemplateId))
+ .ToListAsync();
+ var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
+ var positions = await _db.Queryable()
+ .Where(item => positionIds.Contains(item.Id))
+ .ToListAsync();
+ var contracts = await _db.Queryable()
+ .Where(item => templateIds.Contains(item.OnboardingTemplateId))
+ .ToListAsync();
+ var fields = await _db.Queryable()
+ .Where(item => templateIds.Contains(item.OnboardingTemplateId))
+ .ToListAsync();
+ var materials = await _db.Queryable()
+ .Where(item => templateIds.Contains(item.OnboardingTemplateId))
+ .ToListAsync();
+
+ return templates.Select(template =>
+ {
+ var templatePositions = relations.Where(item => item.OnboardingTemplateId == template.Id).ToList();
+ return new OnboardingTemplateOutput
+ {
+ Id = template.Id,
+ TemplateCode = template.TemplateCode,
+ TemplateName = template.TemplateName,
+ CompanyId = template.CompanyId,
+ CompanyName = companies.FirstOrDefault(item => item.Id == template.CompanyId)?.CompanyName,
+ Description = template.Description,
+ Status = template.Status,
+ CreateTime = template.CreateTime,
+ UpdateTime = template.UpdateTime,
+ PositionIds = templatePositions.Select(item => item.PositionId).ToList(),
+ PositionNames = templatePositions
+ .Select(relation => positions.FirstOrDefault(position => position.Id == relation.PositionId)?.PositionName)
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .ToList(),
+ ContractCount = contracts.Count(item => item.OnboardingTemplateId == template.Id),
+ FormFieldCount = fields.Count(item => item.OnboardingTemplateId == template.Id),
+ MaterialCount = materials.Count(item => item.OnboardingTemplateId == template.Id)
+ };
+ }).ToList();
+ }
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
index 79f61aa..c3ba935 100644
--- a/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/PublicSubmission/OnboardingPublicSubmissionService.cs
@@ -1,92 +1,95 @@
namespace SimpleAdmin.Application;
///
-/// 实现员工端入职资料提交业务能力。
+/// 实现员工端动态资料、上传材料、合同确认和最终提交能力。
///
public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionService
{
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
- public OnboardingPublicSubmissionService(
- IOnboardingContractSnapshotService snapshotService)
+ public OnboardingPublicSubmissionService(IOnboardingContractSnapshotService snapshotService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
}
///
- /// 保存候选人填写的动态字段值。
+ /// 保存候选人填写的动态补充字段值。
///
public async Task SaveDynamicValues(OnboardingSaveDynamicValuesInput input)
{
var task = await GetEditableTask(input.Token);
- var requirements = await GetEffectiveFormRequirements(task);
+ var requirements = await GetFormRequirements(task.Id);
var fieldIds = input.Values.Select(item => item.FieldId).Distinct().ToList();
- var hasInapplicableField = fieldIds.Any(id => requirements.All(requirement => requirement.FieldId != id));
+ var hasInapplicableField = fieldIds.Any(id => requirements.All(item => item.FieldId != id));
if (hasInapplicableField)
- throw Oops.Bah("填写字段不适用于当前公司或岗位");
+ 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 requirement = requirements.FirstOrDefault(item => item.FieldId == value.FieldId)
+ ?? throw Oops.Bah("动态补充字段不存在");
var old = await _db.Queryable()
- .FirstAsync(item => item.TaskId == task.Id && item.FieldId == field.Id);
+ .FirstAsync(item => item.TaskId == task.Id && item.FieldId == requirement.FieldId);
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,
+ FieldId = requirement.FieldId,
+ FieldCode = requirement.FieldCode,
+ FieldName = requirement.FieldName,
FieldValue = value.FieldValue,
SubmitRound = task.SubmitCount + 1
};
if (old == null)
- {
await _db.Insertable(row).ExecuteCommandAsync();
- }
else
- {
await _db.Updateable(row).ExecuteCommandAsync();
- }
}
+
+ await _snapshotService.Build(task.Id, invalidateGeneratedPreview: true);
}
///
- /// 关联候选人已经上传的系统文件与材料类型。
+ /// 关联已上传 OSS 文件与任务材料要求。
///
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 requirement = await _db.Queryable()
+ .FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
+ ?? 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("该资料已达到最大上传数量");
+ var existingMaterials = await _db.Queryable()
+ .Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
+ .ToListAsync();
+ var supplementMaterial = existingMaterials.FirstOrDefault(
+ item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
+ if (supplementMaterial != null)
+ {
+ supplementMaterial.FileId = input.FileId;
+ supplementMaterial.SubmitRound = task.SubmitCount + 1;
+ supplementMaterial.UploadedAt = DateTime.Now;
+ supplementMaterial.ReviewStatus = TaskMaterialReviewStatus.Pending;
+ supplementMaterial.ReviewRemark = null;
+ await _db.Updateable(supplementMaterial).ExecuteCommandAsync();
+ return;
+ }
+
+ if (existingMaterials.Count >= requirement.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,
+ MaterialTypeId = requirement.MaterialTypeId,
+ MaterialName = requirement.MaterialName,
FileId = input.FileId,
SubmitRound = task.SubmitCount + 1,
UploadedAt = DateTime.Now
@@ -94,7 +97,7 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
}
///
- /// 记录候选人已阅读并确认指定任务合同。
+ /// 记录候选人已经查看并确认动态生成的合同。
///
public async Task ConfirmContract(OnboardingConfirmContractInput input)
{
@@ -102,15 +105,16 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
var contract = await _db.Queryable()
.FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == task.Id)
?? throw Oops.Bah("合同不存在");
+ if (!contract.GeneratedDocxFileId.HasValue || !contract.ReadAt.HasValue)
+ 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)
{
@@ -126,8 +130,7 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
await _snapshotService.Build(task.Id, info);
- // 提交时统一完成动态字段、材料、合同变量和合同确认校验,保存阶段只负责暂存。
- var requiredFields = (await GetEffectiveFormRequirements(task))
+ var requiredFields = (await GetFormRequirements(task.Id))
.Where(item => item.Required)
.Select(item => item.FieldId)
.ToList();
@@ -137,9 +140,9 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
var hasMissingRequiredField = requiredFields.Any(
id => values.All(item => item.FieldId != id || string.IsNullOrWhiteSpace(item.FieldValue)));
if (hasMissingRequiredField)
- throw Oops.Bah("仍有必填动态信息未填写");
+ throw Oops.Bah("仍有必填补充信息未填写");
- var requiredMaterials = (await GetEffectiveMaterialRequirements(task))
+ var requiredMaterials = (await GetMaterialRequirements(task.Id))
.Where(item => item.Required)
.Select(item => item.MaterialTypeId)
.ToList();
@@ -149,14 +152,14 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
var hasMissingRequiredMaterial = requiredMaterials.Any(
id => materials.All(item => item.MaterialTypeId != id));
if (hasMissingRequiredMaterial)
- throw Oops.Bah("仍有必传资料未上传");
+ 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("请确认阅读全部必签合同");
+ throw Oops.Bah("请查看并确认全部必签合同");
task.TaskStatus = OnboardingTaskStatus.Reviewing;
task.SubmitCount++;
@@ -177,36 +180,34 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
}
///
- /// 在保存文件前校验材料适用范围、扩展名、大小和数量。
+ /// 上传 OSS 前按照任务材料快照校验扩展名、文件大小和数量。
///
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 requirement = await _db.Queryable()
+ .FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
+ ?? throw Oops.Bah("材料类型不属于当前入职模板");
var suffix = Path.GetExtension(input.FileName).TrimStart('.').ToLowerInvariant();
- var allowedExtensions = (material.AllowedExtensions ?? string.Empty)
+ var allowedExtensions = (requirement.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("该资料不允许上传此文件类型");
+ throw Oops.Bah("该材料不允许上传此文件类型");
- var fileTooLarge = material.MaxFileSizeMb.HasValue
- && input.FileLength > material.MaxFileSizeMb.Value * 1024L * 1024L;
+ var fileTooLarge = requirement.MaxFileSizeMb.HasValue
+ && input.FileLength > requirement.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("该资料已达到最大上传数量");
+ var materials = await _db.Queryable()
+ .Where(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
+ .ToListAsync();
+ var canReplaceSupplement = materials.Any(
+ item => item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
+ if (materials.Count >= requirement.MaxFileCount && !canReplaceSupplement)
+ throw Oops.Bah("该材料已达到最大上传数量");
}
private async Task EnsureRequiredContractVariables(ObOnboardingTask task)
@@ -234,30 +235,20 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
throw Oops.Bah($"合同“{missing.contract.ContractName}”的必填变量“{missing.definition.VariableName}”尚未填写");
}
- private async Task> GetEffectiveFormRequirements(ObOnboardingTask task)
+ private Task> GetFormRequirements(long taskId)
{
- var requirements = await _db.Queryable()
- .Where(item => item.CompanyId == task.CompanyId
- && (item.PositionId == null || item.PositionId == task.PositionId))
+ return _db.Queryable()
+ .Where(item => item.TaskId == taskId)
+ .OrderBy(item => item.SortCode)
.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)
+ private Task> GetMaterialRequirements(long taskId)
{
- var requirements = await _db.Queryable()
- .Where(item => item.CompanyId == task.CompanyId
- && (item.PositionId == null || item.PositionId == task.PositionId))
+ return _db.Queryable()
+ .Where(item => item.TaskId == taskId)
+ .OrderBy(item => item.SortCode)
.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)
@@ -283,4 +274,4 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
return task;
}
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
index 31d8c25..28866ea 100644
--- a/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/PublicTask/OnboardingPublicTaskService.cs
@@ -1,22 +1,25 @@
namespace SimpleAdmin.Application;
///
-/// 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。
+/// 员工手机端公开入口,仅凭一次性任务链接访问入职资料。
///
public class OnboardingPublicTaskService : IOnboardingPublicTaskService
{
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
+ private readonly IOnboardingContractDocumentService _contractDocumentService;
public OnboardingPublicTaskService(
- IOnboardingContractSnapshotService snapshotService)
+ IOnboardingContractSnapshotService snapshotService,
+ IOnboardingContractDocumentService contractDocumentService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
+ _contractDocumentService = contractDocumentService;
}
///
- /// 通过有效 Token 获取员工端任务、字段、材料和合同信息。
+ /// 通过有效 Token 获取任务创建时冻结的字段、材料和合同信息。
///
public async Task Detail(OnboardingTaskPublicAccessInput input)
{
@@ -28,70 +31,65 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
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")
+ var formRequirements = await _db.Queryable()
+ .Where(item => item.TaskId == task.Id)
+ .OrderBy(item => item.SortCode)
.ToListAsync();
- var materialTypes = await _db.Queryable()
- .Where(x => materialRequirements.Select(r => r.MaterialTypeId).Contains(x.Id) && x.Status == "enable")
+ var materialRequirements = await _db.Queryable()
+ .Where(item => item.TaskId == task.Id)
+ .OrderBy(item => item.SortCode)
.ToListAsync();
return new OnboardingTaskPublicDetailOutput
{
Task = task,
- EmployeeInfo = await _db.Queryable().FirstAsync(it => it.TaskId == task.Id),
+ EmployeeInfo = await _db.Queryable()
+ .FirstAsync(item => item.TaskId == task.Id),
Contracts = await _db.Queryable()
- .Where(it => it.TaskId == task.Id)
+ .Where(item => item.TaskId == task.Id)
.ToListAsync(),
DynamicValues = await _db.Queryable()
- .Where(x => x.TaskId == task.Id)
+ .Where(item => item.TaskId == task.Id)
.ToListAsync(),
Materials = await _db.Queryable()
- .Where(x => x.TaskId == task.Id)
+ .Where(item => item.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()
+ FormFields = formRequirements.Select(requirement => new OnboardingPublicFormFieldOutput
+ {
+ FieldId = requirement.FieldId,
+ FieldCode = requirement.FieldCode,
+ FieldName = requirement.FieldName,
+ DataType = requirement.DataType,
+ OptionsJson = requirement.OptionsJson,
+ Required = requirement.Required
+ }).ToList(),
+ MaterialRequirements = materialRequirements.Select(requirement => new OnboardingPublicMaterialOutput
+ {
+ MaterialTypeId = requirement.MaterialTypeId,
+ MaterialName = requirement.MaterialName,
+ AllowedExtensions = requirement.AllowedExtensions,
+ MaxFileCount = requirement.MaxFileCount,
+ Required = requirement.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)
+ if (task.TaskStatus != OnboardingTaskStatus.PendingFill
+ && task.TaskStatus != OnboardingTaskStatus.Rejected)
+ {
throw Oops.Bah("当前任务不可编辑");
+ }
- var old = await _db.Queryable().FirstAsync(it => it.TaskId == task.Id);
+ var old = await _db.Queryable()
+ .FirstAsync(item => item.TaskId == task.Id);
var entity = input.Adapt();
entity.TaskId = task.Id;
@@ -106,56 +104,41 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
await _db.Updateable(entity).ExecuteCommandAsync();
}
- await _snapshotService.Build(task.Id, entity);
+ await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true);
}
///
- /// 校验任务归属后返回合同模板源文件 Id。
+ /// 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。
///
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)
+ .FirstAsync(item => item.Id == input.TaskContractId && item.TaskId == link.TaskId)
?? throw Oops.Bah("合同不存在");
- var version = await _db.Queryable().InSingleAsync(contract.TemplateVersionId)
- ?? throw Oops.Bah("合同模板版本不存在");
- return version.SourceDocxFileId;
- }
+ await _snapshotService.Build(link.TaskId);
+ var fileId = await _contractDocumentService.Generate(new OnboardingGenerateContractInput
+ {
+ TaskContractId = contract.Id
+ });
- 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();
+ contract.ReadAt ??= DateTime.Now;
+ if (contract.ContractStatus == TaskContractStatus.PendingGenerate)
+ contract.ContractStatus = TaskContractStatus.PendingConfirm;
+ await _db.Updateable(contract).ExecuteCommandAsync();
+ return fileId;
}
private async Task GetActiveLink(string token)
{
+ if (string.IsNullOrWhiteSpace(token))
+ throw Oops.Bah("入职链接 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)
+ var link = await _db.Queryable().FirstAsync(item => item.TokenHash == hash)
?? throw Oops.Bah("链接不存在");
if (link.LinkStatus != OnboardingLinkStatus.Active || link.ExpiresAt <= DateTime.Now)
@@ -163,4 +146,4 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
return link;
}
-}
+}
\ No newline at end of file
diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskInput.cs b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskInput.cs
index 36e9e65..2a86371 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskInput.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Task/Dto/OnboardingTaskInput.cs
@@ -16,6 +16,16 @@ public class OnboardingTaskCreateInput
///
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
public long PositionId { get; set; }
+ ///
+ /// 本次任务使用的入职模板 Id。
+ ///
+ [IdNotNull(ErrorMessage = "入职模板Id不能为空")]
+ public long OnboardingTemplateId { get; set; }
+
+ ///
+ /// 创建任务时由人事预先填写的合同变量。
+ ///
+ public List ContractVariables { get; set; } = new();
///
/// 创建任务时指定的审核人用户 Id。
@@ -33,6 +43,28 @@ public class OnboardingTaskCreateInput
public string Remark { get; set; }
}
+///
+/// 创建任务时填写的单个合同变量。
+///
+public class OnboardingInitialContractVariableInput
+{
+ ///
+ /// 变量所属合同模板版本 Id。
+ ///
+ [IdNotNull(ErrorMessage = "合同模板版本Id不能为空")]
+ public long ContractTemplateVersionId { get; set; }
+
+ ///
+ /// DOCX 中的完整变量编码,例如 hr.MonthlySalary。
+ ///
+ public string VariableCode { get; set; }
+
+ ///
+ /// 人事录入的变量值。
+ ///
+ public string VariableValue { get; set; }
+}
+
///
/// 入职任务创建结果。
///
diff --git a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs
index b6d77a7..369fe14 100644
--- a/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/Task/OnboardingTaskService.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Application;
///
-/// 实现入职任务创建业务能力。
+/// 实现基于入职模板创建任务及冻结配置快照的业务能力。
///
public class OnboardingTaskService : IOnboardingTaskService
{
@@ -19,42 +19,69 @@ public class OnboardingTaskService : IOnboardingTaskService
}
///
- /// 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
+ /// 校验公司、岗位和入职模板后创建任务,并冻结合同、补充字段及材料要求。
///
public async Task Create(OnboardingTaskCreateInput input)
{
var companyEnabled = await _db.Queryable()
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
if (!companyEnabled)
- {
throw Oops.Bah("公司不存在或未启用");
- }
var positionEnabled = await _db.Queryable()
.AnyAsync(item => item.Id == input.PositionId
&& item.CompanyId == input.CompanyId
&& item.Status == "enable");
if (!positionEnabled)
- {
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
- }
- var scopes = await _db.Queryable()
- .Where(item => item.CompanyId == input.CompanyId
- && (item.PositionId == null || item.PositionId == input.PositionId))
+ var onboardingTemplate = await _db.Queryable()
+ .FirstAsync(item => item.Id == input.OnboardingTemplateId
+ && item.CompanyId == input.CompanyId
+ && item.Status == "enable")
+ ?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司");
+ var templateSupportsPosition = await _db.Queryable()
+ .AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
+ && item.PositionId == input.PositionId);
+ if (!templateSupportsPosition)
+ throw Oops.Bah("所选入职模板不适用于该岗位");
+
+ var contractConfigs = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync();
+ if (contractConfigs.Count == 0)
+ throw Oops.Bah("入职模板尚未配置合同文件");
+
+ var fieldConfigs = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
+ .OrderBy(item => item.SortCode)
+ .ToListAsync();
+ var materialConfigs = await _db.Queryable()
+ .Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
+ .OrderBy(item => item.SortCode)
.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 templateIds = contractConfigs.Select(item => item.ContractTemplateId).Distinct().ToList();
+ var contractTemplates = await _db.Queryable()
+ .Where(item => templateIds.Contains(item.Id))
+ .ToListAsync();
+ var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList();
+ var enabledVersionCount = await _db.Queryable()
+ .CountAsync(item => versionIds.Contains(item.Id)
+ && item.VersionStatus == ContractTemplateVersionStatus.Enabled);
+ if (enabledVersionCount != versionIds.Count)
+ throw Oops.Bah("入职模板包含未启用的合同版本,请先调整模板配置");
+
+ var fieldIds = fieldConfigs.Select(item => item.FieldId).Distinct().ToList();
+ var fields = fieldIds.Count == 0
+ ? new List()
+ : await _db.Queryable().Where(item => fieldIds.Contains(item.Id)).ToListAsync();
+ var materialIds = materialConfigs.Select(item => item.MaterialTypeId).Distinct().ToList();
+ var materialTypes = materialIds.Count == 0
+ ? new List()
+ : await _db.Queryable().Where(item => materialIds.Contains(item.Id)).ToListAsync();
- var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var expiryConfig = await _configService.GetByConfigKey(
CateGoryConst.CONFIG_BIZ_DEFINE,
"onboard.link.expireHours");
@@ -65,6 +92,7 @@ public class OnboardingTaskService : IOnboardingTaskService
throw Oops.Bah("请在系统配置中设置 onboard.link.expireHours(正整数小时)");
}
+ var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var expiresAt = DateTime.Now.AddHours(expiryHours);
var token = Guid.NewGuid().ToString("N");
var task = new ObOnboardingTask
@@ -73,13 +101,13 @@ public class OnboardingTaskService : IOnboardingTaskService
TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}",
CompanyId = input.CompanyId,
PositionId = input.PositionId,
+ OnboardingTemplateId = onboardingTemplate.Id,
AssignedReviewerUserId = input.AssignedReviewerUserId,
LinkExpireAt = expiresAt,
TaskStatus = OnboardingTaskStatus.PendingFill,
Remark = input.Remark
};
- // 任务、链接、合同实例和变量快照属于同一创建动作,必须保证事务一致性。
await _db.Ado.BeginTranAsync();
try
{
@@ -93,27 +121,68 @@ public class OnboardingTaskService : IOnboardingTaskService
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 contracts = contractConfigs.Select(config =>
{
- var template = templates.First(item => item.Id == scope.TemplateId);
+ var template = contractTemplates.First(item => item.Id == config.ContractTemplateId);
return new ObTaskContract
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TaskId = taskId,
- TemplateId = scope.TemplateId,
- TemplateVersionId = scope.TemplateVersionId,
+ TemplateId = config.ContractTemplateId,
+ TemplateVersionId = config.ContractTemplateVersionId,
ContractName = template.TemplateName,
ContractType = template.ContractType,
- Required = scope.Required
+ Required = config.Required,
+ PrintCopies = config.PrintCopies
};
}).ToList();
-
await _db.Insertable(contracts).ExecuteCommandAsync();
+
+ if (fieldConfigs.Count > 0)
+ {
+ var fieldSnapshots = fieldConfigs.Select(config =>
+ {
+ var field = fields.First(item => item.Id == config.FieldId);
+ return new ObTaskFormRequirement
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ TaskId = taskId,
+ FieldId = field.Id,
+ FieldCode = field.FieldCode,
+ FieldName = field.FieldName,
+ DataType = field.DataType,
+ OptionsJson = field.OptionsJson,
+ Required = config.Required,
+ SortCode = config.SortCode
+ };
+ }).ToList();
+ await _db.Insertable(fieldSnapshots).ExecuteCommandAsync();
+ }
+
+ if (materialConfigs.Count > 0)
+ {
+ var materialSnapshots = materialConfigs.Select(config =>
+ {
+ var material = materialTypes.First(item => item.Id == config.MaterialTypeId);
+ return new ObTaskMaterialRequirement
+ {
+ Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
+ TaskId = taskId,
+ MaterialTypeId = material.Id,
+ MaterialCode = material.MaterialCode,
+ MaterialName = material.MaterialName,
+ AllowedExtensions = material.AllowedExtensions,
+ MaxFileCount = material.MaxFileCount,
+ MaxFileSizeMb = material.MaxFileSizeMb,
+ Required = config.Required,
+ SortCode = config.SortCode
+ };
+ }).ToList();
+ await _db.Insertable(materialSnapshots).ExecuteCommandAsync();
+ }
+
await _snapshotService.Build(taskId);
+ await ApplyInitialContractVariables(input.ContractVariables, contracts);
await _db.Ado.CommitTranAsync();
}
catch
@@ -130,4 +199,73 @@ public class OnboardingTaskService : IOnboardingTaskService
ExpiresAt = expiresAt
};
}
+
+ ///
+ /// 校验并保存任务创建时填写的人事合同变量。
+ ///
+ private async Task ApplyInitialContractVariables(
+ List inputs,
+ List contracts)
+ {
+ inputs ??= new List();
+ var versionIds = contracts.Select(item => item.TemplateVersionId).Distinct().ToList();
+ var definitions = await _db.Queryable()
+ .Where(item => versionIds.Contains(item.TemplateVersionId)
+ && item.HrEditable
+ && (item.VariableType == ContractVariableType.Contract
+ || item.VariableType == ContractVariableType.Company))
+ .ToListAsync();
+
+ var duplicateInput = inputs
+ .GroupBy(item => new { item.ContractTemplateVersionId, item.VariableCode })
+ .FirstOrDefault(group => group.Count() > 1);
+ if (duplicateInput != null)
+ throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
+
+ foreach (var input in inputs)
+ {
+ var definitionExists = definitions.Any(item =>
+ item.TemplateVersionId == input.ContractTemplateVersionId
+ && item.VariableCode == input.VariableCode);
+ if (!definitionExists)
+ throw Oops.Bah($"合同变量“{input.VariableCode}”不存在或不允许人事填写");
+ }
+
+ var missingRequired = definitions.FirstOrDefault(definition =>
+ {
+ if (!definition.Required)
+ return false;
+
+ var input = inputs.FirstOrDefault(item =>
+ item.ContractTemplateVersionId == definition.TemplateVersionId
+ && item.VariableCode == definition.VariableCode);
+ return string.IsNullOrWhiteSpace(input?.VariableValue ?? definition.DefaultValue);
+ });
+ if (missingRequired != null)
+ throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”");
+
+ if (inputs.Count == 0)
+ return;
+
+ var contractByVersion = contracts.ToDictionary(item => item.TemplateVersionId);
+ var contractIds = contracts.Select(item => item.Id).ToList();
+ var rows = await _db.Queryable()
+ .Where(item => contractIds.Contains(item.TaskContractId))
+ .ToListAsync();
+
+ foreach (var input in inputs)
+ {
+ var contract = contractByVersion[input.ContractTemplateVersionId];
+ var row = rows.FirstOrDefault(item =>
+ item.TaskContractId == contract.Id && item.VariableCode == input.VariableCode)
+ ?? throw Oops.Bah($"合同变量快照“{input.VariableCode}”不存在");
+ row.VariableValue = input.VariableValue;
+ row.SourceType = ContractVariableSourceType.HrInput;
+ row.IsManualOverride = true;
+ }
+
+ var changedRows = rows.Where(item => item.IsManualOverride).ToList();
+ if (changedRows.Count > 0)
+ await _db.Updateable(changedRows).ExecuteCommandAsync();
+ }
}
diff --git a/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs
index 3f7b58d..6bc5c09 100644
--- a/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs
+++ b/SimpleAdmin.Application/Services/Onboarding/TaskContractVariable/OnboardingTaskContractVariableService.cs
@@ -141,6 +141,15 @@ public class OnboardingTaskContractVariableService : IOnboardingTaskContractVari
{
await _db.Updateable(rows)
.ExecuteCommandAsync();
+
+ // 人事参数变化后,原合同文件和员工确认必须失效,避免员工确认旧内容。
+ contract.GeneratedDocxFileId = null;
+ contract.GeneratedPdfFileId = null;
+ contract.GeneratedAt = null;
+ contract.ReadAt = null;
+ contract.ConfirmedAt = null;
+ contract.ContractStatus = TaskContractStatus.PendingGenerate;
+ await _db.Updateable(contract).ExecuteCommandAsync();
}
}
}
diff --git a/SimpleAdmin.Application/SimpleAdmin.Application.xml b/SimpleAdmin.Application/SimpleAdmin.Application.xml
index b3aa902..925de83 100644
--- a/SimpleAdmin.Application/SimpleAdmin.Application.xml
+++ b/SimpleAdmin.Application/SimpleAdmin.Application.xml
@@ -504,6 +504,11 @@
入职任务所属岗位 Id。
+
+
+ 创建任务时锁定的入职模板 Id。
+
+
创建任务时指定的审核人用户 Id。
@@ -549,6 +554,131 @@
业务备注。
+
+
+ 入职模板,统一组织公司、岗位、合同、补充字段和上传材料。
+
+
+
+
+ 入职模板业务编码。
+
+
+
+
+ 入职模板显示名称。
+
+
+
+
+ 所属入职业务公司 Id。
+
+
+
+
+ 模板用途和适用范围说明。
+
+
+
+
+ 入职模板需要使用的合同文件及锁定版本。
+
+
+
+
+ 入职模板 Id。
+
+
+
+
+ 合同模板主档 Id。
+
+
+
+
+ 锁定的合同模板版本 Id。
+
+
+
+
+ 员工是否必须查看并确认该合同。
+
+
+
+
+ 合同打印份数。
+
+
+
+
+ 显示和生成顺序。
+
+
+
+
+ 入职模板需要员工填写的动态补充字段。
+
+
+
+
+ 入职模板 Id。
+
+
+
+
+ 动态补充字段 Id。
+
+
+
+
+ 员工是否必须填写。
+
+
+
+
+ 手机端显示顺序。
+
+
+
+
+ 入职模板需要员工上传的材料配置。
+
+
+
+
+ 入职模板 Id。
+
+
+
+
+ 上传材料类型 Id。
+
+
+
+
+ 员工是否必须上传。
+
+
+
+
+ 手机端显示顺序。
+
+
+
+
+ 入职模板适用岗位关系。
+
+
+
+
+ 入职模板 Id。
+
+
+
+
+ 适用岗位 Id。
+
+
入职业务岗位档案,保存岗位默认合同期限、试用期及业务参数。
@@ -664,6 +794,11 @@
是否为候选人必须阅读确认的合同。
+
+
+ 任务创建时冻结的合同打印份数。
+
+
任务合同当前状态。
@@ -889,6 +1024,51 @@
业务备注。
+
+
+ 入职任务创建时冻结的动态补充字段要求。
+
+
+
+
+ 入职任务 Id。
+
+
+
+
+ 原动态字段 Id。
+
+
+
+
+ 冻结的字段业务编码。
+
+
+
+
+ 冻结的字段显示名称。
+
+
+
+
+ 冻结的数据类型。
+
+
+
+
+ 冻结的选项 JSON。
+
+
+
+
+ 是否必须填写。
+
+
+
+
+ 手机端显示顺序。
+
+
候选人动态字段填写值,保留字段快照及提交轮次。
@@ -1009,6 +1189,56 @@
材料审核意见。
+
+
+ 入职任务创建时冻结的上传材料要求。
+
+
+
+
+ 入职任务 Id。
+
+
+
+
+ 原材料类型 Id。
+
+
+
+
+ 冻结的材料编码。
+
+
+
+
+ 冻结的材料名称。
+
+
+
+
+ 冻结的允许扩展名。
+
+
+
+
+ 冻结的最大文件数量。
+
+
+
+
+ 冻结的单文件大小上限,单位 MB。
+
+
+
+
+ 是否必须上传。
+
+
+
+
+ 手机端显示顺序。
+
+
入职任务操作日志,用于追踪关键状态变化及操作人。
@@ -1249,6 +1479,11 @@
合同变量类型。
+
+
+ 导入模板时尚未确认来源的变量。
+
+
值来源于候选人或正式员工信息。
@@ -1536,12 +1771,12 @@
- 实现入职合同文件生成业务能力。
+ 实现入职合同预览和归档 DOCX 文件生成能力。
- 根据已锁定的模板版本和变量快照生成任务合同 DOCX 文件。
+ 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
@@ -1549,86 +1784,35 @@
定义任务合同变量快照业务能力。
-
+
根据任务、公司与候选人资料刷新自动变量;人事已手工填写的变量不会被覆盖。
根据任务、公司和候选人资料构建或刷新合同变量快照。
- 冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。
- 实现任务合同变量快照业务能力。
+ 构建任务合同变量快照,合同生成阶段只读取本快照。
-
+
- 根据任务、公司和候选人资料构建或刷新合同变量快照。
+ 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。
-
+
- 合同适用范围分页查询参数。
+ 负责从 DOCX 中识别占位符并在不依赖 Word 进程的情况下替换文本节点。
-
+
- 入职业务公司 Id。
+ 扫描正文、表格、页眉和页脚,返回去重后的变量编码。
-
+
- 岗位 Id;为空时表示公司级配置。
-
-
-
-
- 合同模板 Id。
-
-
-
-
- 新增合同适用范围参数。
-
-
-
-
- 入职业务公司 Id。
-
-
-
-
- 岗位 Id;为空时表示公司级配置。
-
-
-
-
- 合同模板 Id。
-
-
-
-
- 合同模板版本 Id。
-
-
-
-
- 是否必填、必传或必签。
-
-
-
-
- 显示排序码。
-
-
-
-
- 编辑合同适用范围参数。
-
-
-
-
- 业务数据 Id。
+ 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。
@@ -1853,7 +2037,7 @@
- 定义合同模板、版本、变量和适用范围业务能力。
+ 定义合同模板、DOCX 版本和自动识别变量的管理能力。
@@ -1878,7 +2062,7 @@
- 删除未被版本、适用范围或任务合同引用的模板。
+ 删除未被版本、入职模板或任务合同引用的模板。
@@ -1888,7 +2072,7 @@
- 新增合同模板 DOCX 版本。
+ 新增合同模板 DOCX 版本并自动识别变量。
@@ -1898,37 +2082,12 @@
- 分页查询指定模板版本的变量定义。
-
-
-
-
- 为草稿模板版本新增变量。
+ 分页查询指定模板版本自动识别出的变量。
- 编辑草稿模板版本中的变量。
-
-
-
-
- 删除草稿模板版本中的变量。
-
-
-
-
- 新增公司或岗位的合同适用范围。
-
-
-
-
- 编辑合同适用范围。
-
-
-
-
- 删除合同适用范围。
+ 编辑并确认草稿模板版本中的变量定义。
@@ -1981,39 +2140,9 @@
分页查询指定模板版本的变量定义。
-
-
- 为草稿模板版本新增变量。
-
-
- 编辑草稿模板版本中的变量。
-
-
-
-
- 删除草稿模板版本中的变量。
-
-
-
-
- 分页查询合同适用范围。
-
-
-
-
- 新增公司或岗位的合同适用范围。
-
-
-
-
- 编辑合同适用范围。
-
-
-
-
- 删除合同适用范围。
+ 编辑草稿模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
@@ -2182,79 +2311,9 @@
启用状态或状态筛选条件。
-
-
- 保存公司岗位动态字段要求参数。
-
-
-
-
- 业务数据 Id。
-
-
-
-
- 入职业务公司 Id。
-
-
-
-
- 岗位 Id;为空时表示公司级配置。
-
-
-
-
- 动态字段 Id。
-
-
-
-
- 是否必填、必传或必签。
-
-
-
-
- 显示排序码。
-
-
-
-
- 保存公司岗位材料要求参数。
-
-
-
-
- 业务数据 Id。
-
-
-
-
- 入职业务公司 Id。
-
-
-
-
- 岗位 Id;为空时表示公司级配置。
-
-
-
-
- 材料类型 Id。
-
-
-
-
- 是否必填、必传或必签。
-
-
-
-
- 显示排序码。
-
-
- 定义动态字段、材料类型及公司岗位要求配置业务能力。
+ 定义员工动态补充字段与上传材料类型的基础配置能力。
@@ -2269,7 +2328,7 @@
- 删除未被填写要求引用的动态字段。
+ 删除未被入职模板引用的动态字段。
@@ -2284,42 +2343,12 @@
- 删除未被要求或上传记录引用的材料类型。
-
-
-
-
- 查询指定公司的动态字段填写要求。
-
-
-
-
- 新增或编辑公司岗位动态字段要求。
-
-
-
-
- 删除动态字段填写要求。
-
-
-
-
- 查询指定公司的材料上传要求。
-
-
-
-
- 新增或编辑公司岗位材料要求。
-
-
-
-
- 删除材料上传要求。
+ 删除未被入职模板或上传记录引用的材料类型。
- 维护员工端动态字段、材料类型及公司岗位要求。
+ 维护员工端动态补充字段和材料类型;与公司、岗位的组合关系由入职模板统一管理。
@@ -2357,36 +2386,6 @@
删除未被要求或上传记录引用的材料类型。
-
-
- 查询指定公司的动态字段填写要求。
-
-
-
-
- 新增或编辑公司岗位动态字段要求。
-
-
-
-
- 删除动态字段填写要求。
-
-
-
-
- 查询指定公司的材料上传要求。
-
-
-
-
- 新增或编辑公司岗位材料要求。
-
-
-
-
- 删除材料上传要求。
-
-
定义入职任务撤销和归档能力。
@@ -2457,6 +2456,261 @@
审核单份候选人上传材料并记录审核意见。
+
+
+ 入职模板分页查询参数。
+
+
+
+
+ 模板编码或名称关键词。
+
+
+
+
+ 所属公司 Id。
+
+
+
+
+ 适用岗位 Id。
+
+
+
+
+ 启用状态。
+
+
+
+
+ 保存入职模板参数。
+
+
+
+
+ 编辑时传入的入职模板 Id。
+
+
+
+
+ 入职模板业务编码。
+
+
+
+
+ 入职模板名称。
+
+
+
+
+ 所属公司 Id。
+
+
+
+
+ 适用岗位 Id 列表。
+
+
+
+
+ 合同配置列表。
+
+
+
+
+ 动态补充字段列表。
+
+
+
+
+ 上传材料列表。
+
+
+
+
+ 模板状态。
+
+
+
+
+ 模板说明。
+
+
+
+
+ 入职模板合同配置参数。
+
+
+
+
+ 合同模板 Id。
+
+
+
+
+ 合同模板版本 Id。
+
+
+
+
+ 是否必须查看确认。
+
+
+
+
+ 打印份数。
+
+
+
+
+ 排序码。
+
+
+
+
+ 入职模板补充字段参数。
+
+
+
+
+ 动态字段 Id。
+
+
+
+
+ 是否必填。
+
+
+
+
+ 排序码。
+
+
+
+
+ 入职模板上传材料参数。
+
+
+
+
+ 材料类型 Id。
+
+
+
+
+ 是否必传。
+
+
+
+
+ 排序码。
+
+
+
+
+ 入职模板列表输出。
+
+
+
+
+ 所属公司名称。
+
+
+
+
+ 适用岗位 Id 列表。
+
+
+
+
+ 适用岗位名称列表。
+
+
+
+
+ 合同配置数量。
+
+
+
+
+ 动态补充字段数量。
+
+
+
+
+ 上传材料数量。
+
+
+
+
+ 入职模板详情输出。
+
+
+
+
+ 合同配置明细。
+
+
+
+
+ 动态补充字段配置明细。
+
+
+
+
+ 上传材料配置明细。
+
+
+
+
+ 入职模板聚合配置服务。
+
+
+
+
+ 分页查询入职模板。
+
+
+
+
+ 查询入职模板详情。
+
+
+
+
+ 新增或编辑入职模板及全部关联配置。
+
+
+
+
+ 删除未被任务使用的入职模板。
+
+
+
+
+ 查询公司岗位可使用的入职模板。
+
+
+
+
+ 实现入职模板聚合配置及公司岗位匹配能力。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
入职业务岗位分页查询参数。
@@ -2709,32 +2963,32 @@
- 实现员工端入职资料提交业务能力。
+ 实现员工端动态资料、上传材料、合同确认和最终提交能力。
- 保存候选人填写的动态字段值。
+ 保存候选人填写的动态补充字段值。
- 关联候选人已经上传的系统文件与材料类型。
+ 关联已上传 OSS 文件与任务材料要求。
- 记录候选人已阅读并确认指定任务合同。
+ 记录候选人已经查看并确认动态生成的合同。
- 校验全部必填资料后提交入职任务进入审核。
+ 校验固定信息、动态字段、材料、合同变量和合同确认后提交审核。
- 在保存文件前校验材料适用范围、扩展名、大小和数量。
+ 上传 OSS 前按照任务材料快照校验扩展名、文件大小和数量。
@@ -2759,22 +3013,22 @@
- 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。
+ 员工手机端公开入口,仅凭一次性任务链接访问入职资料。
- 通过有效 Token 获取员工端任务、字段、材料和合同信息。
+ 通过有效 Token 获取任务创建时冻结的字段、材料和合同信息。
- 保存候选人固定基础信息并刷新合同变量快照。
+ 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。
- 校验任务归属后返回合同模板源文件 Id。
+ 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。
@@ -3027,6 +3281,16 @@
入职任务所属岗位 Id。
+
+
+ 本次任务使用的入职模板 Id。
+
+
+
+
+ 创建任务时由人事预先填写的合同变量。
+
+
创建任务时指定的审核人用户 Id。
@@ -3042,6 +3306,26 @@
业务备注。
+
+
+ 创建任务时填写的单个合同变量。
+
+
+
+
+ 变量所属合同模板版本 Id。
+
+
+
+
+ DOCX 中的完整变量编码,例如 hr.MonthlySalary。
+
+
+
+
+ 人事录入的变量值。
+
+
入职任务创建结果。
@@ -3234,12 +3518,17 @@
- 实现入职任务创建业务能力。
+ 实现基于入职模板创建任务及冻结配置快照的业务能力。
- 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
+ 校验公司、岗位和入职模板后创建任务,并冻结合同、补充字段及材料要求。
+
+
+
+
+ 校验并保存任务创建时填写的人事合同变量。
diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs
index ed86c31..0080abc 100644
--- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs
+++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Web.Core;
///
-/// 提供合同模板、版本、变量和适用范围相关 HTTP 接口。
+/// 提供合同模板、DOCX 版本和自动识别变量配置接口。
///
[ApiDescriptionSettings("Application", Tag = "入职合同模板")]
[Route("biz/onboarding/contract-template")]
@@ -118,15 +118,6 @@ public class OnboardingContractTemplateController
return await _service.VariablePage(input);
}
- ///
- /// 新增模板变量。
- ///
- [HttpPost("variable/add")]
- public async Task AddVariable([FromBody] OnboardingTemplateVariableAddInput input)
- {
- await _service.AddVariable(input);
- }
-
///
/// 编辑模板变量。
///
@@ -135,49 +126,4 @@ public class OnboardingContractTemplateController
{
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/OnboardingEntryConfigController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs
index 2a71aee..d1ae1c0 100644
--- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs
+++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingEntryConfigController.cs
@@ -1,7 +1,7 @@
namespace SimpleAdmin.Web.Core;
///
-/// 提供入职字段与材料要求配置相关 HTTP 接口。
+/// 提供员工动态补充字段与上传材料类型配置接口。
///
[ApiDescriptionSettings("Application", Tag = "入职填写与材料配置")]
[Route("biz/onboarding/entry-config")]
@@ -68,58 +68,4 @@ public class OnboardingEntryConfigController
{
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/OnboardingPublicTaskController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs
index 0977df3..7692220 100644
--- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs
+++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingPublicTaskController.cs
@@ -36,7 +36,7 @@ public class OnboardingPublicTaskController
}
///
- /// 下载当前任务锁定的合同模板源文件。
+ /// 下载根据当前任务资料动态生成并已替换变量的合同文件。
///
[HttpGet("contract-source")]
public async Task ContractSource([FromQuery] OnboardingPublicContractSourceInput input)
diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTemplateController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTemplateController.cs
new file mode 100644
index 0000000..0e8f0ef
--- /dev/null
+++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingTemplateController.cs
@@ -0,0 +1,62 @@
+namespace SimpleAdmin.Web.Core;
+
+///
+/// 提供入职模板聚合配置接口。
+///
+[ApiDescriptionSettings("Application", Tag = "入职模板")]
+[Route("biz/onboarding/template")]
+[RolePermission]
+public class OnboardingTemplateController
+{
+ private readonly IOnboardingTemplateService _service;
+
+ public OnboardingTemplateController(IOnboardingTemplateService service)
+ {
+ _service = service;
+ }
+
+ ///
+ /// 分页查询入职模板。
+ ///
+ [HttpGet("page")]
+ public async Task Page([FromQuery] OnboardingTemplatePageInput input)
+ {
+ return await _service.Page(input);
+ }
+
+ ///
+ /// 查询入职模板详情及全部关联配置。
+ ///
+ [HttpGet("detail")]
+ public async Task Detail([FromQuery] BaseIdInput input)
+ {
+ return await _service.Detail(input);
+ }
+
+ ///
+ /// 新增或编辑入职模板。
+ ///
+ [HttpPost("save")]
+ public async Task Save([FromBody] OnboardingTemplateSaveInput input)
+ {
+ return await _service.Save(input);
+ }
+
+ ///
+ /// 删除未被任务使用的入职模板。
+ ///
+ [HttpPost("delete")]
+ public async Task Delete([FromBody] BaseIdListInput input)
+ {
+ await _service.Delete(input);
+ }
+
+ ///
+ /// 查询指定公司岗位可使用的入职模板。
+ ///
+ [HttpGet("list-enabled")]
+ public async Task ListEnabled([FromQuery] long companyId, [FromQuery] long positionId)
+ {
+ return await _service.ListEnabled(companyId, positionId);
+ }
+}
\ 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 5e260ac..7385258 100644
--- a/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml
+++ b/SimpleAdmin.Web.Core/SimpleAdmin.Web.Core.xml
@@ -88,7 +88,7 @@
- 提供合同模板、版本、变量和适用范围相关 HTTP 接口。
+ 提供合同模板、DOCX 版本和自动识别变量配置接口。
@@ -141,41 +141,11 @@
分页查询模板变量。
-
-
- 新增模板变量。
-
-
编辑模板变量。
-
-
- 删除模板变量。
-
-
-
-
- 分页查询合同适用范围。
-
-
-
-
- 新增合同适用范围。
-
-
-
-
- 编辑合同适用范围。
-
-
-
-
- 删除合同适用范围。
-
-
提供正式员工档案相关 HTTP 接口。
@@ -193,7 +163,7 @@
- 提供入职字段与材料要求配置相关 HTTP 接口。
+ 提供员工动态补充字段与上传材料类型配置接口。
@@ -226,36 +196,6 @@
删除入职材料类型。
-
-
- 查询公司岗位动态字段要求。
-
-
-
-
- 保存公司岗位动态字段要求。
-
-
-
-
- 删除动态字段要求。
-
-
-
-
- 查询公司岗位材料要求。
-
-
-
-
- 保存公司岗位材料要求。
-
-
-
-
- 删除材料要求。
-
-
提供候选人材料审核相关 HTTP 接口。
@@ -348,7 +288,7 @@
- 下载当前任务锁定的合同模板源文件。
+ 下载根据当前任务资料动态生成并已替换变量的合同文件。
@@ -406,6 +346,36 @@
查询入职任务完整详情。
+
+
+ 提供入职模板聚合配置接口。
+
+
+
+
+ 分页查询入职模板。
+
+
+
+
+ 查询入职模板详情及全部关联配置。
+
+
+
+
+ 新增或编辑入职模板。
+
+
+
+
+ 删除未被任务使用的入职模板。
+
+
+
+
+ 查询指定公司岗位可使用的入职模板。
+
+
机构管理控制器
diff --git a/database/migrations/20260824_onboarding_template_refactor.sql b/database/migrations/20260824_onboarding_template_refactor.sql
new file mode 100644
index 0000000..cd21123
--- /dev/null
+++ b/database/migrations/20260824_onboarding_template_refactor.sql
@@ -0,0 +1,364 @@
+-- OnboardX 入职模板聚合重构
+-- 适用数据库:MySQL 8.x
+-- 执行前请备份 OnboardX 数据库。
+-- 新逻辑不再读取 ob_form_requirement、ob_material_requirement、ob_contract_scope,
+-- 三张旧表暂时保留,仅用于迁移和回滚;对应写接口已从业务代码中移除。
+-- 执行本脚本后请重启后端服务。
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+CREATE TABLE IF NOT EXISTS `ob_onboarding_template` (
+ `Id` BIGINT NOT NULL COMMENT '主键Id',
+ `TemplateCode` VARCHAR(100) NOT NULL COMMENT '入职模板编码',
+ `TemplateName` VARCHAR(200) NOT NULL COMMENT '入职模板名称',
+ `CompanyId` BIGINT NOT NULL COMMENT '所属公司Id',
+ `Description` VARCHAR(1000) NULL COMMENT '模板说明',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable' COMMENT '状态',
+ `CreateTime` DATETIME NULL COMMENT '创建时间',
+ `UpdateTime` DATETIME NULL COMMENT '更新时间',
+ `CreateUserId` BIGINT NULL COMMENT '创建者Id',
+ `UpdateUserId` BIGINT NULL COMMENT '修改者Id',
+ `CreateUser` VARCHAR(100) NULL COMMENT '创建人',
+ `UpdateUser` VARCHAR(100) NULL COMMENT '更新人',
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除',
+ `ExtJson` LONGTEXT NULL COMMENT '扩展信息',
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_onboarding_template_code` (`TemplateCode`),
+ KEY `idx_ob_onboarding_template_company` (`CompanyId`, `Status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板';
+
+CREATE TABLE IF NOT EXISTS `ob_onboarding_template_position` (
+ `Id` BIGINT NOT NULL,
+ `OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
+ `PositionId` BIGINT NOT NULL COMMENT '岗位Id',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_template_position` (`OnboardingTemplateId`, `PositionId`),
+ KEY `idx_ob_template_position_position` (`PositionId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板适用岗位';
+
+CREATE TABLE IF NOT EXISTS `ob_onboarding_template_contract` (
+ `Id` BIGINT NOT NULL,
+ `OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
+ `ContractTemplateId` BIGINT NOT NULL COMMENT '合同模板Id',
+ `ContractTemplateVersionId` BIGINT NOT NULL COMMENT '合同模板版本Id',
+ `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必须确认',
+ `PrintCopies` INT NOT NULL DEFAULT 1 COMMENT '打印份数',
+ `SortCode` INT NULL COMMENT '排序码',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_template_contract_version` (`OnboardingTemplateId`, `ContractTemplateVersionId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板合同配置';
+
+CREATE TABLE IF NOT EXISTS `ob_onboarding_template_form_field` (
+ `Id` BIGINT NOT NULL,
+ `OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
+ `FieldId` BIGINT NOT NULL COMMENT '动态字段Id',
+ `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必填',
+ `SortCode` INT NULL COMMENT '排序码',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_template_form_field` (`OnboardingTemplateId`, `FieldId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板补充字段';
+
+CREATE TABLE IF NOT EXISTS `ob_onboarding_template_material` (
+ `Id` BIGINT NOT NULL,
+ `OnboardingTemplateId` BIGINT NOT NULL COMMENT '入职模板Id',
+ `MaterialTypeId` BIGINT NOT NULL COMMENT '材料类型Id',
+ `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必传',
+ `SortCode` INT NULL COMMENT '排序码',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_template_material` (`OnboardingTemplateId`, `MaterialTypeId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入职模板上传材料';
+
+CREATE TABLE IF NOT EXISTS `ob_task_form_requirement` (
+ `Id` BIGINT NOT NULL,
+ `TaskId` BIGINT NOT NULL COMMENT '入职任务Id',
+ `FieldId` BIGINT NOT NULL COMMENT '原动态字段Id',
+ `FieldCode` VARCHAR(100) NOT NULL COMMENT '冻结字段编码',
+ `FieldName` VARCHAR(200) NOT NULL COMMENT '冻结字段名称',
+ `DataType` VARCHAR(50) NOT NULL COMMENT '冻结数据类型',
+ `OptionsJson` JSON NULL COMMENT '冻结选项JSON',
+ `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必填',
+ `SortCode` INT NULL COMMENT '排序码',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_task_form_requirement` (`TaskId`, `FieldId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务补充字段快照';
+
+CREATE TABLE IF NOT EXISTS `ob_task_material_requirement` (
+ `Id` BIGINT NOT NULL,
+ `TaskId` BIGINT NOT NULL COMMENT '入职任务Id',
+ `MaterialTypeId` BIGINT NOT NULL COMMENT '原材料类型Id',
+ `MaterialCode` VARCHAR(100) NOT NULL COMMENT '冻结材料编码',
+ `MaterialName` VARCHAR(200) NOT NULL COMMENT '冻结材料名称',
+ `AllowedExtensions` VARCHAR(500) NULL COMMENT '冻结允许扩展名',
+ `MaxFileCount` INT NOT NULL DEFAULT 1 COMMENT '冻结最大文件数',
+ `MaxFileSizeMb` INT NULL COMMENT '冻结单文件大小MB',
+ `Required` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否必传',
+ `SortCode` INT NULL COMMENT '排序码',
+ `Status` VARCHAR(20) NULL DEFAULT 'enable',
+ `CreateTime` DATETIME NULL,
+ `UpdateTime` DATETIME NULL,
+ `CreateUserId` BIGINT NULL,
+ `UpdateUserId` BIGINT NULL,
+ `CreateUser` VARCHAR(100) NULL,
+ `UpdateUser` VARCHAR(100) NULL,
+ `IsDelete` TINYINT(1) NOT NULL DEFAULT 0,
+ `ExtJson` LONGTEXT NULL,
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `uk_ob_task_material_requirement` (`TaskId`, `MaterialTypeId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务上传材料快照';
+
+DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
+DELIMITER $$
+CREATE PROCEDURE `ob_add_column_if_missing`(
+ IN p_table_name VARCHAR(128),
+ IN p_column_name VARCHAR(128),
+ IN p_column_definition VARCHAR(1000)
+)
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = p_table_name
+ AND COLUMN_NAME = p_column_name
+ ) THEN
+ SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `',
+ p_column_name, '` ', p_column_definition);
+ PREPARE stmt FROM @ddl;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+ END IF;
+END$$
+DELIMITER ;
+
+CALL `ob_add_column_if_missing`(
+ 'ob_onboarding_task',
+ 'OnboardingTemplateId',
+ 'BIGINT NOT NULL DEFAULT 0 COMMENT ''创建任务时锁定的入职模板Id'' AFTER `PositionId`'
+);
+CALL `ob_add_column_if_missing`(
+ 'ob_task_contract',
+ 'PrintCopies',
+ 'INT NOT NULL DEFAULT 1 COMMENT ''合同打印份数'' AFTER `Required`'
+);
+DROP PROCEDURE IF EXISTS `ob_add_column_if_missing`;
+
+-- 将旧的公司/岗位合同范围迁移为“一公司一岗位一入职模板”。
+-- 公司级合同、字段和材料要求会自动继承到该公司的每个岗位模板。
+DROP TEMPORARY TABLE IF EXISTS `tmp_ob_template_map`;
+CREATE TEMPORARY TABLE `tmp_ob_template_map` (
+ `CompanyId` BIGINT NOT NULL,
+ `PositionId` BIGINT NOT NULL,
+ `TemplateCode` VARCHAR(100) NOT NULL,
+ `OnboardingTemplateId` BIGINT NULL,
+ PRIMARY KEY (`CompanyId`, `PositionId`)
+);
+
+INSERT IGNORE INTO `tmp_ob_template_map` (`CompanyId`, `PositionId`, `TemplateCode`)
+SELECT DISTINCT p.`CompanyId`, p.`Id`, CONCAT('MIGRATED_', p.`CompanyId`, '_', p.`Id`)
+FROM `ob_position` p
+INNER JOIN (
+ SELECT `CompanyId` FROM `ob_contract_scope` WHERE `IsDelete` = 0
+ UNION
+ SELECT `CompanyId` FROM `ob_form_requirement` WHERE `IsDelete` = 0
+ UNION
+ SELECT `CompanyId` FROM `ob_material_requirement` WHERE `IsDelete` = 0
+) configured_company ON configured_company.`CompanyId` = p.`CompanyId`
+WHERE p.`IsDelete` = 0;
+
+INSERT IGNORE INTO `ob_onboarding_template` (
+ `Id`, `TemplateCode`, `TemplateName`, `CompanyId`, `Description`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(),
+ m.`TemplateCode`,
+ CONCAT(COALESCE(c.`CompanyName`, m.`CompanyId`), '-', COALESCE(p.`PositionName`, m.`PositionId`), '-入职模板'),
+ m.`CompanyId`,
+ '由旧公司岗位配置自动迁移,请管理员检查后使用',
+ 'enable',
+ NOW(),
+ 0
+FROM `tmp_ob_template_map` m
+LEFT JOIN `ob_company` c ON c.`Id` = m.`CompanyId`
+LEFT JOIN `ob_position` p ON p.`Id` = m.`PositionId`;
+
+UPDATE `tmp_ob_template_map` m
+INNER JOIN `ob_onboarding_template` t ON t.`TemplateCode` = m.`TemplateCode`
+SET m.`OnboardingTemplateId` = t.`Id`;
+
+INSERT IGNORE INTO `ob_onboarding_template_position` (
+ `Id`, `OnboardingTemplateId`, `PositionId`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT UUID_SHORT(), m.`OnboardingTemplateId`, m.`PositionId`, 'enable', NOW(), 0
+FROM `tmp_ob_template_map` m;
+
+INSERT IGNORE INTO `ob_onboarding_template_contract` (
+ `Id`, `OnboardingTemplateId`, `ContractTemplateId`, `ContractTemplateVersionId`,
+ `Required`, `PrintCopies`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(),
+ m.`OnboardingTemplateId`,
+ MAX(s.`TemplateId`),
+ s.`TemplateVersionId`,
+ COALESCE(
+ MAX(CASE WHEN s.`PositionId` = m.`PositionId` THEN s.`Required` END),
+ MAX(s.`Required`)
+ ),
+ 1,
+ COALESCE(
+ MAX(CASE WHEN s.`PositionId` = m.`PositionId` THEN s.`SortCode` END),
+ MAX(s.`SortCode`)
+ ),
+ 'enable',
+ NOW(),
+ 0
+FROM `tmp_ob_template_map` m
+INNER JOIN `ob_contract_scope` s
+ ON s.`CompanyId` = m.`CompanyId`
+ AND (s.`PositionId` IS NULL OR s.`PositionId` = m.`PositionId`)
+ AND s.`IsDelete` = 0
+GROUP BY m.`OnboardingTemplateId`, s.`TemplateVersionId`;
+
+INSERT IGNORE INTO `ob_onboarding_template_form_field` (
+ `Id`, `OnboardingTemplateId`, `FieldId`, `Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(),
+ m.`OnboardingTemplateId`,
+ r.`FieldId`,
+ COALESCE(
+ MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`Required` END),
+ MAX(r.`Required`)
+ ),
+ COALESCE(
+ MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`SortCode` END),
+ MAX(r.`SortCode`)
+ ),
+ 'enable',
+ NOW(),
+ 0
+FROM `tmp_ob_template_map` m
+INNER JOIN `ob_form_requirement` r
+ ON r.`CompanyId` = m.`CompanyId`
+ AND (r.`PositionId` IS NULL OR r.`PositionId` = m.`PositionId`)
+ AND r.`IsDelete` = 0
+GROUP BY m.`OnboardingTemplateId`, r.`FieldId`;
+
+INSERT IGNORE INTO `ob_onboarding_template_material` (
+ `Id`, `OnboardingTemplateId`, `MaterialTypeId`, `Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(),
+ m.`OnboardingTemplateId`,
+ r.`MaterialTypeId`,
+ COALESCE(
+ MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`Required` END),
+ MAX(r.`Required`)
+ ),
+ COALESCE(
+ MAX(CASE WHEN r.`PositionId` = m.`PositionId` THEN r.`SortCode` END),
+ MAX(r.`SortCode`)
+ ),
+ 'enable',
+ NOW(),
+ 0
+FROM `tmp_ob_template_map` m
+INNER JOIN `ob_material_requirement` r
+ ON r.`CompanyId` = m.`CompanyId`
+ AND (r.`PositionId` IS NULL OR r.`PositionId` = m.`PositionId`)
+ AND r.`IsDelete` = 0
+GROUP BY m.`OnboardingTemplateId`, r.`MaterialTypeId`;
+
+-- 为迁移前已创建的入职任务回填入职模板,并冻结字段/材料要求快照。
+UPDATE `ob_onboarding_task` task
+INNER JOIN `tmp_ob_template_map` m
+ ON m.`CompanyId` = task.`CompanyId`
+ AND m.`PositionId` = task.`PositionId`
+SET task.`OnboardingTemplateId` = m.`OnboardingTemplateId`
+WHERE task.`OnboardingTemplateId` = 0;
+
+INSERT IGNORE INTO `ob_task_form_requirement` (
+ `Id`, `TaskId`, `FieldId`, `FieldCode`, `FieldName`, `DataType`, `OptionsJson`,
+ `Required`, `SortCode`, `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(), task.`Id`, config.`FieldId`, field.`FieldCode`, field.`FieldName`,
+ field.`DataType`, field.`OptionsJson`, config.`Required`, config.`SortCode`,
+ 'enable', NOW(), 0
+FROM `ob_onboarding_task` task
+INNER JOIN `ob_onboarding_template_form_field` config
+ ON config.`OnboardingTemplateId` = task.`OnboardingTemplateId`
+INNER JOIN `ob_form_field` field ON field.`Id` = config.`FieldId` AND field.`IsDelete` = 0
+WHERE task.`OnboardingTemplateId` <> 0;
+
+INSERT IGNORE INTO `ob_task_material_requirement` (
+ `Id`, `TaskId`, `MaterialTypeId`, `MaterialCode`, `MaterialName`,
+ `AllowedExtensions`, `MaxFileCount`, `MaxFileSizeMb`, `Required`, `SortCode`,
+ `Status`, `CreateTime`, `IsDelete`
+)
+SELECT
+ UUID_SHORT(), task.`Id`, config.`MaterialTypeId`, material.`MaterialCode`,
+ material.`MaterialName`, material.`AllowedExtensions`, material.`MaxFileCount`,
+ material.`MaxFileSizeMb`, config.`Required`, config.`SortCode`,
+ 'enable', NOW(), 0
+FROM `ob_onboarding_task` task
+INNER JOIN `ob_onboarding_template_material` config
+ ON config.`OnboardingTemplateId` = task.`OnboardingTemplateId`
+INNER JOIN `ob_material_type` material ON material.`Id` = config.`MaterialTypeId` AND material.`IsDelete` = 0
+WHERE task.`OnboardingTemplateId` <> 0;
+
+DROP TEMPORARY TABLE IF EXISTS `tmp_ob_template_map`;
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- 执行后检查:
+-- SELECT * FROM ob_onboarding_template;
+-- SELECT * FROM ob_onboarding_template_position;
+-- SELECT * FROM ob_onboarding_template_contract;
+-- SELECT * FROM ob_onboarding_template_form_field;
+-- SELECT * FROM ob_onboarding_template_material;