入职任务预览,入职任务cope,验证填入的信息
This commit is contained in:
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user