入职任务预览,入职任务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";
}
}