Files
OnboardXAdmin/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/OnboardingContractTemplateService.cs
T
2026-08-25 20:37:17 +08:00

340 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace SimpleAdmin.Application;
/// <summary>
/// 合同模板主档案;具体 DOCX 文件、变量与适用范围均挂在此档案下。
/// </summary>
public class OnboardingContractTemplateService : DbRepository<ObContractTemplate>, IOnboardingContractTemplateService
{
private readonly IFileService _fileService;
public OnboardingContractTemplateService(IFileService fileService)
{
_fileService = fileService;
}
/// <summary>
/// 分页查询合同模板主档案。
/// </summary>
public async Task<SqlSugarPagedList<ObContractTemplate>> Page(OnboardingContractTemplatePageInput input)
{
return await Context.Queryable<ObContractTemplate>()
.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);
}
/// <summary>
/// 查询合同模板详情。
/// </summary>
public async Task<ObContractTemplate> Detail(BaseIdInput input)
{
return await GetByIdAsync(input.Id) ?? throw Oops.Bah("合同模板不存在");
}
/// <summary>
/// 新增合同模板主档案。
/// </summary>
public async Task Add(OnboardingContractTemplateAddInput input)
{
await EnsureCodeUnique(input.TemplateCode, 0);
await InsertAsync(input.Adapt<ObContractTemplate>());
}
/// <summary>
/// 编辑合同模板主档案。
/// </summary>
public async Task Edit(OnboardingContractTemplateEditInput input)
{
await Detail(new BaseIdInput { Id = input.Id });
await EnsureCodeUnique(input.TemplateCode, input.Id);
await UpdateAsync(input.Adapt<ObContractTemplate>());
}
/// <summary>
/// 删除未被入职模板或任务合同引用的模板(级联删除未使用的版本和变量)。
/// </summary>
public async Task Delete(BaseIdListInput input)
{
if (input.Ids.Count == 0)
return;
var hasOnboardingTemplates = await Context.Queryable<ObOnboardingTemplateContract>()
.AnyAsync(it => input.Ids.Contains(it.ContractTemplateId));
if (hasOnboardingTemplates)
throw Oops.Bah("模板已被入职模板引用,不能删除,请先在入职模板中移除关联");
var hasContracts = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => input.Ids.Contains(it.TemplateId));
if (hasContracts)
throw Oops.Bah("模板已生成入职任务合同,不能删除,可改为停用");
var versionIds = await Context.Queryable<ObContractTemplateVersion>()
.Where(it => input.Ids.Contains(it.TemplateId))
.Select(it => it.Id)
.ToListAsync();
await Context.Ado.BeginTranAsync();
try
{
if (versionIds.Count > 0)
{
await Context.Deleteable<ObTemplateVariable>()
.Where(it => versionIds.Contains(it.TemplateVersionId))
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplateVersion>()
.In(versionIds)
.ExecuteCommandAsync();
}
await Context.Deleteable<ObContractScope>()
.Where(it => input.Ids.Contains(it.TemplateId))
.ExecuteCommandAsync();
await DeleteByIdsAsync(input.Ids.Cast<object>().ToArray());
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 删除未被入职模板或任务使用的合同版本。
/// </summary>
public async Task DeleteVersion(BaseIdInput input)
{
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
?? throw Oops.Bah("合同版本不存在");
var isUsedInOnboardingTemplate = await Context.Queryable<ObOnboardingTemplateContract>()
.AnyAsync(it => it.ContractTemplateVersionId == version.Id);
if (isUsedInOnboardingTemplate)
throw Oops.Bah("该版本已被入职模板引用,不能删除,请先在入职模板中移除关联");
var isUsedInTask = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => it.TemplateVersionId == version.Id);
if (isUsedInTask)
throw Oops.Bah("该版本已用于实际入职任务,不能删除,可改为停用");
await Context.Ado.BeginTranAsync();
try
{
await Context.Deleteable<ObTemplateVariable>()
.Where(it => it.TemplateVersionId == version.Id)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractScope>()
.Where(it => it.TemplateVersionId == version.Id)
.ExecuteCommandAsync();
await Context.Deleteable<ObContractTemplateVersion>()
.In(version.Id)
.ExecuteCommandAsync();
await Context.Ado.CommitTranAsync();
}
catch
{
await Context.Ado.RollbackTranAsync();
throw;
}
}
/// <summary>
/// 分页查询指定合同模板的版本。
/// </summary>
public async Task<SqlSugarPagedList<ObContractTemplateVersion>> VersionPage(OnboardingContractVersionPageInput input)
{
return await Context.Queryable<ObContractTemplateVersion>()
.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);
}
/// <summary>
/// 新增合同模板 DOCX 版本。
/// </summary>
public async Task AddVersion(OnboardingContractVersionAddInput input)
{
await Detail(new BaseIdInput { Id = input.TemplateId });
var file = await Context.Queryable<SysFile>().InSingleAsync(input.SourceDocxFileId)
?? throw Oops.Bah("DOCX 源文件不存在");
if (!string.Equals(file.Suffix, "docx", StringComparison.OrdinalIgnoreCase))
throw Oops.Bah("合同模板仅支持 DOCX 文件");
var versionExists = await Context.Queryable<ObContractTemplateVersion>()
.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 dictList = await Context.Queryable<ObVariableDefinition>().ToListAsync();
var dictMap = dictList.ToDictionary(it => it.VariableCode, it => it, StringComparer.OrdinalIgnoreCase);
var undefinedCodes = variableCodes.Where(code => !dictMap.ContainsKey(code)).ToList();
if (undefinedCodes.Count > 0)
{
throw Oops.Bah($"DOCX 模板包含未在字典库中注册的占位符:{string.Join(",", undefinedCodes)}。请检查拼写或先在【变量字典】中添加该变量。");
}
var versionId = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId();
var version = input.Adapt<ObContractTemplateVersion>();
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) =>
{
var def = dictMap.TryGetValue(code, out var d) ? d : null;
return BuildDetectedVariable(versionId, code, def, 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;
}
}
/// <summary>
/// 启用或停用合同模板版本。
/// </summary>
public async Task SetVersionStatus(OnboardingContractVersionStatusInput input)
{
if (input.VersionStatus != ContractTemplateVersionStatus.Enabled
&& input.VersionStatus != ContractTemplateVersionStatus.Disabled)
{
throw Oops.Bah("版本状态仅支持启用或停用");
}
var version = await Context.Queryable<ObContractTemplateVersion>().InSingleAsync(input.Id)
?? throw Oops.Bah("模板版本不存在");
if (input.VersionStatus == ContractTemplateVersionStatus.Enabled)
{
var hasUnresolvedVariables = await Context.Queryable<ObTemplateVariable>()
.AnyAsync(item => item.TemplateVersionId == input.Id
&& item.VariableType == ContractVariableType.Unresolved);
if (hasUnresolvedVariables)
throw Oops.Bah("仍有未识别来源的合同变量,请逐项确认后再启用");
}
version.VersionStatus = input.VersionStatus;
await Context.Updateable(version).ExecuteCommandAsync();
}
/// <summary>
/// 分页查询指定模板版本的变量定义。
/// </summary>
public async Task<SqlSugarPagedList<ObTemplateVariable>> VariablePage(OnboardingTemplateVariablePageInput input)
{
return await Context.Queryable<ObTemplateVariable>()
.Where(it => it.TemplateVersionId == input.TemplateVersionId)
.OrderBy(it => it.SortCode)
.OrderBy(it => it.VariableCode)
.ToPagedListAsync(input.PageNum, input.PageSize);
}
/// <summary>
/// 编辑模板版本中自动识别出的变量,不允许修改 DOCX 占位符编码。
/// </summary>
public async Task EditVariable(OnboardingTemplateVariableEditInput input)
{
var variable = await Context.Queryable<ObTemplateVariable>().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 占位符编码由系统识别,不能修改");
var isUsedInTask = await Context.Queryable<ObTaskContract>()
.AnyAsync(it => it.TemplateVersionId == input.TemplateVersionId);
if (isUsedInTask)
throw Oops.Bah("该版本已生成实际入职任务合同,变量配置已锁定");
var entity = input.Adapt<ObTemplateVariable>();
entity.VariableCode = variable.VariableCode;
await Context.Updateable(entity).ExecuteCommandAsync();
}
private static ObTemplateVariable BuildDetectedVariable(long versionId, string code, ObVariableDefinition def, 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
};
var varName = def != null && !string.IsNullOrWhiteSpace(def.VariableName) ? def.VariableName : sourceField;
var dataType = def != null && !string.IsNullOrWhiteSpace(def.DataType) ? def.DataType : InferDataType(sourceField);
return new ObTemplateVariable
{
Id = global::SimpleAdmin.Core.Utils.CommonUtils.GetSingleId(),
TemplateVersionId = versionId,
VariableCode = code,
VariableName = varName,
VariableType = variableType,
DataType = dataType,
EmployeeField = variableType is ContractVariableType.Employee or ContractVariableType.Company
? sourceField
: null,
Required = def?.Required ?? true,
HrEditable = variableType == ContractVariableType.Contract,
SortCode = (index + 1) * 10,
Remark = def?.Remark
};
}
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<ObContractTemplate>()
.AnyAsync(it => it.TemplateCode == code && it.Id != id);
if (exists)
throw Oops.Bah("模板编码已存在");
}
}