逻辑调整
This commit is contained in:
@@ -37,6 +37,11 @@ public class ObOnboardingTask : BaseEntity
|
||||
/// </summary>
|
||||
public long PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时锁定的入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时指定的审核人用户 Id。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板,统一组织公司、岗位、合同、补充字段和上传材料。
|
||||
/// </summary>
|
||||
[SugarTable("ob_onboarding_template", TableDescription = "入职模板")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObOnboardingTemplate : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职模板业务编码。
|
||||
/// </summary>
|
||||
public string TemplateCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板显示名称。
|
||||
/// </summary>
|
||||
public string TemplateName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属入职业务公司 Id。
|
||||
/// </summary>
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模板用途和适用范围说明。
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板需要使用的合同文件及锁定版本。
|
||||
/// </summary>
|
||||
[SugarTable("ob_onboarding_template_contract", TableDescription = "入职模板合同配置")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObOnboardingTemplateContract : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板主档 Id。
|
||||
/// </summary>
|
||||
public long ContractTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 锁定的合同模板版本 Id。
|
||||
/// </summary>
|
||||
public long ContractTemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工是否必须查看并确认该合同。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同打印份数。
|
||||
/// </summary>
|
||||
public int PrintCopies { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 显示和生成顺序。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板需要员工填写的动态补充字段。
|
||||
/// </summary>
|
||||
[SugarTable("ob_onboarding_template_form_field", TableDescription = "入职模板补充字段")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObOnboardingTemplateFormField : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动态补充字段 Id。
|
||||
/// </summary>
|
||||
public long FieldId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工是否必须填写。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机端显示顺序。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板需要员工上传的材料配置。
|
||||
/// </summary>
|
||||
[SugarTable("ob_onboarding_template_material", TableDescription = "入职模板上传材料")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObOnboardingTemplateMaterial : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上传材料类型 Id。
|
||||
/// </summary>
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 员工是否必须上传。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机端显示顺序。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板适用岗位关系。
|
||||
/// </summary>
|
||||
[SugarTable("ob_onboarding_template_position", TableDescription = "入职模板适用岗位")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObOnboardingTemplatePosition : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职模板 Id。
|
||||
/// </summary>
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用岗位 Id。
|
||||
/// </summary>
|
||||
public long PositionId { get; set; }
|
||||
}
|
||||
@@ -47,6 +47,11 @@ public class ObTaskContract : BaseEntity
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务创建时冻结的合同打印份数。
|
||||
/// </summary>
|
||||
public int PrintCopies { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 任务合同当前状态。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职任务创建时冻结的动态补充字段要求。
|
||||
/// </summary>
|
||||
[SugarTable("ob_task_form_requirement", TableDescription = "任务补充字段快照")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObTaskFormRequirement : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职任务 Id。
|
||||
/// </summary>
|
||||
public long TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原动态字段 Id。
|
||||
/// </summary>
|
||||
public long FieldId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的字段业务编码。
|
||||
/// </summary>
|
||||
public string FieldCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的字段显示名称。
|
||||
/// </summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的数据类型。
|
||||
/// </summary>
|
||||
public string DataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的选项 JSON。
|
||||
/// </summary>
|
||||
public string OptionsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必须填写。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机端显示顺序。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职任务创建时冻结的上传材料要求。
|
||||
/// </summary>
|
||||
[SugarTable("ob_task_material_requirement", TableDescription = "任务上传材料快照")]
|
||||
[Tenant(SqlSugarConst.DB_DEFAULT)]
|
||||
public class ObTaskMaterialRequirement : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职任务 Id。
|
||||
/// </summary>
|
||||
public long TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原材料类型 Id。
|
||||
/// </summary>
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的材料编码。
|
||||
/// </summary>
|
||||
public string MaterialCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的材料名称。
|
||||
/// </summary>
|
||||
public string MaterialName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的允许扩展名。
|
||||
/// </summary>
|
||||
public string AllowedExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的最大文件数量。
|
||||
/// </summary>
|
||||
public int MaxFileCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 冻结的单文件大小上限,单位 MB。
|
||||
/// </summary>
|
||||
public int? MaxFileSizeMb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必须上传。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机端显示顺序。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
@@ -139,6 +139,10 @@ public static class TaskContractStatus
|
||||
/// </summary>
|
||||
public static class ContractVariableType
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入模板时尚未确认来源的变量。
|
||||
/// </summary>
|
||||
public const string Unresolved = "unresolved";
|
||||
/// <summary>
|
||||
/// 值来源于候选人或正式员工信息。
|
||||
/// </summary>
|
||||
|
||||
@@ -28,12 +28,12 @@ public class OnboardingAuditService : IOnboardingAuditService
|
||||
.FirstAsync(item => item.TaskId == task.Id)
|
||||
?? throw Oops.Bah("候选人资料不存在");
|
||||
|
||||
var hasMaterialNeedSupplement = await _db.Queryable<ObTaskMaterial>()
|
||||
var hasUnapprovedMaterial = await _db.Queryable<ObTaskMaterial>()
|
||||
.AnyAsync(item => item.TaskId == task.Id
|
||||
&& item.ReviewStatus == TaskMaterialReviewStatus.NeedSupplement);
|
||||
if (hasMaterialNeedSupplement)
|
||||
&& item.ReviewStatus != TaskMaterialReviewStatus.Passed);
|
||||
if (hasUnapprovedMaterial)
|
||||
{
|
||||
throw Oops.Bah("仍有资料需要补充,不能审核通过");
|
||||
throw Oops.Bah("仍有资料未审核通过,不能审核通过入职任务");
|
||||
}
|
||||
|
||||
// 审核通过是创建正式员工的唯一入口,因此必须在入库前拦截身份证号或手机号重复。
|
||||
|
||||
+29
-59
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职合同文件生成业务能力。
|
||||
/// 实现入职合同预览和归档 DOCX 文件生成能力。
|
||||
/// </summary>
|
||||
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
|
||||
{
|
||||
@@ -15,7 +15,7 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据已锁定的模板版本和变量快照生成任务合同 DOCX 文件。
|
||||
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
||||
/// </summary>
|
||||
public async Task<long> Generate(OnboardingGenerateContractInput input)
|
||||
{
|
||||
@@ -23,10 +23,12 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
||||
?? throw Oops.Bah("任务合同不存在");
|
||||
var task = await _db.Queryable<ObOnboardingTask>().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<SysFile>().InSingleAsync(version.SourceDocxFileId)
|
||||
?? throw Oops.Bah("合同源文件不存在");
|
||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||
.Where(x => x.TaskContractId == contract.Id)
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => item.TemplateVersionId == version.Id)
|
||||
.ToListAsync();
|
||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||
.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<string, string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,5 +9,5 @@ public interface IOnboardingContractSnapshotService : ITransient
|
||||
/// <summary>
|
||||
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
|
||||
/// </summary>
|
||||
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null);
|
||||
Task Build(long taskId, ObTaskEmployeeInfo employeeInfo = null, bool invalidateGeneratedPreview = false);
|
||||
}
|
||||
|
||||
+147
-44
@@ -1,32 +1,53 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>冻结任务合同变量;DOCX 生成阶段只读取本快照,不再读取模板实时值。</summary>
|
||||
/// <summary>
|
||||
/// 实现任务合同变量快照业务能力。
|
||||
/// 构建任务合同变量快照,合同生成阶段只读取本快照。
|
||||
/// </summary>
|
||||
public class OnboardingContractSnapshotService : IOnboardingContractSnapshotService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
public OnboardingContractSnapshotService() => _db = DbContext.DB;
|
||||
|
||||
public OnboardingContractSnapshotService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据任务、公司和候选人资料构建或刷新合同变量快照。
|
||||
/// 根据任务、公司、员工固定信息和动态补充字段构建合同变量快照。
|
||||
/// </summary>
|
||||
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<ObOnboardingTask>().InSingleAsync(taskId) ?? throw Oops.Bah("入职任务不存在");
|
||||
var company = await _db.Queryable<ObCompany>().InSingleAsync(task.CompanyId) ?? throw Oops.Bah("入职公司不存在");
|
||||
employeeInfo ??= await _db.Queryable<ObTaskEmployeeInfo>().FirstAsync(x => x.TaskId == taskId);
|
||||
var contracts = await _db.Queryable<ObTaskContract>().Where(x => x.TaskId == taskId).ToListAsync();
|
||||
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(taskId)
|
||||
?? throw Oops.Bah("入职任务不存在");
|
||||
var company = await _db.Queryable<ObCompany>().InSingleAsync(task.CompanyId)
|
||||
?? throw Oops.Bah("入职公司不存在");
|
||||
employeeInfo ??= await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == taskId);
|
||||
var dynamicValues = await _db.Queryable<ObTaskFormValue>()
|
||||
.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<ObTaskContract>()
|
||||
.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<ObTemplateVariable>().Where(x => x.TemplateVersionId == contract.TemplateVersionId).OrderBy(x => x.SortCode).ToListAsync();
|
||||
var oldRows = await _db.Queryable<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ToListAsync();
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.Where(item => item.TemplateVersionId == contract.TemplateVersionId)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
var oldRows = await _db.Queryable<ObTaskContractVariable>()
|
||||
.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<ObTaskContractVariable>().Where(x => x.TaskContractId == contract.Id).ExecuteCommandAsync();
|
||||
if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync();
|
||||
|
||||
await _db.Deleteable<ObTaskContractVariable>()
|
||||
.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<string, string> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 负责从 DOCX 中识别占位符并在不依赖 Word 进程的情况下替换文本节点。
|
||||
/// </summary>
|
||||
public static class DocxTemplateProcessor
|
||||
{
|
||||
private static readonly global::System.Text.RegularExpressions.Regex PlaceholderRegex = new(
|
||||
@"\{\{\s*(?<code>[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";
|
||||
|
||||
/// <summary>
|
||||
/// 扫描正文、表格、页眉和页脚,返回去重后的变量编码。
|
||||
/// </summary>
|
||||
public static List<string> 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<string>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。
|
||||
/// </summary>
|
||||
public static byte[] Render(byte[] docx, IReadOnlyDictionary<string, string> 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<string, string> 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<global::System.Text.RegularExpressions.Match>()
|
||||
.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<global::System.Xml.Linq.XElement> 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));
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 合同适用范围分页查询参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopePageInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
public long? CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板 Id。
|
||||
/// </summary>
|
||||
public long? TemplateId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同适用范围参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopeAddInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "模板Id不能为空")]
|
||||
public long TemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板版本 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "模板版本Id不能为空")]
|
||||
public long TemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围参数。
|
||||
/// </summary>
|
||||
public class OnboardingContractScopeEditInput : OnboardingContractScopeAddInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "适用范围Id不能为空")]
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+15
-25
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义合同模板、版本、变量和适用范围业务能力。
|
||||
/// 定义合同模板、DOCX 版本和自动识别变量的管理能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingContractTemplateService : ITransient
|
||||
{
|
||||
@@ -9,59 +9,49 @@ public interface IOnboardingContractTemplateService : ITransient
|
||||
/// 分页查询合同模板主档案。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询合同模板详情。
|
||||
/// </summary>
|
||||
Task<ObContractTemplate> Detail(BaseIdInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同模板主档案。
|
||||
/// </summary>
|
||||
Task Add(OnboardingContractTemplateAddInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同模板主档案。
|
||||
/// </summary>
|
||||
Task Edit(OnboardingContractTemplateEditInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被版本、适用范围或任务合同引用的模板。
|
||||
/// 删除未被版本、入职模板或任务合同引用的模板。
|
||||
/// </summary>
|
||||
Task Delete(BaseIdListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询指定合同模板的版本。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同模板 DOCX 版本。
|
||||
/// 新增合同模板 DOCX 版本并自动识别变量。
|
||||
/// </summary>
|
||||
Task AddVersion(OnboardingContractVersionAddInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 启用或停用合同模板版本。
|
||||
/// </summary>
|
||||
Task SetVersionStatus(OnboardingContractVersionStatusInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询指定模板版本的变量定义。
|
||||
/// 分页查询指定模板版本自动识别出的变量。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 为草稿模板版本新增变量。
|
||||
/// </summary>
|
||||
Task AddVariable(OnboardingTemplateVariableAddInput input);
|
||||
/// <summary>
|
||||
/// 编辑草稿模板版本中的变量。
|
||||
/// 编辑并确认草稿模板版本中的变量定义。
|
||||
/// </summary>
|
||||
Task EditVariable(OnboardingTemplateVariableEditInput input);
|
||||
/// <summary>
|
||||
/// 删除草稿模板版本中的变量。
|
||||
/// </summary>
|
||||
Task DeleteVariable(BaseIdListInput input); Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input);
|
||||
/// <summary>
|
||||
/// 新增公司或岗位的合同适用范围。
|
||||
/// </summary>
|
||||
Task AddScope(OnboardingContractScopeAddInput input);
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
Task EditScope(OnboardingContractScopeEditInput input);
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
Task DeleteScope(BaseIdListInput input);}
|
||||
}
|
||||
+95
-123
@@ -5,6 +5,12 @@ namespace SimpleAdmin.Application;
|
||||
/// </summary>
|
||||
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
|
||||
{
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public OnboardingContractTemplateService(IFileService fileService)
|
||||
{
|
||||
_fileService = fileService;
|
||||
}
|
||||
/// <summary>
|
||||
/// 分页查询合同模板主档案。
|
||||
/// </summary>
|
||||
@@ -64,7 +70,12 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
var hasScopes = await Context.Queryable<ObContractScope>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||
if (hasScopes)
|
||||
throw Oops.Bah("模板已配置适用范围,不能删除");
|
||||
throw Oops.Bah("模板已配置旧版适用范围,不能删除");
|
||||
|
||||
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplateContract>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
|
||||
if (hasOnboardingTemplates)
|
||||
throw Oops.Bah("模板已被入职模板引用,不能删除");
|
||||
|
||||
var hasContracts = await Context.Queryable<ObTaskContract>()
|
||||
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
|
||||
@@ -92,18 +103,45 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
public async Task AddVersion(OnboardingContractVersionAddInput input)
|
||||
{
|
||||
await Detail(new BaseIdInput { Id = input.TemplateId });
|
||||
|
||||
var file = await Context.Queryable<SysFile>().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<ObContractTemplateVersion>()
|
||||
.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<ObContractTemplateVersion>()).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<ObContractTemplateVersion>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,10 +160,11 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
|
||||
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
||||
{
|
||||
var hasVariables = await Context.Queryable<ObTemplateVariable>()
|
||||
.AnyAsync(it => it.TemplateVersionId == input.Id);
|
||||
if (!hasVariables)
|
||||
throw Oops.Bah("请先配置模板变量后再启用");
|
||||
var hasUnresolvedVariables = await Context.Queryable<ObTemplateVariable>()
|
||||
.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<ObContractTemplate
|
||||
.OrderBy(it => it.VariableCode)
|
||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为草稿模板版本新增变量。
|
||||
/// </summary>
|
||||
public async Task AddVariable(OnboardingTemplateVariableAddInput input)
|
||||
{
|
||||
await EnsureVersionDraft(input.TemplateVersionId);
|
||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, 0);
|
||||
await Context.Insertable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑草稿模板版本中的变量。
|
||||
/// 编辑草稿模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
|
||||
/// </summary>
|
||||
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
|
||||
{
|
||||
@@ -163,77 +191,61 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
?? throw Oops.Bah("模板变量不存在");
|
||||
if (variable.TemplateVersionId != input.TemplateVersionId)
|
||||
throw Oops.Bah("不允许变更变量所属版本");
|
||||
if (!string.Equals(variable.VariableCode, input.VariableCode, StringComparison.Ordinal))
|
||||
throw Oops.Bah("DOCX 占位符编码由系统识别,不能修改");
|
||||
|
||||
await EnsureVersionDraft(input.TemplateVersionId);
|
||||
await EnsureVariableUnique(input.TemplateVersionId, input.VariableCode, input.Id);
|
||||
await Context.Updateable(input.Adapt<ObTemplateVariable>()).ExecuteCommandAsync();
|
||||
var entity = input.Adapt<ObTemplateVariable>();
|
||||
entity.VariableCode = variable.VariableCode;
|
||||
await Context.Updateable(entity).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除草稿模板版本中的变量。
|
||||
/// </summary>
|
||||
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<ObTemplateVariable>()
|
||||
.Where(it => input.Ids.Contains(it.Id))
|
||||
.Select(it => it.TemplateVersionId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var versionId in versionIds.Distinct())
|
||||
await EnsureVersionDraft(versionId);
|
||||
|
||||
await Context.Deleteable<ObTemplateVariable>().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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询合同适用范围。
|
||||
/// </summary>
|
||||
public async Task<SqlSugarPagedList<ObContractScope>> ScopePage(OnboardingContractScopePageInput input)
|
||||
private static string InferDataType(string sourceField)
|
||||
{
|
||||
return await Context.Queryable<ObContractScope>()
|
||||
.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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增公司或岗位的合同适用范围。
|
||||
/// </summary>
|
||||
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<ObContractScope>()).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
public async Task EditScope(OnboardingContractScopeEditInput input)
|
||||
{
|
||||
var scopeExists = await Context.Queryable<ObContractScope>().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<ObContractScope>()).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
public async Task DeleteScope(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return;
|
||||
|
||||
await Context.Deleteable<ObContractScope>().In(input.Ids).ExecuteCommandAsync();
|
||||
return "string";
|
||||
}
|
||||
|
||||
private async Task EnsureCodeUnique(string code, long id)
|
||||
@@ -251,44 +263,4 @@ public class OnboardingContractTemplateService : DbRepository<ObContractTemplate
|
||||
if (version.VersionStatus != ContractTemplateVersionStatus.Draft)
|
||||
throw Oops.Bah("只有草稿版本可维护变量");
|
||||
}
|
||||
|
||||
private async Task EnsureVariableUnique(long versionId, string code, long id)
|
||||
{
|
||||
var exists = await Context.Queryable<ObTemplateVariable>()
|
||||
.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<ObCompany>()
|
||||
.AnyAsync(it => it.Id == companyId && it.Status == "enable");
|
||||
if (!companyExists)
|
||||
throw Oops.Bah("所属公司不存在或未启用");
|
||||
|
||||
if (positionId.HasValue)
|
||||
{
|
||||
var positionExists = await Context.Queryable<ObPosition>()
|
||||
.AnyAsync(it => it.Id == positionId.Value && it.CompanyId == companyId && it.Status == "enable");
|
||||
if (!positionExists)
|
||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
||||
}
|
||||
|
||||
var version = await Context.Queryable<ObContractTemplateVersion>().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<ObContractScope>()
|
||||
.AnyAsync(it => it.CompanyId == companyId
|
||||
&& it.PositionId == positionId
|
||||
&& it.TemplateVersionId == versionId
|
||||
&& it.Id != id);
|
||||
if (exists)
|
||||
throw Oops.Bah("该公司、岗位和合同版本的适用范围已存在");
|
||||
}
|
||||
}
|
||||
|
||||
-76
@@ -91,79 +91,3 @@ public class OnboardingMaterialTypeSaveInput
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "enable";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位动态字段要求参数。
|
||||
/// </summary>
|
||||
public class OnboardingFormRequirementSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动态字段 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "字段Id不能为空")]
|
||||
public long FieldId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位材料要求参数。
|
||||
/// </summary>
|
||||
public class OnboardingMaterialRequirementSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务数据 Id。
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职业务公司 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 岗位 Id;为空时表示公司级配置。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料类型 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "资料类型Id不能为空")]
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填、必传或必签。
|
||||
/// </summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
+9
-28
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 定义动态字段、材料类型及公司岗位要求配置业务能力。
|
||||
/// 定义员工动态补充字段与上传材料类型的基础配置能力。
|
||||
/// </summary>
|
||||
public interface IOnboardingEntryConfigService : ITransient
|
||||
{
|
||||
@@ -9,48 +9,29 @@ public interface IOnboardingEntryConfigService : ITransient
|
||||
/// 查询全部动态入职字段。
|
||||
/// </summary>
|
||||
Task<List<ObFormField>> ListFormFields();
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑动态入职字段。
|
||||
/// </summary>
|
||||
Task SaveFormField(OnboardingFormFieldSaveInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被填写要求引用的动态字段。
|
||||
/// 删除未被入职模板引用的动态字段。
|
||||
/// </summary>
|
||||
Task DeleteFormFields(BaseIdListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询全部入职材料类型。
|
||||
/// </summary>
|
||||
Task<List<ObMaterialType>> ListMaterialTypes();
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑入职材料类型。
|
||||
/// </summary>
|
||||
Task SaveMaterialType(OnboardingMaterialTypeSaveInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被要求或上传记录引用的材料类型。
|
||||
/// 删除未被入职模板或上传记录引用的材料类型。
|
||||
/// </summary>
|
||||
Task DeleteMaterialTypes(BaseIdListInput input);
|
||||
/// <summary>
|
||||
/// 查询指定公司的动态字段填写要求。
|
||||
/// </summary>
|
||||
Task<List<ObFormRequirement>> ListFormRequirements(long companyId);
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
Task SaveFormRequirement(OnboardingFormRequirementSaveInput input);
|
||||
/// <summary>
|
||||
/// 删除动态字段填写要求。
|
||||
/// </summary>
|
||||
Task DeleteFormRequirements(BaseIdListInput input);
|
||||
/// <summary>
|
||||
/// 查询指定公司的材料上传要求。
|
||||
/// </summary>
|
||||
Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId);
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位材料要求。
|
||||
/// </summary>
|
||||
Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input);
|
||||
/// <summary>
|
||||
/// 删除材料上传要求。
|
||||
/// </summary>
|
||||
Task DeleteMaterialRequirements(BaseIdListInput input);
|
||||
}
|
||||
}
|
||||
+10
-144
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 维护员工端动态字段、材料类型及公司岗位要求。
|
||||
/// 维护员工端动态补充字段和材料类型;与公司、岗位的组合关系由入职模板统一管理。
|
||||
/// </summary>
|
||||
public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
{
|
||||
@@ -92,8 +92,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
var isReferenced = await _db.Queryable<ObFormRequirement>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
||||
if (isReferenced)
|
||||
throw Oops.Bah("字段已被填写要求引用,不能删除");
|
||||
var isReferencedByTemplate = await _db.Queryable<ObOnboardingTemplateFormField>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.FieldId));
|
||||
if (isReferenced || isReferencedByTemplate)
|
||||
throw Oops.Bah("字段已被入职模板引用,不能删除");
|
||||
|
||||
await _db.Deleteable<ObFormField>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
@@ -155,8 +157,10 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
var isReferenced = await _db.Queryable<ObMaterialRequirement>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
if (isReferenced)
|
||||
throw Oops.Bah("资料类型已被资料要求引用,不能删除");
|
||||
var isReferencedByTemplate = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
if (isReferenced || isReferencedByTemplate)
|
||||
throw Oops.Bah("资料类型已被入职模板引用,不能删除");
|
||||
|
||||
var hasUploads = await _db.Queryable<ObTaskMaterial>()
|
||||
.AnyAsync(x => input.Ids.Contains(x.MaterialTypeId));
|
||||
@@ -165,142 +169,4 @@ public class OnboardingEntryConfigService : IOnboardingEntryConfigService
|
||||
|
||||
await _db.Deleteable<ObMaterialType>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司的动态字段填写要求。
|
||||
/// </summary>
|
||||
public Task<List<ObFormRequirement>> ListFormRequirements(long companyId)
|
||||
{
|
||||
return _db.Queryable<ObFormRequirement>()
|
||||
.Where(x => x.CompanyId == companyId)
|
||||
.OrderBy(x => x.SortCode)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
public async Task SaveFormRequirement(OnboardingFormRequirementSaveInput input)
|
||||
{
|
||||
await EnsureCompanyAndPosition(input.CompanyId, input.PositionId);
|
||||
|
||||
var fieldExists = await _db.Queryable<ObFormField>()
|
||||
.AnyAsync(x => x.Id == input.FieldId && x.Status == "enable");
|
||||
if (!fieldExists)
|
||||
throw Oops.Bah("动态字段不存在或未启用");
|
||||
|
||||
var requirementExists = await _db.Queryable<ObFormRequirement>()
|
||||
.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除动态字段填写要求。
|
||||
/// </summary>
|
||||
public Task DeleteFormRequirements(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _db.Deleteable<ObFormRequirement>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司的材料上传要求。
|
||||
/// </summary>
|
||||
public Task<List<ObMaterialRequirement>> ListMaterialRequirements(long companyId)
|
||||
{
|
||||
return _db.Queryable<ObMaterialRequirement>()
|
||||
.Where(x => x.CompanyId == companyId)
|
||||
.OrderBy(x => x.SortCode)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑公司岗位材料要求。
|
||||
/// </summary>
|
||||
public async Task SaveMaterialRequirement(OnboardingMaterialRequirementSaveInput input)
|
||||
{
|
||||
await EnsureCompanyAndPosition(input.CompanyId, input.PositionId);
|
||||
|
||||
var materialTypeExists = await _db.Queryable<ObMaterialType>()
|
||||
.AnyAsync(x => x.Id == input.MaterialTypeId && x.Status == "enable");
|
||||
if (!materialTypeExists)
|
||||
throw Oops.Bah("资料类型不存在或未启用");
|
||||
|
||||
var requirementExists = await _db.Queryable<ObMaterialRequirement>()
|
||||
.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除材料上传要求。
|
||||
/// </summary>
|
||||
public Task DeleteMaterialRequirements(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return _db.Deleteable<ObMaterialRequirement>().In(input.Ids).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private async Task EnsureCompanyAndPosition(long companyId, long? positionId)
|
||||
{
|
||||
var companyExists = await _db.Queryable<ObCompany>()
|
||||
.AnyAsync(x => x.Id == companyId && x.Status == "enable");
|
||||
if (!companyExists)
|
||||
throw Oops.Bah("公司不存在或未启用");
|
||||
|
||||
if (!positionId.HasValue)
|
||||
return;
|
||||
|
||||
var positionExists = await _db.Queryable<ObPosition>()
|
||||
.AnyAsync(x => x.Id == positionId.Value && x.CompanyId == companyId && x.Status == "enable");
|
||||
if (!positionExists)
|
||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
||||
}
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板分页查询参数。
|
||||
/// </summary>
|
||||
public class OnboardingTemplatePageInput : BasePageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 模板编码或名称关键词。
|
||||
/// </summary>
|
||||
public string Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属公司 Id。
|
||||
/// </summary>
|
||||
public long? CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用岗位 Id。
|
||||
/// </summary>
|
||||
public long? PositionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启用状态。
|
||||
/// </summary>
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存入职模板参数。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateSaveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 编辑时传入的入职模板 Id。
|
||||
/// </summary>
|
||||
public long? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板业务编码。
|
||||
/// </summary>
|
||||
public string TemplateCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板名称。
|
||||
/// </summary>
|
||||
public string TemplateName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属公司 Id。
|
||||
/// </summary>
|
||||
public long CompanyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用岗位 Id 列表。
|
||||
/// </summary>
|
||||
public List<long> PositionIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 合同配置列表。
|
||||
/// </summary>
|
||||
public List<OnboardingTemplateContractInput> Contracts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 动态补充字段列表。
|
||||
/// </summary>
|
||||
public List<OnboardingTemplateFormFieldInput> FormFields { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 上传材料列表。
|
||||
/// </summary>
|
||||
public List<OnboardingTemplateMaterialInput> Materials { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 模板状态。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "enable";
|
||||
|
||||
/// <summary>
|
||||
/// 模板说明。
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板合同配置参数。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateContractInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 合同模板 Id。
|
||||
/// </summary>
|
||||
public long ContractTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 合同模板版本 Id。
|
||||
/// </summary>
|
||||
public long ContractTemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必须查看确认。
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 打印份数。
|
||||
/// </summary>
|
||||
public int PrintCopies { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板补充字段参数。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateFormFieldInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态字段 Id。
|
||||
/// </summary>
|
||||
public long FieldId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填。
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板上传材料参数。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateMaterialInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 材料类型 Id。
|
||||
/// </summary>
|
||||
public long MaterialTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必传。
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 排序码。
|
||||
/// </summary>
|
||||
public int? SortCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板列表输出。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateOutput : ObOnboardingTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属公司名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用岗位 Id 列表。
|
||||
/// </summary>
|
||||
public List<long> PositionIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 适用岗位名称列表。
|
||||
/// </summary>
|
||||
public List<string> PositionNames { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 合同配置数量。
|
||||
/// </summary>
|
||||
public int ContractCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动态补充字段数量。
|
||||
/// </summary>
|
||||
public int FormFieldCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上传材料数量。
|
||||
/// </summary>
|
||||
public int MaterialCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板详情输出。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateDetailOutput : OnboardingTemplateOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 合同配置明细。
|
||||
/// </summary>
|
||||
public List<ObOnboardingTemplateContract> Contracts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 动态补充字段配置明细。
|
||||
/// </summary>
|
||||
public List<ObOnboardingTemplateFormField> FormFields { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 上传材料配置明细。
|
||||
/// </summary>
|
||||
public List<ObOnboardingTemplateMaterial> Materials { get; set; } = new();
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 入职模板聚合配置服务。
|
||||
/// </summary>
|
||||
public interface IOnboardingTemplateService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询入职模板。
|
||||
/// </summary>
|
||||
Task<SqlSugarPagedList<OnboardingTemplateOutput>> Page(OnboardingTemplatePageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询入职模板详情。
|
||||
/// </summary>
|
||||
Task<OnboardingTemplateDetailOutput> Detail(BaseIdInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑入职模板及全部关联配置。
|
||||
/// </summary>
|
||||
Task<long> Save(OnboardingTemplateSaveInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被任务使用的入职模板。
|
||||
/// </summary>
|
||||
Task Delete(BaseIdListInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查询公司岗位可使用的入职模板。
|
||||
/// </summary>
|
||||
Task<List<OnboardingTemplateOutput>> ListEnabled(long companyId, long positionId);
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职模板聚合配置及公司岗位匹配能力。
|
||||
/// </summary>
|
||||
public class OnboardingTemplateService : IOnboardingTemplateService
|
||||
{
|
||||
private readonly SqlSugarScope _db;
|
||||
|
||||
public OnboardingTemplateService()
|
||||
{
|
||||
_db = DbContext.DB;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SqlSugarPagedList<OnboardingTemplateOutput>> Page(OnboardingTemplatePageInput input)
|
||||
{
|
||||
var query = _db.Queryable<ObOnboardingTemplate>()
|
||||
.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<ObOnboardingTemplatePosition>()
|
||||
.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<OnboardingTemplateOutput>
|
||||
{
|
||||
PageNum = page.PageNum,
|
||||
PageSize = page.PageSize,
|
||||
Total = page.Total,
|
||||
Pages = page.Pages,
|
||||
HasPrevPages = page.HasPrevPages,
|
||||
HasNextPages = page.HasNextPages,
|
||||
List = rows
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OnboardingTemplateDetailOutput> Detail(BaseIdInput input)
|
||||
{
|
||||
var template = await _db.Queryable<ObOnboardingTemplate>().InSingleAsync(input.Id)
|
||||
?? throw Oops.Bah("入职模板不存在");
|
||||
var summary = (await BuildOutputs(new List<ObOnboardingTemplate> { 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<ObOnboardingTemplateContract>()
|
||||
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync(),
|
||||
FormFields = await _db.Queryable<ObOnboardingTemplateFormField>()
|
||||
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync(),
|
||||
Materials = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.Where(item => item.OnboardingTemplateId == template.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<long> Save(OnboardingTemplateSaveInput input)
|
||||
{
|
||||
NormalizeAndValidateInput(input);
|
||||
await ValidateReferences(input);
|
||||
|
||||
var templateId = input.Id ?? global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||
var codeExists = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.AnyAsync(item => item.TemplateCode == input.TemplateCode && item.Id != templateId);
|
||||
if (codeExists)
|
||||
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<ObOnboardingTemplate>().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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Delete(BaseIdListInput input)
|
||||
{
|
||||
if (input.Ids.Count == 0)
|
||||
return;
|
||||
|
||||
var isUsed = await _db.Queryable<ObOnboardingTask>()
|
||||
.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<ObOnboardingTemplate>().In(input.Ids).ExecuteCommandAsync();
|
||||
await _db.Ado.CommitTranAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _db.Ado.RollbackTranAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<OnboardingTemplateOutput>> ListEnabled(long companyId, long positionId)
|
||||
{
|
||||
var templateIds = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||
.Where(item => item.PositionId == positionId)
|
||||
.Select(item => item.OnboardingTemplateId)
|
||||
.ToListAsync();
|
||||
var templates = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.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<long>();
|
||||
input.Contracts = input.Contracts?
|
||||
.GroupBy(item => item.ContractTemplateVersionId)
|
||||
.Select(group => group.First())
|
||||
.ToList() ?? new List<OnboardingTemplateContractInput>();
|
||||
input.FormFields = input.FormFields?
|
||||
.GroupBy(item => item.FieldId)
|
||||
.Select(group => group.First())
|
||||
.ToList() ?? new List<OnboardingTemplateFormFieldInput>();
|
||||
input.Materials = input.Materials?
|
||||
.GroupBy(item => item.MaterialTypeId)
|
||||
.Select(group => group.First())
|
||||
.ToList() ?? new List<OnboardingTemplateMaterialInput>();
|
||||
|
||||
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<ObCompany>()
|
||||
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||
if (!companyEnabled)
|
||||
throw Oops.Bah("所属公司不存在或未启用");
|
||||
|
||||
var positionCount = await _db.Queryable<ObPosition>()
|
||||
.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<ObContractTemplateVersion>()
|
||||
.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<ObTemplateVariable>()
|
||||
.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<ObFormField>()
|
||||
: await _db.Queryable<ObFormField>()
|
||||
.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<ObTemplateVariable>()
|
||||
.Where(item => versionIds.Contains(item.TemplateVersionId)
|
||||
&& item.VariableType == ContractVariableType.Employee)
|
||||
.ToListAsync();
|
||||
var availableFields = new HashSet<string>(
|
||||
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<ObMaterialType>()
|
||||
.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<ObOnboardingTemplatePosition>()
|
||||
.Where(item => item.OnboardingTemplateId == templateId)
|
||||
.ExecuteCommandAsync();
|
||||
await _db.Deleteable<ObOnboardingTemplateContract>()
|
||||
.Where(item => item.OnboardingTemplateId == templateId)
|
||||
.ExecuteCommandAsync();
|
||||
await _db.Deleteable<ObOnboardingTemplateFormField>()
|
||||
.Where(item => item.OnboardingTemplateId == templateId)
|
||||
.ExecuteCommandAsync();
|
||||
await _db.Deleteable<ObOnboardingTemplateMaterial>()
|
||||
.Where(item => item.OnboardingTemplateId == templateId)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private async Task<List<OnboardingTemplateOutput>> BuildOutputs(List<ObOnboardingTemplate> templates)
|
||||
{
|
||||
if (templates.Count == 0)
|
||||
return new List<OnboardingTemplateOutput>();
|
||||
|
||||
var templateIds = templates.Select(item => item.Id).ToList();
|
||||
var companyIds = templates.Select(item => item.CompanyId).Distinct().ToList();
|
||||
var companies = await _db.Queryable<ObCompany>()
|
||||
.Where(item => companyIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
var relations = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||
.ToListAsync();
|
||||
var positionIds = relations.Select(item => item.PositionId).Distinct().ToList();
|
||||
var positions = await _db.Queryable<ObPosition>()
|
||||
.Where(item => positionIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
var contracts = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||
.ToListAsync();
|
||||
var fields = await _db.Queryable<ObOnboardingTemplateFormField>()
|
||||
.Where(item => templateIds.Contains(item.OnboardingTemplateId))
|
||||
.ToListAsync();
|
||||
var materials = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
+71
-80
@@ -1,92 +1,95 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现员工端入职资料提交业务能力。
|
||||
/// 实现员工端动态资料、上传材料、合同确认和最终提交能力。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存候选人填写的动态字段值。
|
||||
/// 保存候选人填写的动态补充字段值。
|
||||
/// </summary>
|
||||
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<ObFormField>()
|
||||
.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<ObTaskFormValue>()
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关联候选人已经上传的系统文件与材料类型。
|
||||
/// 关联已上传 OSS 文件与任务材料要求。
|
||||
/// </summary>
|
||||
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<ObMaterialType>()
|
||||
.FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable")
|
||||
?? throw Oops.Bah("资料类型不存在或已停用");
|
||||
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.FirstAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId)
|
||||
?? throw Oops.Bah("材料类型不属于当前入职模板");
|
||||
var fileExists = await _db.Queryable<SysFile>().AnyAsync(item => item.Id == input.FileId);
|
||||
if (!fileExists)
|
||||
throw Oops.Bah("上传文件不存在");
|
||||
|
||||
var materialCount = await _db.Queryable<ObTaskMaterial>()
|
||||
.CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId);
|
||||
if (materialCount >= material.MaxFileCount)
|
||||
throw Oops.Bah("该资料已达到最大上传数量");
|
||||
var existingMaterials = await _db.Queryable<ObTaskMaterial>()
|
||||
.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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录候选人已阅读并确认指定任务合同。
|
||||
/// 记录候选人已经查看并确认动态生成的合同。
|
||||
/// </summary>
|
||||
public async Task ConfirmContract(OnboardingConfirmContractInput input)
|
||||
{
|
||||
@@ -102,15 +105,16 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验全部必填资料后提交入职任务进入审核。
|
||||
/// 校验固定信息、动态字段、材料、合同变量和合同确认后提交审核。
|
||||
/// </summary>
|
||||
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<ObTaskContract>()
|
||||
.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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在保存文件前校验材料适用范围、扩展名、大小和数量。
|
||||
/// 上传 OSS 前按照任务材料快照校验扩展名、文件大小和数量。
|
||||
/// </summary>
|
||||
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<ObMaterialType>()
|
||||
.FirstAsync(item => item.Id == input.MaterialTypeId && item.Status == "enable")
|
||||
?? throw Oops.Bah("资料类型不存在或已停用");
|
||||
var requirement = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.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<ObTaskMaterial>()
|
||||
.CountAsync(item => item.TaskId == task.Id && item.MaterialTypeId == input.MaterialTypeId);
|
||||
if (materialCount >= material.MaxFileCount)
|
||||
throw Oops.Bah("该资料已达到最大上传数量");
|
||||
var materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.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<List<ObFormRequirement>> GetEffectiveFormRequirements(ObOnboardingTask task)
|
||||
private Task<List<ObTaskFormRequirement>> GetFormRequirements(long taskId)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObFormRequirement>()
|
||||
.Where(item => item.CompanyId == task.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == task.PositionId))
|
||||
return _db.Queryable<ObTaskFormRequirement>()
|
||||
.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<List<ObMaterialRequirement>> GetEffectiveMaterialRequirements(ObOnboardingTask task)
|
||||
private Task<List<ObTaskMaterialRequirement>> GetMaterialRequirements(long taskId)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObMaterialRequirement>()
|
||||
.Where(item => item.CompanyId == task.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == task.PositionId))
|
||||
return _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.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<ObOnboardingTask> GetEditableTask(string token)
|
||||
@@ -283,4 +274,4 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-76
@@ -1,22 +1,25 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 员工手机端入口;仅凭一次性任务链接访问,绝不创建正式员工。
|
||||
/// 员工手机端公开入口,仅凭一次性任务链接访问入职资料。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过有效 Token 获取员工端任务、字段、材料和合同信息。
|
||||
/// 通过有效 Token 获取任务创建时冻结的字段、材料和合同信息。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskPublicDetailOutput> 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<ObFormField>()
|
||||
.Where(x => formRequirements.Select(r => r.FieldId).Contains(x.Id) && x.Status == "enable")
|
||||
var formRequirements = await _db.Queryable<ObTaskFormRequirement>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
var materialTypes = await _db.Queryable<ObMaterialType>()
|
||||
.Where(x => materialRequirements.Select(r => r.MaterialTypeId).Contains(x.Id) && x.Status == "enable")
|
||||
var materialRequirements = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
|
||||
return new OnboardingTaskPublicDetailOutput
|
||||
{
|
||||
Task = task,
|
||||
EmployeeInfo = await _db.Queryable<ObTaskEmployeeInfo>().FirstAsync(it => it.TaskId == task.Id),
|
||||
EmployeeInfo = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id),
|
||||
Contracts = await _db.Queryable<ObTaskContract>()
|
||||
.Where(it => it.TaskId == task.Id)
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
DynamicValues = await _db.Queryable<ObTaskFormValue>()
|
||||
.Where(x => x.TaskId == task.Id)
|
||||
.Where(item => item.TaskId == task.Id)
|
||||
.ToListAsync(),
|
||||
Materials = await _db.Queryable<ObTaskMaterial>()
|
||||
.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()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存候选人固定基础信息并刷新合同变量快照。
|
||||
/// 保存候选人的固定基础信息并刷新尚未确认的合同变量快照。
|
||||
/// </summary>
|
||||
public async Task SaveEmployeeInfo(OnboardingTaskPublicSaveInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var task = await _db.Queryable<ObOnboardingTask>().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<ObTaskEmployeeInfo>().FirstAsync(it => it.TaskId == task.Id);
|
||||
var old = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||
.FirstAsync(item => item.TaskId == task.Id);
|
||||
var entity = input.Adapt<ObTaskEmployeeInfo>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验任务归属后返回合同模板源文件 Id。
|
||||
/// 根据员工当前已填写信息生成合同预览,并返回已替换变量的 DOCX 文件 Id。
|
||||
/// </summary>
|
||||
public async Task<long> GetContractSourceFileId(OnboardingPublicContractSourceInput input)
|
||||
{
|
||||
var link = await GetActiveLink(input.Token);
|
||||
var contract = await _db.Queryable<ObTaskContract>()
|
||||
.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<ObContractTemplateVersion>().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<List<ObFormRequirement>> GetEffectiveFormRequirements(ObOnboardingTask task)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObFormRequirement>()
|
||||
.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<List<ObMaterialRequirement>> GetEffectiveMaterialRequirements(ObOnboardingTask task)
|
||||
{
|
||||
var requirements = await _db.Queryable<ObMaterialRequirement>()
|
||||
.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<ObTaskLink> 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<ObTaskLink>().FirstAsync(it => it.TokenHash == hash)
|
||||
var link = await _db.Queryable<ObTaskLink>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,16 @@ public class OnboardingTaskCreateInput
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "岗位Id不能为空")]
|
||||
public long PositionId { get; set; }
|
||||
/// <summary>
|
||||
/// 本次任务使用的入职模板 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "入职模板Id不能为空")]
|
||||
public long OnboardingTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时由人事预先填写的合同变量。
|
||||
/// </summary>
|
||||
public List<OnboardingInitialContractVariableInput> ContractVariables { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时指定的审核人用户 Id。
|
||||
@@ -33,6 +43,28 @@ public class OnboardingTaskCreateInput
|
||||
public string Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建任务时填写的单个合同变量。
|
||||
/// </summary>
|
||||
public class OnboardingInitialContractVariableInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 变量所属合同模板版本 Id。
|
||||
/// </summary>
|
||||
[IdNotNull(ErrorMessage = "合同模板版本Id不能为空")]
|
||||
public long ContractTemplateVersionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DOCX 中的完整变量编码,例如 hr.MonthlySalary。
|
||||
/// </summary>
|
||||
public string VariableCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 人事录入的变量值。
|
||||
/// </summary>
|
||||
public string VariableValue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入职任务创建结果。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 实现入职任务创建业务能力。
|
||||
/// 实现基于入职模板创建任务及冻结配置快照的业务能力。
|
||||
/// </summary>
|
||||
public class OnboardingTaskService : IOnboardingTaskService
|
||||
{
|
||||
@@ -19,42 +19,69 @@ public class OnboardingTaskService : IOnboardingTaskService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
|
||||
/// 校验公司、岗位和入职模板后创建任务,并冻结合同、补充字段及材料要求。
|
||||
/// </summary>
|
||||
public async Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input)
|
||||
{
|
||||
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||
if (!companyEnabled)
|
||||
{
|
||||
throw Oops.Bah("公司不存在或未启用");
|
||||
}
|
||||
|
||||
var positionEnabled = await _db.Queryable<ObPosition>()
|
||||
.AnyAsync(item => item.Id == input.PositionId
|
||||
&& item.CompanyId == input.CompanyId
|
||||
&& item.Status == "enable");
|
||||
if (!positionEnabled)
|
||||
{
|
||||
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
|
||||
}
|
||||
|
||||
var scopes = await _db.Queryable<ObContractScope>()
|
||||
.Where(item => item.CompanyId == input.CompanyId
|
||||
&& (item.PositionId == null || item.PositionId == input.PositionId))
|
||||
var onboardingTemplate = await _db.Queryable<ObOnboardingTemplate>()
|
||||
.FirstAsync(item => item.Id == input.OnboardingTemplateId
|
||||
&& item.CompanyId == input.CompanyId
|
||||
&& item.Status == "enable")
|
||||
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司");
|
||||
var templateSupportsPosition = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
|
||||
&& item.PositionId == input.PositionId);
|
||||
if (!templateSupportsPosition)
|
||||
throw Oops.Bah("所选入职模板不适用于该岗位");
|
||||
|
||||
var contractConfigs = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
if (contractConfigs.Count == 0)
|
||||
throw Oops.Bah("入职模板尚未配置合同文件");
|
||||
|
||||
var fieldConfigs = await _db.Queryable<ObOnboardingTemplateFormField>()
|
||||
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||
.OrderBy(item => item.SortCode)
|
||||
.ToListAsync();
|
||||
var materialConfigs = await _db.Queryable<ObOnboardingTemplateMaterial>()
|
||||
.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<ObContractTemplate>()
|
||||
.Where(item => templateIds.Contains(item.Id))
|
||||
.ToListAsync();
|
||||
var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList();
|
||||
var enabledVersionCount = await _db.Queryable<ObContractTemplateVersion>()
|
||||
.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<ObFormField>()
|
||||
: await _db.Queryable<ObFormField>().Where(item => fieldIds.Contains(item.Id)).ToListAsync();
|
||||
var materialIds = materialConfigs.Select(item => item.MaterialTypeId).Distinct().ToList();
|
||||
var materialTypes = materialIds.Count == 0
|
||||
? new List<ObMaterialType>()
|
||||
: await _db.Queryable<ObMaterialType>().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<ObContractTemplate>()
|
||||
.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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验并保存任务创建时填写的人事合同变量。
|
||||
/// </summary>
|
||||
private async Task ApplyInitialContractVariables(
|
||||
List<OnboardingInitialContractVariableInput> inputs,
|
||||
List<ObTaskContract> contracts)
|
||||
{
|
||||
inputs ??= new List<OnboardingInitialContractVariableInput>();
|
||||
var versionIds = contracts.Select(item => item.TemplateVersionId).Distinct().ToList();
|
||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||
.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<ObTaskContractVariable>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-56
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供合同模板、版本、变量和适用范围相关 HTTP 接口。
|
||||
/// 提供合同模板、DOCX 版本和自动识别变量配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职合同模板")]
|
||||
[Route("biz/onboarding/contract-template")]
|
||||
@@ -118,15 +118,6 @@ public class OnboardingContractTemplateController
|
||||
return await _service.VariablePage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增模板变量。
|
||||
/// </summary>
|
||||
[HttpPost("variable/add")]
|
||||
public async Task AddVariable([FromBody] OnboardingTemplateVariableAddInput input)
|
||||
{
|
||||
await _service.AddVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑模板变量。
|
||||
/// </summary>
|
||||
@@ -135,49 +126,4 @@ public class OnboardingContractTemplateController
|
||||
{
|
||||
await _service.EditVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除模板变量。
|
||||
/// </summary>
|
||||
[HttpPost("variable/delete")]
|
||||
public async Task DeleteVariable([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteVariable(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询合同适用范围。
|
||||
/// </summary>
|
||||
[HttpGet("scope/page")]
|
||||
public async Task<dynamic> ScopePage([FromQuery] OnboardingContractScopePageInput input)
|
||||
{
|
||||
return await _service.ScopePage(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/add")]
|
||||
public async Task AddScope([FromBody] OnboardingContractScopeAddInput input)
|
||||
{
|
||||
await _service.AddScope(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/edit")]
|
||||
public async Task EditScope([FromBody] OnboardingContractScopeEditInput input)
|
||||
{
|
||||
await _service.EditScope(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除合同适用范围。
|
||||
/// </summary>
|
||||
[HttpPost("scope/delete")]
|
||||
public async Task DeleteScope([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteScope(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-56
@@ -1,7 +1,7 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供入职字段与材料要求配置相关 HTTP 接口。
|
||||
/// 提供员工动态补充字段与上传材料类型配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职填写与材料配置")]
|
||||
[Route("biz/onboarding/entry-config")]
|
||||
@@ -68,58 +68,4 @@ public class OnboardingEntryConfigController
|
||||
{
|
||||
await _service.DeleteMaterialTypes(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
[HttpGet("form-requirement/list")]
|
||||
public async Task<dynamic> ListFormRequirements([FromQuery] long companyId)
|
||||
{
|
||||
return await _service.ListFormRequirements(companyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位动态字段要求。
|
||||
/// </summary>
|
||||
[HttpPost("form-requirement/save")]
|
||||
public async Task SaveFormRequirement([FromBody] OnboardingFormRequirementSaveInput input)
|
||||
{
|
||||
await _service.SaveFormRequirement(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除动态字段要求。
|
||||
/// </summary>
|
||||
[HttpPost("form-requirement/delete")]
|
||||
public async Task DeleteFormRequirements([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteFormRequirements(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询公司岗位材料要求。
|
||||
/// </summary>
|
||||
[HttpGet("material-requirement/list")]
|
||||
public async Task<dynamic> ListMaterialRequirements([FromQuery] long companyId)
|
||||
{
|
||||
return await _service.ListMaterialRequirements(companyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存公司岗位材料要求。
|
||||
/// </summary>
|
||||
[HttpPost("material-requirement/save")]
|
||||
public async Task SaveMaterialRequirement([FromBody] OnboardingMaterialRequirementSaveInput input)
|
||||
{
|
||||
await _service.SaveMaterialRequirement(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除材料要求。
|
||||
/// </summary>
|
||||
[HttpPost("material-requirement/delete")]
|
||||
public async Task DeleteMaterialRequirements([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.DeleteMaterialRequirements(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class OnboardingPublicTaskController
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载当前任务锁定的合同模板源文件。
|
||||
/// 下载根据当前任务资料动态生成并已替换变量的合同文件。
|
||||
/// </summary>
|
||||
[HttpGet("contract-source")]
|
||||
public async Task<FileStreamResult> ContractSource([FromQuery] OnboardingPublicContractSourceInput input)
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
namespace SimpleAdmin.Web.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 提供入职模板聚合配置接口。
|
||||
/// </summary>
|
||||
[ApiDescriptionSettings("Application", Tag = "入职模板")]
|
||||
[Route("biz/onboarding/template")]
|
||||
[RolePermission]
|
||||
public class OnboardingTemplateController
|
||||
{
|
||||
private readonly IOnboardingTemplateService _service;
|
||||
|
||||
public OnboardingTemplateController(IOnboardingTemplateService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询入职模板。
|
||||
/// </summary>
|
||||
[HttpGet("page")]
|
||||
public async Task<dynamic> Page([FromQuery] OnboardingTemplatePageInput input)
|
||||
{
|
||||
return await _service.Page(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询入职模板详情及全部关联配置。
|
||||
/// </summary>
|
||||
[HttpGet("detail")]
|
||||
public async Task<dynamic> Detail([FromQuery] BaseIdInput input)
|
||||
{
|
||||
return await _service.Detail(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或编辑入职模板。
|
||||
/// </summary>
|
||||
[HttpPost("save")]
|
||||
public async Task<long> Save([FromBody] OnboardingTemplateSaveInput input)
|
||||
{
|
||||
return await _service.Save(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除未被任务使用的入职模板。
|
||||
/// </summary>
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] BaseIdListInput input)
|
||||
{
|
||||
await _service.Delete(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定公司岗位可使用的入职模板。
|
||||
/// </summary>
|
||||
[HttpGet("list-enabled")]
|
||||
public async Task<dynamic> ListEnabled([FromQuery] long companyId, [FromQuery] long positionId)
|
||||
{
|
||||
return await _service.ListEnabled(companyId, positionId);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingContractTemplateController">
|
||||
<summary>
|
||||
提供合同模板、版本、变量和适用范围相关 HTTP 接口。
|
||||
提供合同模板、DOCX 版本和自动识别变量配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.UploadDocx(Microsoft.AspNetCore.Http.IFormFile)">
|
||||
@@ -141,41 +141,11 @@
|
||||
分页查询模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.AddVariable(SimpleAdmin.Application.OnboardingTemplateVariableAddInput)">
|
||||
<summary>
|
||||
新增模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.EditVariable(SimpleAdmin.Application.OnboardingTemplateVariableEditInput)">
|
||||
<summary>
|
||||
编辑模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.DeleteVariable(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除模板变量。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.ScopePage(SimpleAdmin.Application.OnboardingContractScopePageInput)">
|
||||
<summary>
|
||||
分页查询合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.AddScope(SimpleAdmin.Application.OnboardingContractScopeAddInput)">
|
||||
<summary>
|
||||
新增合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.EditScope(SimpleAdmin.Application.OnboardingContractScopeEditInput)">
|
||||
<summary>
|
||||
编辑合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingContractTemplateController.DeleteScope(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除合同适用范围。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingEmployeeController">
|
||||
<summary>
|
||||
提供正式员工档案相关 HTTP 接口。
|
||||
@@ -193,7 +163,7 @@
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingEntryConfigController">
|
||||
<summary>
|
||||
提供入职字段与材料要求配置相关 HTTP 接口。
|
||||
提供员工动态补充字段与上传材料类型配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListFormFields">
|
||||
@@ -226,36 +196,6 @@
|
||||
删除入职材料类型。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListFormRequirements(System.Int64)">
|
||||
<summary>
|
||||
查询公司岗位动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.SaveFormRequirement(SimpleAdmin.Application.OnboardingFormRequirementSaveInput)">
|
||||
<summary>
|
||||
保存公司岗位动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.DeleteFormRequirements(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除动态字段要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.ListMaterialRequirements(System.Int64)">
|
||||
<summary>
|
||||
查询公司岗位材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.SaveMaterialRequirement(SimpleAdmin.Application.OnboardingMaterialRequirementSaveInput)">
|
||||
<summary>
|
||||
保存公司岗位材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingEntryConfigController.DeleteMaterialRequirements(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除材料要求。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingMaterialReviewController">
|
||||
<summary>
|
||||
提供候选人材料审核相关 HTTP 接口。
|
||||
@@ -348,7 +288,7 @@
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingPublicTaskController.ContractSource(SimpleAdmin.Application.OnboardingPublicContractSourceInput)">
|
||||
<summary>
|
||||
下载当前任务锁定的合同模板源文件。
|
||||
下载根据当前任务资料动态生成并已替换变量的合同文件。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingTaskContractVariableController">
|
||||
@@ -406,6 +346,36 @@
|
||||
查询入职任务完整详情。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.OnboardingTemplateController">
|
||||
<summary>
|
||||
提供入职模板聚合配置接口。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Page(SimpleAdmin.Application.OnboardingTemplatePageInput)">
|
||||
<summary>
|
||||
分页查询入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Detail(SimpleAdmin.Core.BaseIdInput)">
|
||||
<summary>
|
||||
查询入职模板详情及全部关联配置。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Save(SimpleAdmin.Application.OnboardingTemplateSaveInput)">
|
||||
<summary>
|
||||
新增或编辑入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.Delete(SimpleAdmin.Core.BaseIdListInput)">
|
||||
<summary>
|
||||
删除未被任务使用的入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:SimpleAdmin.Web.Core.OnboardingTemplateController.ListEnabled(System.Int64,System.Int64)">
|
||||
<summary>
|
||||
查询指定公司岗位可使用的入职模板。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:SimpleAdmin.Web.Core.BizOrgController">
|
||||
<summary>
|
||||
机构管理控制器
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user