入职任务预览,入职任务cope,验证填入的信息
This commit is contained in:
+55
@@ -11,3 +11,58 @@ public class OnboardingGenerateContractInput
|
|||||||
[IdNotNull(ErrorMessage = "任务合同Id不能为空")]
|
[IdNotNull(ErrorMessage = "任务合同Id不能为空")]
|
||||||
public long TaskContractId { get; set; }
|
public long TaskContractId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 人事在创建入职任务前下载合同预览的参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractPreviewInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务公司 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "公司Id不能为空")]
|
||||||
|
public long CompanyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务部门 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "部门Id不能为空")]
|
||||||
|
public long DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职岗位 Id。
|
||||||
|
/// </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>
|
||||||
|
/// 创建任务前生成的临时 DOCX 预览文件。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractPreviewOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DOCX 文件内容,仅通过当前请求返回,不保存到文件表或 OSS。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] FileBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 浏览器下载时使用的文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
+6
@@ -5,6 +5,12 @@ namespace SimpleAdmin.Application;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IOnboardingContractDocumentService : ITransient
|
public interface IOnboardingContractDocumentService : ITransient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,根据人事当前填写的参数生成临时 DOCX 预览。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
|
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+197
@@ -14,6 +14,177 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
_fileService = fileService;
|
_fileService = fileService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在任务创建前生成单份临时 DOCX,仅通过响应返回,不写文件表和 OSS。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
|
||||||
|
OnboardingContractPreviewInput input)
|
||||||
|
{
|
||||||
|
var company = await _db.Queryable<ObCompany>()
|
||||||
|
.FirstAsync(item => item.Id == input.CompanyId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE)
|
||||||
|
?? throw Oops.Bah("公司不存在或未启用");
|
||||||
|
|
||||||
|
var departmentExists = await _db.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(item => item.Id == input.DepartmentId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE);
|
||||||
|
if (!departmentExists)
|
||||||
|
throw Oops.Bah("部门不存在、未启用或不属于该公司");
|
||||||
|
|
||||||
|
var positionExists = await _db.Queryable<ObPosition>()
|
||||||
|
.AnyAsync(item => item.Id == input.PositionId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE);
|
||||||
|
if (!positionExists)
|
||||||
|
throw Oops.Bah("岗位不存在、未启用或不属于该部门/公司");
|
||||||
|
|
||||||
|
var onboardingTemplate = await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.FirstAsync(item => item.Id == input.OnboardingTemplateId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE)
|
||||||
|
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司、部门");
|
||||||
|
|
||||||
|
var supportsPosition = await _db.Queryable<ObOnboardingTemplatePosition>()
|
||||||
|
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
|
||||||
|
&& item.PositionId == input.PositionId);
|
||||||
|
if (!supportsPosition)
|
||||||
|
throw Oops.Bah("所选入职模板不适用于该岗位");
|
||||||
|
|
||||||
|
// 当前业务只预览入职模板中排序第一的合同,后续多合同可在此扩展。
|
||||||
|
var contractConfig = await _db.Queryable<ObOnboardingTemplateContract>()
|
||||||
|
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.FirstAsync()
|
||||||
|
?? throw Oops.Bah("入职模板尚未配置合同文件");
|
||||||
|
|
||||||
|
var contractTemplate = await _db.Queryable<ObContractTemplate>()
|
||||||
|
.FirstAsync(item => item.Id == contractConfig.ContractTemplateId
|
||||||
|
&& item.CompanyId == input.CompanyId
|
||||||
|
&& item.DepartmentId == input.DepartmentId
|
||||||
|
&& item.Status == CommonStatusConst.ENABLE)
|
||||||
|
?? throw Oops.Bah("合同模板不存在、未启用或不属于当前公司、部门");
|
||||||
|
|
||||||
|
var contractSupportsPosition = await _db.Queryable<ObContractTemplatePosition>()
|
||||||
|
.AnyAsync(item => item.ContractTemplateId == contractTemplate.Id
|
||||||
|
&& item.PositionId == input.PositionId);
|
||||||
|
if (!contractSupportsPosition)
|
||||||
|
throw Oops.Bah("合同模板不适用于当前岗位");
|
||||||
|
|
||||||
|
var version = await _db.Queryable<ObContractTemplateVersion>()
|
||||||
|
.FirstAsync(item => item.Id == contractConfig.ContractTemplateVersionId
|
||||||
|
&& item.TemplateId == contractTemplate.Id
|
||||||
|
&& item.VersionStatus == ContractTemplateVersionStatus.Enabled)
|
||||||
|
?? throw Oops.Bah("合同版本不存在、未启用或不属于当前合同模板");
|
||||||
|
|
||||||
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
|
.Where(item => item.TemplateVersionId == version.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync();
|
||||||
|
var duplicateDefinition = definitions
|
||||||
|
.GroupBy(item => item.VariableCode, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateDefinition != null)
|
||||||
|
throw Oops.Bah($"合同变量“{duplicateDefinition.Key}”存在重复定义");
|
||||||
|
|
||||||
|
var inputs = input.ContractVariables
|
||||||
|
?? new List<OnboardingInitialContractVariableInput>();
|
||||||
|
OnboardingTaskService.ValidateInitialContractDateRanges(inputs);
|
||||||
|
|
||||||
|
var duplicateInput = inputs
|
||||||
|
.GroupBy(item => new
|
||||||
|
{
|
||||||
|
item.ContractTemplateVersionId,
|
||||||
|
VariableCode = item.VariableCode?.ToUpperInvariant()
|
||||||
|
})
|
||||||
|
.FirstOrDefault(group => group.Count() > 1);
|
||||||
|
if (duplicateInput != null)
|
||||||
|
throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
|
||||||
|
|
||||||
|
var editableDefinitions = definitions
|
||||||
|
.Where(IsHrEditableDefinition)
|
||||||
|
.ToList();
|
||||||
|
foreach (var submitted in inputs)
|
||||||
|
{
|
||||||
|
var definitionExists = submitted.ContractTemplateVersionId == version.Id
|
||||||
|
&& editableDefinitions.Any(definition =>
|
||||||
|
string.Equals(
|
||||||
|
definition.VariableCode,
|
||||||
|
submitted.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (!definitionExists)
|
||||||
|
throw Oops.Bah($"合同变量“{submitted.VariableCode}”不存在或不允许人事填写");
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingRequired = editableDefinitions.FirstOrDefault(definition =>
|
||||||
|
{
|
||||||
|
if (!definition.Required)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
|
item.ContractTemplateVersionId == version.Id
|
||||||
|
&& string.Equals(
|
||||||
|
item.VariableCode,
|
||||||
|
definition.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
return string.IsNullOrWhiteSpace(
|
||||||
|
submitted?.VariableValue ?? definition.DefaultValue);
|
||||||
|
});
|
||||||
|
if (missingRequired != null)
|
||||||
|
throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”");
|
||||||
|
|
||||||
|
var previewTask = new ObOnboardingTask
|
||||||
|
{
|
||||||
|
TaskNo = "【发起后自动生成】",
|
||||||
|
CompanyId = input.CompanyId,
|
||||||
|
DepartmentId = input.DepartmentId,
|
||||||
|
PositionId = input.PositionId,
|
||||||
|
OnboardingTemplateId = input.OnboardingTemplateId
|
||||||
|
};
|
||||||
|
var emptyDynamicValues = new Dictionary<string, string>(
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
var values = definitions.ToDictionary(
|
||||||
|
definition => definition.VariableCode,
|
||||||
|
definition =>
|
||||||
|
{
|
||||||
|
var submitted = inputs.FirstOrDefault(item =>
|
||||||
|
item.ContractTemplateVersionId == version.Id
|
||||||
|
&& string.Equals(
|
||||||
|
item.VariableCode,
|
||||||
|
definition.VariableCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (submitted != null)
|
||||||
|
return submitted.VariableValue ?? string.Empty;
|
||||||
|
|
||||||
|
return OnboardingContractSnapshotService.ResolveValue(
|
||||||
|
definition,
|
||||||
|
null,
|
||||||
|
emptyDynamicValues,
|
||||||
|
company,
|
||||||
|
previewTask,
|
||||||
|
true)
|
||||||
|
?? string.Empty;
|
||||||
|
},
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var source = await _fileService.Download(
|
||||||
|
new BaseIdInput { Id = version.SourceDocxFileId })
|
||||||
|
?? throw Oops.Bah("无法读取合同源文件");
|
||||||
|
await using var sourceStream = source.FileStream;
|
||||||
|
using var sourceBytes = new MemoryStream();
|
||||||
|
await sourceStream.CopyToAsync(sourceBytes);
|
||||||
|
|
||||||
|
return new OnboardingContractPreviewOutput
|
||||||
|
{
|
||||||
|
FileBytes = DocxTemplateProcessor.Render(
|
||||||
|
sourceBytes.ToArray(),
|
||||||
|
values),
|
||||||
|
FileName = BuildPreviewFileName(contractTemplate.TemplateName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -86,4 +257,30 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
|
|||||||
await _db.Updateable(contract).ExecuteCommandAsync();
|
await _db.Updateable(contract).ExecuteCommandAsync();
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断变量是否允许人事在发起任务前填写。
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsHrEditableDefinition(ObTemplateVariable definition)
|
||||||
|
{
|
||||||
|
return definition.HrEditable
|
||||||
|
&& (definition.VariableType == "hr"
|
||||||
|
|| definition.VariableType == ContractVariableType.Contract
|
||||||
|
|| definition.VariableType == ContractVariableType.Company
|
||||||
|
|| definition.VariableCode.StartsWith(
|
||||||
|
"hr.",
|
||||||
|
StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成兼容 Windows 和 Linux 的合同预览下载文件名。
|
||||||
|
/// </summary>
|
||||||
|
private static string BuildPreviewFileName(string contractName)
|
||||||
|
{
|
||||||
|
var invalidChars = global::System.IO.Path.GetInvalidFileNameChars();
|
||||||
|
var safeName = new string((contractName ?? "合同")
|
||||||
|
.Select(character => invalidChars.Contains(character) ? '_' : character)
|
||||||
|
.ToArray());
|
||||||
|
return $"{safeName}_人事预览_{DateTime.Now:yyyyMMddHHmmss}.docx";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-5
@@ -110,12 +110,13 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveValue(
|
internal static string ResolveValue(
|
||||||
ObTemplateVariable definition,
|
ObTemplateVariable definition,
|
||||||
ObTaskEmployeeInfo employeeInfo,
|
ObTaskEmployeeInfo employeeInfo,
|
||||||
IReadOnlyDictionary<string, string> dynamicValues,
|
IReadOnlyDictionary<string, string> dynamicValues,
|
||||||
ObCompany company,
|
ObCompany company,
|
||||||
ObOnboardingTask task)
|
ObOnboardingTask task,
|
||||||
|
bool useEmployeePlaceholder = false)
|
||||||
{
|
{
|
||||||
var sourceField = GetSourceField(definition);
|
var sourceField = GetSourceField(definition);
|
||||||
if (definition.VariableType == ContractVariableType.Employee)
|
if (definition.VariableType == ContractVariableType.Employee)
|
||||||
@@ -133,7 +134,15 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
|||||||
if (doc.RootElement.TryGetProperty(sourceField, out var prop)
|
if (doc.RootElement.TryGetProperty(sourceField, out var prop)
|
||||||
|| doc.RootElement.TryGetProperty(definition.VariableCode, out prop))
|
|| doc.RootElement.TryGetProperty(definition.VariableCode, out prop))
|
||||||
{
|
{
|
||||||
var str = prop.GetString();
|
var str = prop.ValueKind switch
|
||||||
|
{
|
||||||
|
global::System.Text.Json.JsonValueKind.String => prop.GetString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.Number => prop.ToString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.True => bool.TrueString,
|
||||||
|
global::System.Text.Json.JsonValueKind.False => bool.FalseString,
|
||||||
|
global::System.Text.Json.JsonValueKind.Null => null,
|
||||||
|
_ => throw Oops.Bah($"员工补充变量“{definition.VariableName}”格式不正确")
|
||||||
|
};
|
||||||
if (!string.IsNullOrWhiteSpace(str)) return str;
|
if (!string.IsNullOrWhiteSpace(str)) return str;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,8 +152,11 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dynamicValues.TryGetValue(sourceField, out var dynamicValue)
|
if (dynamicValues.TryGetValue(sourceField, out var dynamicValue))
|
||||||
? dynamicValue
|
return dynamicValue;
|
||||||
|
|
||||||
|
return useEmployeePlaceholder
|
||||||
|
? $"【待员工填写:{definition.VariableName ?? definition.VariableCode}】"
|
||||||
: definition.DefaultValue;
|
: definition.DefaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验任务合同变量的必填状态和员工动态变量数据类型。
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnboardingContractVariableValidationService : ITransient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校验指定任务的全部合同变量快照。
|
||||||
|
/// </summary>
|
||||||
|
Task Validate(long taskId);
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一校验任务合同变量,确保最终提交的数据可以安全用于合同生成。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingContractVariableValidationService
|
||||||
|
: IOnboardingContractVariableValidationService
|
||||||
|
{
|
||||||
|
private readonly SqlSugarScope _db;
|
||||||
|
|
||||||
|
public OnboardingContractVariableValidationService()
|
||||||
|
{
|
||||||
|
_db = DbContext.DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Validate(long taskId)
|
||||||
|
{
|
||||||
|
var contracts = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == taskId)
|
||||||
|
.ToListAsync();
|
||||||
|
if (contracts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contractIds = contracts
|
||||||
|
.Select(item => item.Id)
|
||||||
|
.ToList();
|
||||||
|
var templateVersionIds = contracts
|
||||||
|
.Select(item => item.TemplateVersionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var variables = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => contractIds.Contains(item.TaskContractId))
|
||||||
|
.ToListAsync();
|
||||||
|
var definitions = await _db.Queryable<ObTemplateVariable>()
|
||||||
|
.Where(item => templateVersionIds.Contains(item.TemplateVersionId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var contract in contracts)
|
||||||
|
{
|
||||||
|
var contractDefinitions = definitions
|
||||||
|
.Where(item => item.TemplateVersionId == contract.TemplateVersionId)
|
||||||
|
.OrderBy(item => item.SortCode);
|
||||||
|
|
||||||
|
foreach (var definition in contractDefinitions)
|
||||||
|
{
|
||||||
|
var variable = variables.FirstOrDefault(item =>
|
||||||
|
item.TaskContractId == contract.Id
|
||||||
|
&& item.VariableCode == definition.VariableCode);
|
||||||
|
var value = variable?.VariableValue?.Trim();
|
||||||
|
|
||||||
|
if (definition.Required && string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contract.ContractName}”的必填变量“{definition.VariableName}”尚未填写");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value)
|
||||||
|
|| definition.VariableType != ContractVariableType.Employee)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateDataType(contract.ContractName, definition, value);
|
||||||
|
ValidateOptions(contract.ContractName, definition, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateDataType(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition,
|
||||||
|
string value)
|
||||||
|
{
|
||||||
|
var dataType = definition.DataType?.Trim().ToLowerInvariant() ?? "string";
|
||||||
|
switch (dataType)
|
||||||
|
{
|
||||||
|
case "string":
|
||||||
|
return;
|
||||||
|
case "number":
|
||||||
|
var numberIsValid = decimal.TryParse(
|
||||||
|
value,
|
||||||
|
global::System.Globalization.NumberStyles.Number,
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out _);
|
||||||
|
if (!numberIsValid)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须填写数字");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
case "date":
|
||||||
|
var dateIsValid = DateTime.TryParseExact(
|
||||||
|
value,
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
global::System.Globalization.DateTimeStyles.None,
|
||||||
|
out _);
|
||||||
|
if (!dateIsValid)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须填写正确日期");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”数据类型配置不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOptions(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition,
|
||||||
|
string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(definition.OptionsJson))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var options = ParseOptions(contractName, definition);
|
||||||
|
if (options.Contains(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”必须从配置选项中选择");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> ParseOptions(
|
||||||
|
string contractName,
|
||||||
|
ObTemplateVariable definition)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = global::System.Text.Json.JsonDocument.Parse(
|
||||||
|
definition.OptionsJson);
|
||||||
|
if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Array)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
|
||||||
|
var options = document.RootElement
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(GetOptionValue)
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (options.Count == 0)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah(
|
||||||
|
$"合同“{contractName}”的变量“{definition.VariableName}”可选项配置不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOptionValue(
|
||||||
|
global::System.Text.Json.JsonElement option)
|
||||||
|
{
|
||||||
|
if (option.ValueKind == global::System.Text.Json.JsonValueKind.String)
|
||||||
|
return option.GetString()?.Trim();
|
||||||
|
|
||||||
|
if (option.ValueKind == global::System.Text.Json.JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (option.TryGetProperty("value", out var value))
|
||||||
|
return GetJsonText(value);
|
||||||
|
if (option.TryGetProperty("label", out var label))
|
||||||
|
return GetJsonText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetJsonText(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetJsonText(
|
||||||
|
global::System.Text.Json.JsonElement value)
|
||||||
|
{
|
||||||
|
return value.ValueKind switch
|
||||||
|
{
|
||||||
|
global::System.Text.Json.JsonValueKind.String => value.GetString()?.Trim(),
|
||||||
|
global::System.Text.Json.JsonValueKind.Number => value.ToString(),
|
||||||
|
global::System.Text.Json.JsonValueKind.True => bool.TrueString,
|
||||||
|
global::System.Text.Json.JsonValueKind.False => bool.FalseString,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-36
@@ -8,14 +8,17 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
|||||||
private readonly SqlSugarScope _db;
|
private readonly SqlSugarScope _db;
|
||||||
private readonly IOnboardingContractSnapshotService _snapshotService;
|
private readonly IOnboardingContractSnapshotService _snapshotService;
|
||||||
private readonly IOnboardingIdentityValidationService _identityValidationService;
|
private readonly IOnboardingIdentityValidationService _identityValidationService;
|
||||||
|
private readonly IOnboardingContractVariableValidationService _contractVariableValidationService;
|
||||||
|
|
||||||
public OnboardingPublicSubmissionService(
|
public OnboardingPublicSubmissionService(
|
||||||
IOnboardingContractSnapshotService snapshotService,
|
IOnboardingContractSnapshotService snapshotService,
|
||||||
IOnboardingIdentityValidationService identityValidationService)
|
IOnboardingIdentityValidationService identityValidationService,
|
||||||
|
IOnboardingContractVariableValidationService contractVariableValidationService)
|
||||||
{
|
{
|
||||||
_db = DbContext.DB;
|
_db = DbContext.DB;
|
||||||
_snapshotService = snapshotService;
|
_snapshotService = snapshotService;
|
||||||
_identityValidationService = identityValidationService;
|
_identityValidationService = identityValidationService;
|
||||||
|
_contractVariableValidationService = contractVariableValidationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -119,15 +122,12 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
|||||||
{
|
{
|
||||||
var task = await GetEditableTask(input.Token);
|
var task = await GetEditableTask(input.Token);
|
||||||
var info = await _db.Queryable<ObTaskEmployeeInfo>()
|
var info = await _db.Queryable<ObTaskEmployeeInfo>()
|
||||||
.FirstAsync(item => item.TaskId == task.Id);
|
.FirstAsync(item => item.TaskId == task.Id)
|
||||||
var basicInfoIncomplete = info == null
|
?? throw Oops.Bah("请先完整填写基本信息");
|
||||||
|| string.IsNullOrWhiteSpace(info.EmployeeName)
|
|
||||||
|| string.IsNullOrWhiteSpace(info.Phone)
|
// 最终提交必须重新执行全量校验,不能只依赖员工手机端的页面校验。
|
||||||
|| string.IsNullOrWhiteSpace(info.IdCardNumber)
|
OnboardingEmployeeInfoValidator.ValidateAndNormalize(info);
|
||||||
|| string.IsNullOrWhiteSpace(info.BankCardNumber)
|
await _db.Updateable(info).ExecuteCommandAsync();
|
||||||
|| string.IsNullOrWhiteSpace(info.BankName);
|
|
||||||
if (basicInfoIncomplete)
|
|
||||||
throw Oops.Bah("请先完整填写基本信息");
|
|
||||||
|
|
||||||
await _identityValidationService.EnsureNoDuplicate(
|
await _identityValidationService.EnsureNoDuplicate(
|
||||||
task.Id,
|
task.Id,
|
||||||
@@ -148,7 +148,7 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
|||||||
if (hasMissingRequiredMaterial)
|
if (hasMissingRequiredMaterial)
|
||||||
throw Oops.Bah("仍有必传材料未上传");
|
throw Oops.Bah("仍有必传材料未上传");
|
||||||
|
|
||||||
await EnsureRequiredContractVariables(task);
|
await _contractVariableValidationService.Validate(task.Id);
|
||||||
|
|
||||||
var hasUnconfirmedRequiredContract = await _db.Queryable<ObTaskContract>()
|
var hasUnconfirmedRequiredContract = await _db.Queryable<ObTaskContract>()
|
||||||
.AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null);
|
.AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null);
|
||||||
@@ -214,31 +214,6 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
|
|||||||
throw Oops.Bah("该材料已达到最大上传数量");
|
throw Oops.Bah("该材料已达到最大上传数量");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureRequiredContractVariables(ObOnboardingTask task)
|
|
||||||
{
|
|
||||||
var contracts = await _db.Queryable<ObTaskContract>()
|
|
||||||
.Where(item => item.TaskId == task.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
var contractIds = contracts.Select(contract => contract.Id).ToList();
|
|
||||||
var templateVersionIds = contracts.Select(contract => contract.TemplateVersionId).ToList();
|
|
||||||
var variables = await _db.Queryable<ObTaskContractVariable>()
|
|
||||||
.Where(item => contractIds.Contains(item.TaskContractId))
|
|
||||||
.ToListAsync();
|
|
||||||
var definitions = await _db.Queryable<ObTemplateVariable>()
|
|
||||||
.Where(item => templateVersionIds.Contains(item.TemplateVersionId) && item.Required)
|
|
||||||
.ToListAsync();
|
|
||||||
var missing = contracts
|
|
||||||
.SelectMany(contract => definitions
|
|
||||||
.Where(definition => definition.TemplateVersionId == contract.TemplateVersionId)
|
|
||||||
.Select(definition => new { contract, definition }))
|
|
||||||
.FirstOrDefault(item => string.IsNullOrWhiteSpace(
|
|
||||||
variables.FirstOrDefault(variable => variable.TaskContractId == item.contract.Id
|
|
||||||
&& variable.VariableCode == item.definition.VariableCode)
|
|
||||||
?.VariableValue));
|
|
||||||
if (missing != null)
|
|
||||||
throw Oops.Bah($"合同“{missing.contract.ContractName}”的必填变量“{missing.definition.VariableName}”尚未填写");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<List<ObTaskMaterialRequirement>> GetMaterialRequirements(long taskId)
|
private Task<List<ObTaskMaterialRequirement>> GetMaterialRequirements(long taskId)
|
||||||
{
|
{
|
||||||
return _db.Queryable<ObTaskMaterialRequirement>()
|
return _db.Queryable<ObTaskMaterialRequirement>()
|
||||||
|
|||||||
+295
@@ -0,0 +1,295 @@
|
|||||||
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一校验并规范化员工端固定入职信息。
|
||||||
|
/// </summary>
|
||||||
|
internal static class OnboardingEmployeeInfoValidator
|
||||||
|
{
|
||||||
|
private static readonly int[] IdCardWeights =
|
||||||
|
{
|
||||||
|
7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly char[] IdCardCheckCodes =
|
||||||
|
{
|
||||||
|
'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规范化文本,并校验固定登记表字段的格式。
|
||||||
|
/// </summary>
|
||||||
|
public static void ValidateAndNormalize(ObTaskEmployeeInfo input)
|
||||||
|
{
|
||||||
|
input.EmployeeName = input.EmployeeName?.Trim();
|
||||||
|
input.Phone = input.Phone?.Trim();
|
||||||
|
input.Email = input.Email?.Trim();
|
||||||
|
input.IdCardNumber = input.IdCardNumber?.Trim().ToUpperInvariant();
|
||||||
|
input.BankCardNumber = global::System.Text.RegularExpressions.Regex.Replace(
|
||||||
|
input.BankCardNumber ?? string.Empty,
|
||||||
|
"[\\s-]",
|
||||||
|
string.Empty);
|
||||||
|
input.BankName = input.BankName?.Trim();
|
||||||
|
input.BankAccountName = input.EmployeeName;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.EmployeeName) || input.EmployeeName.Length > 100)
|
||||||
|
throw Oops.Bah("员工姓名不能为空且不能超过 100 个字符");
|
||||||
|
|
||||||
|
if (input.Gender != "男" && input.Gender != "女")
|
||||||
|
throw Oops.Bah("请选择正确的性别");
|
||||||
|
|
||||||
|
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
input.Phone ?? string.Empty,
|
||||||
|
"^1[3-9]\\d{9}$"))
|
||||||
|
{
|
||||||
|
throw Oops.Bah("请输入正确的 11 位手机号码");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryValidateIdCard(input.IdCardNumber, out var idCardBirthday, out var idCardError))
|
||||||
|
throw Oops.Bah(idCardError);
|
||||||
|
|
||||||
|
ValidateBirthday(input.Birthday, idCardBirthday);
|
||||||
|
ValidateOptionalEmail(input.Email);
|
||||||
|
ValidateOptionalNumber(input.Height, "身高", 100M, 250M, "厘米");
|
||||||
|
ValidateOptionalNumber(input.Weight, "体重", 30M, 300M, "公斤");
|
||||||
|
ValidateBankCard(input.BankCardNumber);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(input.BankName) || input.BankName.Length > 100)
|
||||||
|
throw Oops.Bah("开户银行名称不能为空且不能超过 100 个字符");
|
||||||
|
|
||||||
|
ValidateOptionalContactPhone(input.EmergencyPhone);
|
||||||
|
ValidateEducationHistory(input.EducationHistoryJson);
|
||||||
|
ValidateWorkHistory(input.WorkHistoryJson);
|
||||||
|
ValidateFamilyMembers(input.FamilyMembersJson);
|
||||||
|
ValidateJson(input.CustomVariablesJson, global::System.Text.Json.JsonValueKind.Object, "员工补充变量");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryValidateIdCard(
|
||||||
|
string idCardNumber,
|
||||||
|
out DateTime birthday,
|
||||||
|
out string errorMessage)
|
||||||
|
{
|
||||||
|
birthday = default;
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
var structureIsValid = global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
idCardNumber ?? string.Empty,
|
||||||
|
"^\\d{17}[0-9X]$");
|
||||||
|
if (!structureIsValid)
|
||||||
|
{
|
||||||
|
errorMessage = "请输入正确的 18 位身份证号码";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var birthdayText = idCardNumber.Substring(6, 8);
|
||||||
|
var birthdayIsValid = DateTime.TryParseExact(
|
||||||
|
birthdayText,
|
||||||
|
"yyyyMMdd",
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
global::System.Globalization.DateTimeStyles.None,
|
||||||
|
out birthday);
|
||||||
|
if (!birthdayIsValid || birthday.Date > DateTime.Today)
|
||||||
|
{
|
||||||
|
errorMessage = "身份证号码中的出生日期不正确";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
for (var index = 0; index < IdCardWeights.Length; index++)
|
||||||
|
{
|
||||||
|
total += (idCardNumber[index] - '0') * IdCardWeights[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IdCardCheckCodes[total % 11] == idCardNumber[17])
|
||||||
|
return true;
|
||||||
|
|
||||||
|
errorMessage = "身份证号码校验位不正确";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBirthday(string birthdayText, DateTime idCardBirthday)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(birthdayText))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var birthdayIsValid = DateTime.TryParseExact(
|
||||||
|
birthdayText.Trim(),
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
global::System.Globalization.DateTimeStyles.None,
|
||||||
|
out var birthday);
|
||||||
|
if (!birthdayIsValid || birthday.Date > DateTime.Today)
|
||||||
|
throw Oops.Bah("请输入正确的出生日期");
|
||||||
|
|
||||||
|
if (birthday.Date != idCardBirthday.Date)
|
||||||
|
throw Oops.Bah("出生日期与身份证号码不一致");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOptionalEmail(string email)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var emailIsValid = email.Length <= 100
|
||||||
|
&& global::System.Net.Mail.MailAddress.TryCreate(email, out _);
|
||||||
|
if (!emailIsValid)
|
||||||
|
throw Oops.Bah("请输入正确的电子邮箱");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOptionalNumber(
|
||||||
|
string value,
|
||||||
|
string fieldName,
|
||||||
|
decimal min,
|
||||||
|
decimal max,
|
||||||
|
string unit)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var numberIsValid = decimal.TryParse(
|
||||||
|
value.Trim(),
|
||||||
|
global::System.Globalization.NumberStyles.Number,
|
||||||
|
global::System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var number);
|
||||||
|
if (!numberIsValid || number < min || number > max)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"{fieldName}应为 {min:0} 至 {max:0} {unit}之间的数字");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBankCard(string bankCardNumber)
|
||||||
|
{
|
||||||
|
var bankCardIsValid = global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
bankCardNumber ?? string.Empty,
|
||||||
|
"^\\d{12,19}$");
|
||||||
|
if (!bankCardIsValid)
|
||||||
|
throw Oops.Bah("请输入 12 至 19 位银行卡号");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOptionalContactPhone(string phone)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(phone))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contactPhoneIsValid = global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
phone.Trim(),
|
||||||
|
"^(1[3-9]\\d{9}|0\\d{2,3}-?\\d{7,8})$");
|
||||||
|
if (!contactPhoneIsValid)
|
||||||
|
throw Oops.Bah("请输入正确的紧急联系人手机号码或座机号码");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateEducationHistory(string json)
|
||||||
|
{
|
||||||
|
ValidateHistoryRows(json, "教育经历", (row, rowNumber) =>
|
||||||
|
{
|
||||||
|
GetRequiredRowValue(row, "timeRange", rowNumber, "教育经历", "时间范围");
|
||||||
|
GetRequiredRowValue(row, "location", rowNumber, "教育经历", "学校或机构");
|
||||||
|
GetRequiredRowValue(row, "content", rowNumber, "教育经历", "专业或学习内容");
|
||||||
|
GetRequiredRowValue(row, "result", rowNumber, "教育经历", "结果或学位");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateWorkHistory(string json)
|
||||||
|
{
|
||||||
|
ValidateHistoryRows(json, "工作经历", (row, rowNumber) =>
|
||||||
|
{
|
||||||
|
GetRequiredRowValue(row, "timeRange", rowNumber, "工作经历", "起止时间");
|
||||||
|
GetRequiredRowValue(row, "companyName", rowNumber, "工作经历", "工作单位");
|
||||||
|
GetRequiredRowValue(row, "position", rowNumber, "工作经历", "担任职务");
|
||||||
|
GetRequiredRowValue(row, "leaveReason", rowNumber, "工作经历", "离职原因");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateFamilyMembers(string json)
|
||||||
|
{
|
||||||
|
ValidateHistoryRows(json, "家庭成员", (row, rowNumber) =>
|
||||||
|
{
|
||||||
|
GetRequiredRowValue(row, "name", rowNumber, "家庭成员", "姓名");
|
||||||
|
GetRequiredRowValue(row, "relationship", rowNumber, "家庭成员", "关系");
|
||||||
|
var ageText = GetRequiredRowValue(row, "age", rowNumber, "家庭成员", "年龄");
|
||||||
|
var phone = GetRequiredRowValue(row, "phone", rowNumber, "家庭成员", "联系电话");
|
||||||
|
|
||||||
|
var ageIsValid = int.TryParse(ageText, out var age) && age >= 0 && age <= 120;
|
||||||
|
if (!ageIsValid)
|
||||||
|
throw Oops.Bah($"第 {rowNumber} 条家庭成员的年龄应为 0 至 120 之间的整数");
|
||||||
|
|
||||||
|
var phoneIsValid = global::System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
|
phone,
|
||||||
|
"^(1[3-9]\\d{9}|0\\d{2,3}-?\\d{7,8})$");
|
||||||
|
if (!phoneIsValid)
|
||||||
|
throw Oops.Bah($"第 {rowNumber} 条家庭成员的联系电话格式不正确");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateHistoryRows(
|
||||||
|
string json,
|
||||||
|
string fieldName,
|
||||||
|
Action<global::System.Text.Json.JsonElement, int> validateRow)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = global::System.Text.Json.JsonDocument.Parse(json);
|
||||||
|
if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Array)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
|
||||||
|
var rowNumber = 0;
|
||||||
|
foreach (var row in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
rowNumber++;
|
||||||
|
if (row.ValueKind != global::System.Text.Json.JsonValueKind.Object)
|
||||||
|
throw Oops.Bah($"第 {rowNumber} 条{fieldName}格式不正确,请删除后重新添加");
|
||||||
|
|
||||||
|
validateRow(row, rowNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"{fieldName}格式不正确,请刷新页面后重新填写");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRequiredRowValue(
|
||||||
|
global::System.Text.Json.JsonElement row,
|
||||||
|
string propertyName,
|
||||||
|
int rowNumber,
|
||||||
|
string fieldName,
|
||||||
|
string displayName)
|
||||||
|
{
|
||||||
|
if (!row.TryGetProperty(propertyName, out var property)
|
||||||
|
|| property.ValueKind is global::System.Text.Json.JsonValueKind.Null
|
||||||
|
or global::System.Text.Json.JsonValueKind.Undefined)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"第 {rowNumber} 条{fieldName}请填写{displayName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = property.ValueKind == global::System.Text.Json.JsonValueKind.String
|
||||||
|
? property.GetString()
|
||||||
|
: property.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
throw Oops.Bah($"第 {rowNumber} 条{fieldName}请填写{displayName}");
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateJson(
|
||||||
|
string json,
|
||||||
|
global::System.Text.Json.JsonValueKind expectedKind,
|
||||||
|
string fieldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = global::System.Text.Json.JsonDocument.Parse(json);
|
||||||
|
if (document.RootElement.ValueKind != expectedKind)
|
||||||
|
throw new global::System.Text.Json.JsonException();
|
||||||
|
}
|
||||||
|
catch (global::System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
throw Oops.Bah($"{fieldName}格式不正确,请刷新页面后重新填写");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-41
@@ -131,7 +131,7 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
|||||||
throw Oops.Bah("当前任务不可编辑");
|
throw Oops.Bah("当前任务不可编辑");
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateEmployeeInfo(input);
|
OnboardingEmployeeInfoValidator.ValidateAndNormalize(input);
|
||||||
await _identityValidationService.EnsureNoDuplicate(
|
await _identityValidationService.EnsureNoDuplicate(
|
||||||
task.Id,
|
task.Id,
|
||||||
input.EmployeeName,
|
input.EmployeeName,
|
||||||
@@ -170,46 +170,6 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
|
|||||||
await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true);
|
await _snapshotService.Build(task.Id, entity, invalidateGeneratedPreview: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 校验候选人核心身份信息,避免非法格式进入合同和正式员工档案。
|
|
||||||
/// </summary>
|
|
||||||
private static void ValidateEmployeeInfo(OnboardingTaskPublicSaveInput input)
|
|
||||||
{
|
|
||||||
input.EmployeeName = input.EmployeeName?.Trim();
|
|
||||||
input.Phone = input.Phone?.Trim();
|
|
||||||
input.IdCardNumber = input.IdCardNumber?.Trim().ToUpperInvariant();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(input.EmployeeName) || input.EmployeeName.Length > 100)
|
|
||||||
throw Oops.Bah("员工姓名不能为空且不能超过 100 个字符");
|
|
||||||
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
|
|
||||||
input.Phone ?? string.Empty,
|
|
||||||
"^1[3-9]\\d{9}$"))
|
|
||||||
{
|
|
||||||
throw Oops.Bah("请输入正确的 11 位手机号码");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!global::System.Text.RegularExpressions.Regex.IsMatch(
|
|
||||||
input.IdCardNumber ?? string.Empty,
|
|
||||||
"^\\d{17}[0-9X]$"))
|
|
||||||
{
|
|
||||||
throw Oops.Bah("请输入正确的 18 位身份证号码");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(input.CustomVariablesJson))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var document = global::System.Text.Json.JsonDocument.Parse(input.CustomVariablesJson);
|
|
||||||
if (document.RootElement.ValueKind != global::System.Text.Json.JsonValueKind.Object)
|
|
||||||
throw new global::System.Text.Json.JsonException();
|
|
||||||
}
|
|
||||||
catch (global::System.Text.Json.JsonException)
|
|
||||||
{
|
|
||||||
throw Oops.Bah("员工补充变量格式不正确,请刷新页面后重新填写");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据员工当前已填写信息校验变量并提前生成合同 DOCX。
|
/// 根据员工当前已填写信息校验变量并提前生成合同 DOCX。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,6 +49,41 @@ public class OnboardingTaskCreateInput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 业务备注。
|
/// 业务备注。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "人事备注不能为空")]
|
||||||
|
public string Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量复制入职任务参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskCopyInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 被复制的原入职任务 Id。
|
||||||
|
/// </summary>
|
||||||
|
[IdNotNull(ErrorMessage = "原入职任务Id不能为空")]
|
||||||
|
public long SourceTaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在原任务之外需要新建的任务数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每条新任务分别填写的人事备注。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTaskCopyItemInput> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条复制任务的个性化参数。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskCopyItemInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 新任务的人事备注,用于区分候选人和专属链接。
|
||||||
|
/// </summary>
|
||||||
|
[Required(ErrorMessage = "人事备注不能为空")]
|
||||||
public string Remark { get; set; }
|
public string Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,4 +133,14 @@ public class OnboardingTaskCreatedOutput
|
|||||||
/// 员工填写链接过期时间。
|
/// 员工填写链接过期时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime ExpiresAt { get; set; }
|
public DateTime ExpiresAt { get; set; }
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 批量复制入职任务结果。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskCopyOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本次复制生成的全部新任务及独立链接信息。
|
||||||
|
/// </summary>
|
||||||
|
public List<OnboardingTaskCreatedOutput> Tasks { get; set; } = new();
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ public interface IOnboardingTaskService : ITransient
|
|||||||
/// 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
|
/// 校验公司、岗位和合同范围后创建入职任务、链接及任务合同。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input);
|
Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制原任务的业务配置和人事参数,批量创建独立的新入职任务。
|
||||||
|
/// </summary>
|
||||||
|
Task<OnboardingTaskCopyOutput> Copy(OnboardingTaskCopyInput input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input)
|
public async Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input.Remark))
|
||||||
|
throw Oops.Bah("请填写人事备注");
|
||||||
|
|
||||||
var companyEnabled = await _db.Queryable<ObCompany>()
|
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||||
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
|
||||||
if (!companyEnabled)
|
if (!companyEnabled)
|
||||||
@@ -121,7 +124,7 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
AssignedReviewerUserId = input.AssignedReviewerUserId,
|
AssignedReviewerUserId = input.AssignedReviewerUserId,
|
||||||
LinkExpireAt = expiresAt,
|
LinkExpireAt = expiresAt,
|
||||||
TaskStatus = OnboardingTaskStatus.PendingFill,
|
TaskStatus = OnboardingTaskStatus.PendingFill,
|
||||||
Remark = input.Remark
|
Remark = input.Remark.Trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
ValidateInitialContractDateRanges(input.ContractVariables);
|
ValidateInitialContractDateRanges(input.ContractVariables);
|
||||||
@@ -207,6 +210,230 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制原任务锁定的业务配置与人事参数,并原子化创建多条独立任务。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<OnboardingTaskCopyOutput> Copy(OnboardingTaskCopyInput input)
|
||||||
|
{
|
||||||
|
const int maxCopyCount = 50;
|
||||||
|
if (input.Count < 1 || input.Count > maxCopyCount)
|
||||||
|
throw Oops.Bah($"复制数量必须在1到{maxCopyCount}之间");
|
||||||
|
|
||||||
|
input.Items ??= new List<OnboardingTaskCopyItemInput>();
|
||||||
|
if (input.Items.Count != input.Count)
|
||||||
|
throw Oops.Bah("复制数量与人事备注数量不一致");
|
||||||
|
|
||||||
|
var missingRemarkIndex = input.Items.FindIndex(item =>
|
||||||
|
string.IsNullOrWhiteSpace(item.Remark));
|
||||||
|
if (missingRemarkIndex >= 0)
|
||||||
|
throw Oops.Bah($"请填写第{missingRemarkIndex + 1}份任务的人事备注");
|
||||||
|
|
||||||
|
var sourceTask = await _db.Queryable<ObOnboardingTask>()
|
||||||
|
.InSingleAsync(input.SourceTaskId)
|
||||||
|
?? throw Oops.Bah("原入职任务不存在");
|
||||||
|
if (!sourceTask.DepartmentId.HasValue)
|
||||||
|
throw Oops.Bah("原入职任务未设置部门,不能复制");
|
||||||
|
|
||||||
|
var companyEnabled = await _db.Queryable<ObCompany>()
|
||||||
|
.AnyAsync(item => item.Id == sourceTask.CompanyId && item.Status == "enable");
|
||||||
|
if (!companyEnabled)
|
||||||
|
throw Oops.Bah("原任务所属公司不存在或未启用");
|
||||||
|
|
||||||
|
var departmentEnabled = await _db.Queryable<ObDepartment>()
|
||||||
|
.AnyAsync(item => item.Id == sourceTask.DepartmentId.Value
|
||||||
|
&& item.CompanyId == sourceTask.CompanyId
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (!departmentEnabled)
|
||||||
|
throw Oops.Bah("原任务所属部门不存在或未启用");
|
||||||
|
|
||||||
|
var positionEnabled = await _db.Queryable<ObPosition>()
|
||||||
|
.AnyAsync(item => item.Id == sourceTask.PositionId
|
||||||
|
&& item.CompanyId == sourceTask.CompanyId
|
||||||
|
&& item.DepartmentId == sourceTask.DepartmentId.Value
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (!positionEnabled)
|
||||||
|
throw Oops.Bah("原任务所属岗位不存在或未启用");
|
||||||
|
|
||||||
|
var onboardingTemplateEnabled = await _db.Queryable<ObOnboardingTemplate>()
|
||||||
|
.AnyAsync(item => item.Id == sourceTask.OnboardingTemplateId
|
||||||
|
&& item.CompanyId == sourceTask.CompanyId
|
||||||
|
&& item.DepartmentId == sourceTask.DepartmentId.Value
|
||||||
|
&& item.Status == "enable");
|
||||||
|
if (!onboardingTemplateEnabled)
|
||||||
|
throw Oops.Bah("原任务使用的入职模板不存在或未启用");
|
||||||
|
|
||||||
|
var sourceContracts = await _db.Queryable<ObTaskContract>()
|
||||||
|
.Where(item => item.TaskId == sourceTask.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (sourceContracts.Count == 0)
|
||||||
|
throw Oops.Bah("原入职任务没有合同快照,不能复制");
|
||||||
|
|
||||||
|
var versionIds = sourceContracts
|
||||||
|
.Select(item => item.TemplateVersionId)
|
||||||
|
.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 sourceMaterialRequirements = await _db.Queryable<ObTaskMaterialRequirement>()
|
||||||
|
.Where(item => item.TaskId == sourceTask.Id)
|
||||||
|
.OrderBy(item => item.SortCode)
|
||||||
|
.ToListAsync();
|
||||||
|
var sourceContractIds = sourceContracts.Select(item => item.Id).ToList();
|
||||||
|
var sourceManualVariables = await _db.Queryable<ObTaskContractVariable>()
|
||||||
|
.Where(item => sourceContractIds.Contains(item.TaskContractId)
|
||||||
|
&& item.SourceType == ContractVariableSourceType.HrInput
|
||||||
|
&& item.IsManualOverride)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var initialContractVariables = (
|
||||||
|
from contract in sourceContracts
|
||||||
|
join variable in sourceManualVariables
|
||||||
|
on contract.Id equals variable.TaskContractId
|
||||||
|
select new OnboardingInitialContractVariableInput
|
||||||
|
{
|
||||||
|
ContractTemplateVersionId = contract.TemplateVersionId,
|
||||||
|
VariableCode = variable.VariableCode,
|
||||||
|
VariableValue = variable.VariableValue
|
||||||
|
}).ToList();
|
||||||
|
ValidateInitialContractDateRanges(initialContractVariables);
|
||||||
|
|
||||||
|
var expiryConfig = await _configService.GetByConfigKey(
|
||||||
|
CateGoryConst.CONFIG_BIZ_DEFINE,
|
||||||
|
"onboard.link.expireHours");
|
||||||
|
if (expiryConfig == null
|
||||||
|
|| !int.TryParse(expiryConfig.ConfigValue, out var expiryHours)
|
||||||
|
|| expiryHours <= 0)
|
||||||
|
{
|
||||||
|
throw Oops.Bah("请在系统配置中设置 onboard.link.expireHours(正整数小时)");
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputs = new List<OnboardingTaskCreatedOutput>();
|
||||||
|
await _db.Ado.BeginTranAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in input.Items)
|
||||||
|
{
|
||||||
|
var output = await CreateCopiedTask(
|
||||||
|
sourceTask,
|
||||||
|
sourceContracts,
|
||||||
|
sourceMaterialRequirements,
|
||||||
|
initialContractVariables,
|
||||||
|
expiryHours,
|
||||||
|
item.Remark.Trim());
|
||||||
|
outputs.Add(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.Ado.CommitTranAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _db.Ado.RollbackTranAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OnboardingTaskCopyOutput
|
||||||
|
{
|
||||||
|
Tasks = outputs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据原任务快照创建一条全新的待填写任务,不复制任何候选人业务数据。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<OnboardingTaskCreatedOutput> CreateCopiedTask(
|
||||||
|
ObOnboardingTask sourceTask,
|
||||||
|
List<ObTaskContract> sourceContracts,
|
||||||
|
List<ObTaskMaterialRequirement> sourceMaterialRequirements,
|
||||||
|
List<OnboardingInitialContractVariableInput> initialContractVariables,
|
||||||
|
int expiryHours,
|
||||||
|
string remark)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
|
||||||
|
var expiresAt = now.AddHours(expiryHours);
|
||||||
|
var token = Guid.NewGuid().ToString("N");
|
||||||
|
var task = new ObOnboardingTask
|
||||||
|
{
|
||||||
|
Id = taskId,
|
||||||
|
TaskNo = $"OB{now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}",
|
||||||
|
CompanyId = sourceTask.CompanyId,
|
||||||
|
DepartmentId = sourceTask.DepartmentId,
|
||||||
|
PositionId = sourceTask.PositionId,
|
||||||
|
OnboardingTemplateId = sourceTask.OnboardingTemplateId,
|
||||||
|
AssignedReviewerUserId = sourceTask.AssignedReviewerUserId,
|
||||||
|
LinkExpireAt = expiresAt,
|
||||||
|
TaskStatus = OnboardingTaskStatus.PendingFill,
|
||||||
|
Remark = remark
|
||||||
|
};
|
||||||
|
|
||||||
|
await _db.Insertable(task).ExecuteCommandAsync();
|
||||||
|
await _db.Insertable(new ObTaskLink
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
TaskId = taskId,
|
||||||
|
PlainToken = token,
|
||||||
|
TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(
|
||||||
|
global::System.Text.Encoding.UTF8.GetBytes(token))),
|
||||||
|
ExpiresAt = expiresAt
|
||||||
|
}).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
await _db.Insertable(new ObTaskEmployeeInfo
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
TaskId = taskId,
|
||||||
|
Status = CommonStatusConst.ENABLE,
|
||||||
|
CreateTime = now
|
||||||
|
}).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
var copiedContracts = sourceContracts.Select(source => new ObTaskContract
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
TaskId = taskId,
|
||||||
|
TemplateId = source.TemplateId,
|
||||||
|
TemplateVersionId = source.TemplateVersionId,
|
||||||
|
ContractName = source.ContractName,
|
||||||
|
ContractType = source.ContractType,
|
||||||
|
Required = source.Required,
|
||||||
|
PrintCopies = source.PrintCopies
|
||||||
|
}).ToList();
|
||||||
|
await _db.Insertable(copiedContracts).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
if (sourceMaterialRequirements.Count > 0)
|
||||||
|
{
|
||||||
|
var copiedRequirements = sourceMaterialRequirements.Select(source =>
|
||||||
|
new ObTaskMaterialRequirement
|
||||||
|
{
|
||||||
|
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
|
||||||
|
TaskId = taskId,
|
||||||
|
MaterialTypeId = source.MaterialTypeId,
|
||||||
|
MaterialCode = source.MaterialCode,
|
||||||
|
MaterialName = source.MaterialName,
|
||||||
|
AllowedExtensions = source.AllowedExtensions,
|
||||||
|
MaxFileCount = source.MaxFileCount,
|
||||||
|
MaxFileSizeMb = source.MaxFileSizeMb,
|
||||||
|
Required = source.Required,
|
||||||
|
SortCode = source.SortCode
|
||||||
|
}).ToList();
|
||||||
|
await _db.Insertable(copiedRequirements).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成公司、系统和空员工变量,再覆盖原任务的人事参数。
|
||||||
|
await _snapshotService.Build(taskId);
|
||||||
|
await ApplyInitialContractVariables(initialContractVariables, copiedContracts);
|
||||||
|
|
||||||
|
return new OnboardingTaskCreatedOutput
|
||||||
|
{
|
||||||
|
TaskId = taskId,
|
||||||
|
TaskNo = task.TaskNo,
|
||||||
|
LinkToken = token,
|
||||||
|
ExpiresAt = expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验并保存任务创建时填写的人事合同变量。
|
/// 校验并保存任务创建时填写的人事合同变量。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -289,7 +516,7 @@ public class OnboardingTaskService : IOnboardingTaskService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验人事创建任务时填写的合同日期先后关系。
|
/// 校验人事创建任务时填写的合同日期先后关系。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ValidateInitialContractDateRanges(
|
internal static void ValidateInitialContractDateRanges(
|
||||||
List<OnboardingInitialContractVariableInput> inputs)
|
List<OnboardingInitialContractVariableInput> inputs)
|
||||||
{
|
{
|
||||||
inputs ??= new List<OnboardingInitialContractVariableInput>();
|
inputs ??= new List<OnboardingInitialContractVariableInput>();
|
||||||
|
|||||||
+59
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace SimpleAdmin.Application;
|
namespace SimpleAdmin.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 入职任务分页查询参数。
|
/// 入职任务分页查询参数。
|
||||||
@@ -24,6 +24,64 @@ public class OnboardingTaskPageInput : BasePageInput
|
|||||||
/// 入职业务部门 Id。
|
/// 入职业务部门 Id。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long? DepartmentId { get; set; }
|
public long? DepartmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职业务岗位 Id。
|
||||||
|
/// </summary>
|
||||||
|
public long? PositionId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务分页数据及当前查询条件下的状态统计。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskPageOutput : SqlSugarPagedList<ObOnboardingTask>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 不受状态筛选影响的任务状态统计。
|
||||||
|
/// </summary>
|
||||||
|
public OnboardingTaskStatisticsOutput Statistics { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 入职任务状态统计。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskStatisticsOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待候选人填写数量。
|
||||||
|
/// </summary>
|
||||||
|
public int PendingFill { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待人事审核数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Reviewing { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已退回待修改数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Rejected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过或已归档数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Approved { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库按任务状态分组后的内部统计行。
|
||||||
|
/// </summary>
|
||||||
|
public class OnboardingTaskStatusCountOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务状态。
|
||||||
|
/// </summary>
|
||||||
|
public string TaskStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该状态的任务数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ public interface IOnboardingTaskQueryService : ITransient
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询后台入职任务。
|
/// 分页查询后台入职任务。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<SqlSugarPagedList<ObOnboardingTask>> Page(OnboardingTaskPageInput input);
|
Task<OnboardingTaskPageOutput> Page(OnboardingTaskPageInput input);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。
|
/// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。
|
||||||
|
|||||||
+64
-8
@@ -17,22 +17,78 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询后台入职任务。
|
/// 分页查询后台入职任务。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<SqlSugarPagedList<ObOnboardingTask>> Page(OnboardingTaskPageInput input)
|
public async Task<OnboardingTaskPageOutput> Page(OnboardingTaskPageInput input)
|
||||||
{
|
{
|
||||||
|
const string approvedGroupStatus = "approved_or_archived";
|
||||||
|
var filterApprovedGroup = string.Equals(
|
||||||
|
input.TaskStatus,
|
||||||
|
approvedGroupStatus,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var list = await _db.Queryable<ObOnboardingTask>()
|
var list = await _db.Queryable<ObOnboardingTask>()
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo))
|
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo),
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus), item => item.TaskStatus == input.TaskStatus)
|
item => item.TaskNo.Contains(input.TaskNo))
|
||||||
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
|
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus) && !filterApprovedGroup,
|
||||||
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
|
item => item.TaskStatus == input.TaskStatus)
|
||||||
|
.WhereIF(filterApprovedGroup,
|
||||||
|
item => item.TaskStatus == OnboardingTaskStatus.Approved
|
||||||
|
|| item.TaskStatus == OnboardingTaskStatus.Archived)
|
||||||
|
.WhereIF(input.CompanyId.HasValue,
|
||||||
|
item => item.CompanyId == input.CompanyId.Value)
|
||||||
|
.WhereIF(input.DepartmentId.HasValue,
|
||||||
|
item => item.DepartmentId == input.DepartmentId.Value)
|
||||||
|
.WhereIF(input.PositionId.HasValue,
|
||||||
|
item => item.PositionId == input.PositionId.Value)
|
||||||
.OrderByDescending(item => item.CreateTime)
|
.OrderByDescending(item => item.CreateTime)
|
||||||
.ToPagedListAsync(input.PageNum, input.PageSize);
|
.ToPagedListAsync(input.PageNum, input.PageSize);
|
||||||
|
|
||||||
if (list.List != null && list.List.Any())
|
if (list.List != null && list.List.Any())
|
||||||
{
|
|
||||||
await FillTaskMetadata(list.List);
|
await FillTaskMetadata(list.List);
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
// 统计继续应用组织、岗位和任务编号条件,但不受当前状态筛选影响。
|
||||||
|
var statusRows = await _db.Queryable<ObOnboardingTask>()
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo),
|
||||||
|
item => item.TaskNo.Contains(input.TaskNo))
|
||||||
|
.WhereIF(input.CompanyId.HasValue,
|
||||||
|
item => item.CompanyId == input.CompanyId.Value)
|
||||||
|
.WhereIF(input.DepartmentId.HasValue,
|
||||||
|
item => item.DepartmentId == input.DepartmentId.Value)
|
||||||
|
.WhereIF(input.PositionId.HasValue,
|
||||||
|
item => item.PositionId == input.PositionId.Value)
|
||||||
|
.GroupBy(item => item.TaskStatus)
|
||||||
|
.Select(item => new OnboardingTaskStatusCountOutput
|
||||||
|
{
|
||||||
|
TaskStatus = item.TaskStatus,
|
||||||
|
Count = SqlFunc.AggregateCount(item.Id)
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
var statusCountMap = statusRows.ToDictionary(
|
||||||
|
item => item.TaskStatus,
|
||||||
|
item => item.Count);
|
||||||
|
|
||||||
|
return new OnboardingTaskPageOutput
|
||||||
|
{
|
||||||
|
PageNum = list.PageNum,
|
||||||
|
PageSize = list.PageSize,
|
||||||
|
Total = list.Total,
|
||||||
|
Pages = list.Pages,
|
||||||
|
List = list.List,
|
||||||
|
HasPrevPages = list.HasPrevPages,
|
||||||
|
HasNextPages = list.HasNextPages,
|
||||||
|
Statistics = new OnboardingTaskStatisticsOutput
|
||||||
|
{
|
||||||
|
PendingFill = statusCountMap.GetValueOrDefault(
|
||||||
|
OnboardingTaskStatus.PendingFill),
|
||||||
|
Reviewing = statusCountMap.GetValueOrDefault(
|
||||||
|
OnboardingTaskStatus.Reviewing),
|
||||||
|
Rejected = statusCountMap.GetValueOrDefault(
|
||||||
|
OnboardingTaskStatus.Rejected),
|
||||||
|
Approved = statusCountMap.GetValueOrDefault(
|
||||||
|
OnboardingTaskStatus.Approved)
|
||||||
|
+ statusCountMap.GetValueOrDefault(
|
||||||
|
OnboardingTaskStatus.Archived)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+17
@@ -18,6 +18,23 @@ public class OnboardingContractDocumentController
|
|||||||
_service = service;
|
_service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建入职任务前,按人事当前填写的参数下载 DOCX 合同预览。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("preview-before-create")]
|
||||||
|
[DisplayName("创建入职任务前预览合同")]
|
||||||
|
public async Task<FileContentResult> PreviewBeforeCreate(
|
||||||
|
[FromBody] OnboardingContractPreviewInput input)
|
||||||
|
{
|
||||||
|
var preview = await _service.PreviewBeforeCreate(input);
|
||||||
|
return new FileContentResult(
|
||||||
|
preview.FileBytes,
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
||||||
|
{
|
||||||
|
FileDownloadName = preview.FileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成入职合同文件
|
/// 生成入职合同文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace SimpleAdmin.Web.Core;
|
namespace SimpleAdmin.Web.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -25,4 +26,14 @@ public class OnboardingTaskController
|
|||||||
{
|
{
|
||||||
return await _service.Create(input);
|
return await _service.Create(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在原任务之外批量复制独立的新入职任务。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("copy")]
|
||||||
|
[DisplayName("复制入职任务")]
|
||||||
|
public async Task<dynamic> Copy([FromBody] OnboardingTaskCopyInput input)
|
||||||
|
{
|
||||||
|
return await _service.Copy(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user