namespace SimpleAdmin.Application;
///
/// 实现基于入职模板创建任务及冻结配置快照的业务能力。
///
public class OnboardingTaskService : IOnboardingTaskService
{
private readonly SqlSugarScope _db;
private readonly IConfigService _configService;
private readonly IOnboardingContractSnapshotService _snapshotService;
public OnboardingTaskService(
IConfigService configService,
IOnboardingContractSnapshotService snapshotService)
{
_db = DbContext.DB;
_configService = configService;
_snapshotService = snapshotService;
}
///
/// 校验公司、岗位和入职模板后创建任务,并冻结合同、补充字段及材料要求。
///
public async Task Create(OnboardingTaskCreateInput input)
{
var companyEnabled = await _db.Queryable()
.AnyAsync(item => item.Id == input.CompanyId && item.Status == "enable");
if (!companyEnabled)
throw Oops.Bah("公司不存在或未启用");
var positionEnabled = await _db.Queryable()
.AnyAsync(item => item.Id == input.PositionId
&& item.CompanyId == input.CompanyId
&& item.Status == "enable");
if (!positionEnabled)
throw Oops.Bah("岗位不存在、未启用或不属于该公司");
var onboardingTemplate = await _db.Queryable()
.FirstAsync(item => item.Id == input.OnboardingTemplateId
&& item.CompanyId == input.CompanyId
&& item.Status == "enable")
?? throw Oops.Bah("入职模板不存在、未启用或不属于该公司");
var templateSupportsPosition = await _db.Queryable()
.AnyAsync(item => item.OnboardingTemplateId == onboardingTemplate.Id
&& item.PositionId == input.PositionId);
if (!templateSupportsPosition)
throw Oops.Bah("所选入职模板不适用于该岗位");
var contractConfigs = await _db.Queryable()
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
.OrderBy(item => item.SortCode)
.ToListAsync();
if (contractConfigs.Count == 0)
throw Oops.Bah("入职模板尚未配置合同文件");
var fieldConfigs = await _db.Queryable()
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
.OrderBy(item => item.SortCode)
.ToListAsync();
var materialConfigs = await _db.Queryable()
.Where(item => item.OnboardingTemplateId == onboardingTemplate.Id)
.OrderBy(item => item.SortCode)
.ToListAsync();
var templateIds = contractConfigs.Select(item => item.ContractTemplateId).Distinct().ToList();
var contractTemplates = await _db.Queryable()
.Where(item => templateIds.Contains(item.Id))
.ToListAsync();
var versionIds = contractConfigs.Select(item => item.ContractTemplateVersionId).Distinct().ToList();
var enabledVersionCount = await _db.Queryable()
.CountAsync(item => versionIds.Contains(item.Id)
&& item.VersionStatus == ContractTemplateVersionStatus.Enabled);
if (enabledVersionCount != versionIds.Count)
throw Oops.Bah("入职模板包含未启用的合同版本,请先调整模板配置");
var fieldIds = fieldConfigs.Select(item => item.FieldId).Distinct().ToList();
var fields = fieldIds.Count == 0
? new List()
: await _db.Queryable().Where(item => fieldIds.Contains(item.Id)).ToListAsync();
var materialIds = materialConfigs.Select(item => item.MaterialTypeId).Distinct().ToList();
var materialTypes = materialIds.Count == 0
? new List()
: await _db.Queryable().Where(item => materialIds.Contains(item.Id)).ToListAsync();
var 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 taskId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var expiresAt = DateTime.Now.AddHours(expiryHours);
var token = Guid.NewGuid().ToString("N");
var task = new ObOnboardingTask
{
Id = taskId,
TaskNo = $"OB{DateTime.Now:yyyyMMddHHmmss}{taskId.ToString()[^4..]}",
CompanyId = input.CompanyId,
PositionId = input.PositionId,
OnboardingTemplateId = onboardingTemplate.Id,
AssignedReviewerUserId = input.AssignedReviewerUserId,
LinkExpireAt = expiresAt,
TaskStatus = OnboardingTaskStatus.PendingFill,
Remark = input.Remark
};
await _db.Ado.BeginTranAsync();
try
{
await _db.Insertable(task).ExecuteCommandAsync();
await _db.Insertable(new ObTaskLink
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TaskId = taskId,
TokenHash = Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(
global::System.Text.Encoding.UTF8.GetBytes(token))),
ExpiresAt = expiresAt
}).ExecuteCommandAsync();
var contracts = contractConfigs.Select(config =>
{
var template = contractTemplates.First(item => item.Id == config.ContractTemplateId);
return new ObTaskContract
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TaskId = taskId,
TemplateId = config.ContractTemplateId,
TemplateVersionId = config.ContractTemplateVersionId,
ContractName = template.TemplateName,
ContractType = template.ContractType,
Required = config.Required,
PrintCopies = config.PrintCopies
};
}).ToList();
await _db.Insertable(contracts).ExecuteCommandAsync();
if (fieldConfigs.Count > 0)
{
var fieldSnapshots = fieldConfigs.Select(config =>
{
var field = fields.First(item => item.Id == config.FieldId);
return new ObTaskFormRequirement
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TaskId = taskId,
FieldId = field.Id,
FieldCode = field.FieldCode,
FieldName = field.FieldName,
DataType = field.DataType,
OptionsJson = field.OptionsJson,
Required = config.Required,
SortCode = config.SortCode
};
}).ToList();
await _db.Insertable(fieldSnapshots).ExecuteCommandAsync();
}
if (materialConfigs.Count > 0)
{
var materialSnapshots = materialConfigs.Select(config =>
{
var material = materialTypes.First(item => item.Id == config.MaterialTypeId);
return new ObTaskMaterialRequirement
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TaskId = taskId,
MaterialTypeId = material.Id,
MaterialCode = material.MaterialCode,
MaterialName = material.MaterialName,
AllowedExtensions = material.AllowedExtensions,
MaxFileCount = material.MaxFileCount,
MaxFileSizeMb = material.MaxFileSizeMb,
Required = config.Required,
SortCode = config.SortCode
};
}).ToList();
await _db.Insertable(materialSnapshots).ExecuteCommandAsync();
}
await _snapshotService.Build(taskId);
await ApplyInitialContractVariables(input.ContractVariables, contracts);
await _db.Ado.CommitTranAsync();
}
catch
{
await _db.Ado.RollbackTranAsync();
throw;
}
return new OnboardingTaskCreatedOutput
{
TaskId = taskId,
TaskNo = task.TaskNo,
LinkToken = token,
ExpiresAt = expiresAt
};
}
///
/// 校验并保存任务创建时填写的人事合同变量。
///
private async Task ApplyInitialContractVariables(
List inputs,
List contracts)
{
inputs ??= new List();
var versionIds = contracts.Select(item => item.TemplateVersionId).Distinct().ToList();
var definitions = await _db.Queryable()
.Where(item => versionIds.Contains(item.TemplateVersionId)
&& item.HrEditable
&& (item.VariableType == ContractVariableType.Contract
|| item.VariableType == ContractVariableType.Company))
.ToListAsync();
var duplicateInput = inputs
.GroupBy(item => new { item.ContractTemplateVersionId, item.VariableCode })
.FirstOrDefault(group => group.Count() > 1);
if (duplicateInput != null)
throw Oops.Bah($"合同变量“{duplicateInput.Key.VariableCode}”重复提交");
foreach (var input in inputs)
{
var definitionExists = definitions.Any(item =>
item.TemplateVersionId == input.ContractTemplateVersionId
&& item.VariableCode == input.VariableCode);
if (!definitionExists)
throw Oops.Bah($"合同变量“{input.VariableCode}”不存在或不允许人事填写");
}
var missingRequired = definitions.FirstOrDefault(definition =>
{
if (!definition.Required)
return false;
var input = inputs.FirstOrDefault(item =>
item.ContractTemplateVersionId == definition.TemplateVersionId
&& item.VariableCode == definition.VariableCode);
return string.IsNullOrWhiteSpace(input?.VariableValue ?? definition.DefaultValue);
});
if (missingRequired != null)
throw Oops.Bah($"请填写合同变量“{missingRequired.VariableName}”");
if (inputs.Count == 0)
return;
var contractByVersion = contracts.ToDictionary(item => item.TemplateVersionId);
var contractIds = contracts.Select(item => item.Id).ToList();
var rows = await _db.Queryable()
.Where(item => contractIds.Contains(item.TaskContractId))
.ToListAsync();
foreach (var input in inputs)
{
var contract = contractByVersion[input.ContractTemplateVersionId];
var row = rows.FirstOrDefault(item =>
item.TaskContractId == contract.Id && item.VariableCode == input.VariableCode)
?? throw Oops.Bah($"合同变量快照“{input.VariableCode}”不存在");
row.VariableValue = input.VariableValue;
row.SourceType = ContractVariableSourceType.HrInput;
row.IsManualOverride = true;
}
var changedRows = rows.Where(item => item.IsManualOverride).ToList();
if (changedRows.Count > 0)
await _db.Updateable(changedRows).ExecuteCommandAsync();
}
}