namespace SimpleAdmin.Application; /// /// 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。 /// public class OnboardingContractTemplateService : DbRepository, IOnboardingContractTemplateService { private readonly IFileService _fileService; public OnboardingContractTemplateService(IFileService fileService) { _fileService = fileService; } /// /// 分页查询合同模板主档案。 /// public async Task> Page(OnboardingContractTemplatePageInput input) { return await Context.Queryable() .WhereIF( !string.IsNullOrWhiteSpace(input.TemplateName), it => it.TemplateName.Contains(input.TemplateName) || it.TemplateCode.Contains(input.TemplateName)) .WhereIF(!string.IsNullOrWhiteSpace(input.ContractType), it => it.ContractType == input.ContractType) .WhereIF(!string.IsNullOrWhiteSpace(input.Status), it => it.Status == input.Status) .OrderByIF(!string.IsNullOrWhiteSpace(input.SortField), $"{input.SortField} {input.SortOrder}") .OrderBy(it => it.TemplateName) .ToPagedListAsync(input.PageNum, input.PageSize); } /// /// 查询合同模板详情。 /// public async Task Detail(BaseIdInput input) { return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在"); } /// /// 新增合同模板主档案。 /// public async Task Add(OnboardingContractTemplateAddInput input) { await EnsureCodeUnique(input.TemplateCode, 0); await InsertAsync(input.Adapt()); } /// /// 编辑合同模板主档案。 /// public async Task Edit(OnboardingContractTemplateEditInput input) { await Detail(new BaseIdInput { Id = input.Id }); await EnsureCodeUnique(input.TemplateCode, input.Id); await UpdateAsync(input.Adapt()); } /// /// 删除未被版本、适用范围或任务合同引用的模板。 /// public async Task Delete(BaseIdListInput input) { if (input.Ids.Count == 0) return; var hasVersions = await Context.Queryable() .AnyAsync(it => input.Ids.Contains(it.TemplateId)); if (hasVersions) throw Oops.Bah("模板已有版本,不能删除"); var hasScopes = await Context.Queryable() .AnyAsync(it => input.Ids.Contains(it.TemplateId)); if (hasScopes) throw Oops.Bah("模板已配置旧版适用范围,不能删除"); var hasOnboardingTemplates = await Context.Queryable() .AnyAsync(it => input.Ids.Contains(it.ContractTemplateId)); if (hasOnboardingTemplates) throw Oops.Bah("模板已被入职模板引用,不能删除"); var hasContracts = await Context.Queryable() .AnyAsync(it => input.Ids.Contains(it.TemplateId)); if (hasContracts) throw Oops.Bah("模板已生成入职合同,不能删除"); await DeleteByIdsAsync(input.Ids.Cast().ToArray()); } /// /// 分页查询指定合同模板的版本。 /// public async Task> VersionPage(OnboardingContractVersionPageInput input) { return await Context.Queryable() .Where(it => it.TemplateId == input.TemplateId) .WhereIF(!string.IsNullOrWhiteSpace(input.VersionStatus), it => it.VersionStatus == input.VersionStatus) .OrderByDescending(it => it.CreateTime) .ToPagedListAsync(input.PageNum, input.PageSize); } /// /// 新增合同模板 DOCX 版本。 /// public async Task AddVersion(OnboardingContractVersionAddInput input) { await Detail(new BaseIdInput { Id = input.TemplateId }); var file = await Context.Queryable().InSingleAsync(input.SourceDocxFileId) ?? throw Oops.Bah("DOCX 源文件不存在"); if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase)) throw Oops.Bah("合同模板仅支持 DOCX 文件"); var versionExists = await Context.Queryable() .AnyAsync(item => item.TemplateId == input.TemplateId && item.VersionNo == input.VersionNo); if (versionExists) throw Oops.Bah("该模板版本号已存在"); var sourceResult = await _fileService.Download(new BaseIdInput { Id = file.Id }) ?? throw Oops.Bah("无法读取 DOCX 源文件"); await using var sourceStream = sourceResult.FileStream; using var sourceBytes = new MemoryStream(); await sourceStream.CopyToAsync(sourceBytes); var bytes = sourceBytes.ToArray(); var variableCodes = DocxTemplateProcessor.ExtractVariableCodes(bytes); var versionId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(); var version = input.Adapt(); version.Id = versionId; version.VersionStatus = ContractTemplateVersionStatus.Draft; version.FileHash = string.IsNullOrWhiteSpace(input.FileHash) ? Convert.ToHexString(global::System.Security.Cryptography.SHA256.HashData(bytes)) : input.FileHash; var variables = variableCodes.Select((code, index) => BuildDetectedVariable(versionId, code, index)).ToList(); await Context.Ado.BeginTranAsync(); try { await Context.Insertable(version).ExecuteCommandAsync(); if (variables.Count > 0) await Context.Insertable(variables).ExecuteCommandAsync(); await Context.Ado.CommitTranAsync(); } catch { await Context.Ado.RollbackTranAsync(); throw; } } /// /// 启用或停用合同模板版本。 /// public async Task SetVersionStatus(OnboardingContractVersionStatusInput input) { if (input.VersionStatus != ContractTemplateVersionStatus.Enabled && input.VersionStatus != ContractTemplateVersionStatus.Disabled) { throw Oops.Bah("版本状态仅支持启用或停用"); } var version = await Context.Queryable().InSingleAsync(input.Id) ?? throw Oops.Bah("模板版本不存在"); if (input.VersionStatus == ContractTemplateVersionStatus.Enabled) { var hasUnresolvedVariables = await Context.Queryable() .AnyAsync(item => item.TemplateVersionId == input.Id && item.VariableType == ContractVariableType.Unresolved); if (hasUnresolvedVariables) throw Oops.Bah("仍有未识别来源的合同变量,请逐项确认后再启用"); } version.VersionStatus = input.VersionStatus; await Context.Updateable(version).ExecuteCommandAsync(); } /// /// 分页查询指定模板版本的变量定义。 /// public async Task> VariablePage(OnboardingTemplateVariablePageInput input) { return await Context.Queryable() .Where(it => it.TemplateVersionId == input.TemplateVersionId) .OrderBy(it => it.SortCode) .OrderBy(it => it.VariableCode) .ToPagedListAsync(input.PageNum, input.PageSize); } /// /// 编辑草稿模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。 /// public async Task EditVariable(OnboardingTemplateVariableEditInput input) { var variable = await Context.Queryable().InSingleAsync(input.Id) ?? throw Oops.Bah("模板变量不存在"); if (variable.TemplateVersionId != input.TemplateVersionId) throw Oops.Bah("不允许变更变量所属版本"); if (!string.Equals(variable.VariableCode, input.VariableCode, StringComparison.Ordinal)) throw Oops.Bah("DOCX 占位符编码由系统识别,不能修改"); await EnsureVersionDraft(input.TemplateVersionId); var entity = input.Adapt(); entity.VariableCode = variable.VariableCode; await Context.Updateable(entity).ExecuteCommandAsync(); } private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, int index) { var separatorIndex = code.IndexOf('.'); var prefix = separatorIndex > 0 ? code[..separatorIndex].ToLowerInvariant() : string.Empty; var sourceField = separatorIndex > 0 ? code[(separatorIndex + 1)..] : code; var variableType = prefix switch { "employee" => ContractVariableType.Employee, "company" => ContractVariableType.Company, "hr" => ContractVariableType.Contract, "system" => ContractVariableType.System, _ => ContractVariableType.Unresolved }; return new ObTemplateVariable { Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(), TemplateVersionId = versionId, VariableCode = code, VariableName = sourceField, VariableType = variableType, DataType = InferDataType(sourceField), EmployeeField = variableType is ContractVariableType.Employee or ContractVariableType.Company ? sourceField : null, Required = true, HrEditable = variableType == ContractVariableType.Contract, SortCode = (index + 1) * 10, Remark = variableType == ContractVariableType.Unresolved ? "导入时未识别变量来源,请管理员确认" : null }; } private static string InferDataType(string sourceField) { var normalized = sourceField.ToLowerInvariant(); if (normalized.Contains("date") || normalized.EndsWith("at")) return "date"; if (normalized.Contains("salary") || normalized.Contains("amount") || normalized.Contains("wage") || normalized.Contains("copies")) { return "decimal"; } return "string"; } private async Task EnsureCodeUnique(string code, long id) { var exists = await Context.Queryable() .AnyAsync(it => it.TemplateCode == code && it.Id != id); if (exists) throw Oops.Bah("模板编码已存在"); } private async Task EnsureVersionDraft(long versionId) { var version = await Context.Queryable().InSingleAsync(versionId) ?? throw Oops.Bah("模板版本不存在"); if (version.VersionStatus != ContractTemplateVersionStatus.Draft) throw Oops.Bah("只有草稿版本可维护变量"); } }