入职任务预览,入职任务cope,验证填入的信息

This commit is contained in:
fuenmao
2026-08-29 17:26:29 +08:00
parent 1d42c52d53
commit 6151872b6b
17 changed files with 1215 additions and 95 deletions
@@ -11,3 +11,58 @@ public class OnboardingGenerateContractInput
[IdNotNull(ErrorMessage = "任务合同Id不能为空")]
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; }
}
@@ -5,6 +5,12 @@ namespace SimpleAdmin.Application;
/// </summary>
public interface IOnboardingContractDocumentService : ITransient
{
/// <summary>
/// 创建入职任务前,根据人事当前填写的参数生成临时 DOCX 预览。
/// </summary>
Task<OnboardingContractPreviewOutput> PreviewBeforeCreate(
OnboardingContractPreviewInput input);
/// <summary>
/// 根据已审核通过的任务合同生成并保存 DOCX 文件。
/// </summary>
@@ -14,6 +14,177 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
_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>
/// 根据锁定的模板版本和任务变量快照生成已替换变量的合同 DOCX。
/// </summary>
@@ -86,4 +257,30 @@ public class OnboardingContractDocumentService : IOnboardingContractDocumentServ
await _db.Updateable(contract).ExecuteCommandAsync();
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";
}
}
@@ -110,12 +110,13 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
};
}
private static string ResolveValue(
internal static string ResolveValue(
ObTemplateVariable definition,
ObTaskEmployeeInfo employeeInfo,
IReadOnlyDictionary<string, string> dynamicValues,
ObCompany company,
ObOnboardingTask task)
ObOnboardingTask task,
bool useEmployeePlaceholder = false)
{
var sourceField = GetSourceField(definition);
if (definition.VariableType == ContractVariableType.Employee)
@@ -133,7 +134,15 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
if (doc.RootElement.TryGetProperty(sourceField, out var 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;
}
}
@@ -143,8 +152,11 @@ public class OnboardingContractSnapshotService : IOnboardingContractSnapshotServ
}
}
return dynamicValues.TryGetValue(sourceField, out var dynamicValue)
? dynamicValue
if (dynamicValues.TryGetValue(sourceField, out var dynamicValue))
return dynamicValue;
return useEmployeePlaceholder
? $"【待员工填写:{definition.VariableName ?? definition.VariableCode}】"
: definition.DefaultValue;
}
@@ -0,0 +1,12 @@
namespace SimpleAdmin.Application;
/// <summary>
/// 校验任务合同变量的必填状态和员工动态变量数据类型。
/// </summary>
public interface IOnboardingContractVariableValidationService : ITransient
{
/// <summary>
/// 校验指定任务的全部合同变量快照。
/// </summary>
Task Validate(long taskId);
}
@@ -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
};
}
}
@@ -8,14 +8,17 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
private readonly SqlSugarScope _db;
private readonly IOnboardingContractSnapshotService _snapshotService;
private readonly IOnboardingIdentityValidationService _identityValidationService;
private readonly IOnboardingContractVariableValidationService _contractVariableValidationService;
public OnboardingPublicSubmissionService(
IOnboardingContractSnapshotService snapshotService,
IOnboardingIdentityValidationService identityValidationService)
IOnboardingIdentityValidationService identityValidationService,
IOnboardingContractVariableValidationService contractVariableValidationService)
{
_db = DbContext.DB;
_snapshotService = snapshotService;
_identityValidationService = identityValidationService;
_contractVariableValidationService = contractVariableValidationService;
}
/// <summary>
@@ -119,15 +122,12 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
{
var task = await GetEditableTask(input.Token);
var info = await _db.Queryable<ObTaskEmployeeInfo>()
.FirstAsync(item => item.TaskId == task.Id);
var basicInfoIncomplete = info == null
|| string.IsNullOrWhiteSpace(info.EmployeeName)
|| string.IsNullOrWhiteSpace(info.Phone)
|| string.IsNullOrWhiteSpace(info.IdCardNumber)
|| string.IsNullOrWhiteSpace(info.BankCardNumber)
|| string.IsNullOrWhiteSpace(info.BankName);
if (basicInfoIncomplete)
throw Oops.Bah("请先完整填写基本信息");
.FirstAsync(item => item.TaskId == task.Id)
?? throw Oops.Bah("请先完整填写基本信息");
// 最终提交必须重新执行全量校验,不能只依赖员工手机端的页面校验。
OnboardingEmployeeInfoValidator.ValidateAndNormalize(info);
await _db.Updateable(info).ExecuteCommandAsync();
await _identityValidationService.EnsureNoDuplicate(
task.Id,
@@ -148,7 +148,7 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
if (hasMissingRequiredMaterial)
throw Oops.Bah("仍有必传材料未上传");
await EnsureRequiredContractVariables(task);
await _contractVariableValidationService.Validate(task.Id);
var hasUnconfirmedRequiredContract = await _db.Queryable<ObTaskContract>()
.AnyAsync(item => item.TaskId == task.Id && item.Required && item.ConfirmedAt == null);
@@ -214,31 +214,6 @@ public class OnboardingPublicSubmissionService : IOnboardingPublicSubmissionServ
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)
{
return _db.Queryable<ObTaskMaterialRequirement>()
@@ -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}格式不正确,请刷新页面后重新填写");
}
}
}
@@ -131,7 +131,7 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
throw Oops.Bah("当前任务不可编辑");
}
ValidateEmployeeInfo(input);
OnboardingEmployeeInfoValidator.ValidateAndNormalize(input);
await _identityValidationService.EnsureNoDuplicate(
task.Id,
input.EmployeeName,
@@ -170,46 +170,6 @@ public class OnboardingPublicTaskService : IOnboardingPublicTaskService
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>
/// 根据员工当前已填写信息校验变量并提前生成合同 DOCX。
/// </summary>
@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace SimpleAdmin.Application;
/// <summary>
@@ -47,6 +49,41 @@ public class OnboardingTaskCreateInput
/// <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; }
}
@@ -96,4 +133,14 @@ public class OnboardingTaskCreatedOutput
/// 员工填写链接过期时间。
/// </summary>
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>
Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input);
/// <summary>
/// 复制原任务的业务配置和人事参数,批量创建独立的新入职任务。
/// </summary>
Task<OnboardingTaskCopyOutput> Copy(OnboardingTaskCopyInput input);
}
@@ -23,6 +23,9 @@ public class OnboardingTaskService : IOnboardingTaskService
/// </summary>
public async Task<OnboardingTaskCreatedOutput> Create(OnboardingTaskCreateInput input)
{
if (string.IsNullOrWhiteSpace(input.Remark))
throw Oops.Bah("请填写人事备注");
var companyEnabled = await _db.Queryable<ObCompany>()
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
if (!companyEnabled)
@@ -121,7 +124,7 @@ public class OnboardingTaskService : IOnboardingTaskService
AssignedReviewerUserId = input.AssignedReviewerUserId,
LinkExpireAt = expiresAt,
TaskStatus = OnboardingTaskStatus.PendingFill,
Remark = input.Remark
Remark = input.Remark.Trim()
};
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>
@@ -289,7 +516,7 @@ public class OnboardingTaskService : IOnboardingTaskService
/// <summary>
/// 校验人事创建任务时填写的合同日期先后关系。
/// </summary>
private static void ValidateInitialContractDateRanges(
internal static void ValidateInitialContractDateRanges(
List<OnboardingInitialContractVariableInput> inputs)
{
inputs ??= new List<OnboardingInitialContractVariableInput>();
@@ -1,4 +1,4 @@
namespace SimpleAdmin.Application;
namespace SimpleAdmin.Application;
/// <summary>
/// 入职任务分页查询参数。
@@ -24,6 +24,64 @@ public class OnboardingTaskPageInput : BasePageInput
/// 入职业务部门 Id。
/// </summary>
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>
@@ -8,7 +8,7 @@ public interface IOnboardingTaskQueryService : ITransient
/// <summary>
/// 分页查询后台入职任务。
/// </summary>
Task<SqlSugarPagedList<ObOnboardingTask>> Page(OnboardingTaskPageInput input);
Task<OnboardingTaskPageOutput> Page(OnboardingTaskPageInput input);
/// <summary>
/// 查询任务基础信息、人员资料、合同、材料、表单值、链接和审核记录。
@@ -17,22 +17,78 @@ public class OnboardingTaskQueryService : IOnboardingTaskQueryService
/// <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>()
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo), item => item.TaskNo.Contains(input.TaskNo))
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus), item => item.TaskStatus == input.TaskStatus)
.WhereIF(input.CompanyId.HasValue, item => item.CompanyId == input.CompanyId.Value)
.WhereIF(input.DepartmentId.HasValue, item => item.DepartmentId == input.DepartmentId.Value)
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskNo),
item => item.TaskNo.Contains(input.TaskNo))
.WhereIF(!string.IsNullOrWhiteSpace(input.TaskStatus) && !filterApprovedGroup,
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)
.ToPagedListAsync(input.PageNum, input.PageSize);
if (list.List != null && list.List.Any())
{
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>
@@ -18,6 +18,23 @@ public class OnboardingContractDocumentController
_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>
@@ -1,4 +1,5 @@
using System.ComponentModel;
namespace SimpleAdmin.Web.Core;
/// <summary>
@@ -25,4 +26,14 @@ public class OnboardingTaskController
{
return await _service.Create(input);
}
/// <summary>
/// 在原任务之外批量复制独立的新入职任务。
/// </summary>
[HttpPost("copy")]
[DisplayName("复制入职任务")]
public async Task<dynamic> Copy([FromBody] OnboardingTaskCopyInput input)
{
return await _service.Copy(input);
}
}