Files
OnboardXAdmin/SimpleAdmin.Application/Services/Onboarding/ContractDocument/OnboardingContractDocumentService.cs
T
2026-08-24 15:32:50 +08:00

106 lines
4.6 KiB
C#

namespace SimpleAdmin.Application;
/// <summary>
/// 实现入职合同文件生成业务能力。
/// </summary>
public class OnboardingContractDocumentService : IOnboardingContractDocumentService
{
private readonly SqlSugarScope _db;
private readonly IFileService _fileService;
public OnboardingContractDocumentService(IFileService fileService)
{
_db = DbContext.DB;
_fileService = fileService;
}
/// <summary>
/// 根据已锁定的模板版本和变量快照生成任务合同 DOCX 文件。
/// </summary>
public async Task<long> Generate(OnboardingGenerateContractInput input)
{
var contract = await _db.Queryable<ObTaskContract>().InSingleAsync(input.TaskContractId)
?? throw Oops.Bah("任务合同不存在");
var task = await _db.Queryable<ObOnboardingTask>().InSingleAsync(contract.TaskId)
?? throw Oops.Bah("入职任务不存在");
if (task.TaskStatus != OnboardingTaskStatus.Approved)
throw Oops.Bah("仅审核通过的任务可生成归档合同");
if (contract.GeneratedDocxFileId.HasValue)
return contract.GeneratedDocxFileId.Value;
var version = await _db.Queryable<ObContractTemplateVersion>().InSingleAsync(contract.TemplateVersionId)
?? throw Oops.Bah("合同版本不存在");
var source = await _db.Queryable<SysFile>().InSingleAsync(version.SourceDocxFileId)
?? throw Oops.Bah("合同源文件不存在");
var variables = await _db.Queryable<ObTaskContractVariable>()
.Where(x => x.TaskContractId == contract.Id)
.ToListAsync();
var sourceResult = await _fileService.Download(new BaseIdInput { Id = source.Id })
?? throw Oops.Bah("无法读取合同源文件");
await using var sourceStream = sourceResult.FileStream;
using var sourceBytes = new MemoryStream();
await sourceStream.CopyToAsync(sourceBytes);
// 合同只读取任务变量快照,确保模板或员工档案后续修改不会影响已生成文件。
var rendered = ReplaceDocxText(
sourceBytes.ToArray(),
variables.ToDictionary(x => x.VariableCode, x => x.VariableValue ?? string.Empty));
var name = $"{task.TaskNo}_{contract.ContractName}.docx";
var fileId = await _fileService.StoreGeneratedFile(source.Engine, name, rendered);
contract.GeneratedDocxFileId = fileId;
contract.GeneratedAt = DateTime.Now;
await _db.Updateable(contract).ExecuteCommandAsync();
return fileId;
}
private static byte[] ReplaceDocxText(byte[] docx, Dictionary<string, string> values)
{
using var input = new MemoryStream(docx);
using var archive = new global::System.IO.Compression.ZipArchive(
input,
global::System.IO.Compression.ZipArchiveMode.Read);
using var output = new MemoryStream();
using var target = new global::System.IO.Compression.ZipArchive(
output,
global::System.IO.Compression.ZipArchiveMode.Create,
true);
// DOCX 本质是 ZIP 包,仅替换正文、页眉和页脚 XML 中的文本节点,其他资源原样复制。
foreach (var entry in archive.Entries)
{
var next = target.CreateEntry(entry.FullName, global::System.IO.Compression.CompressionLevel.Optimal);
using var reader = entry.Open();
using var writer = next.Open();
var isWordTextXml = entry.FullName == "word/document.xml"
|| ((entry.FullName.StartsWith("word/header") || entry.FullName.StartsWith("word/footer"))
&& entry.FullName.EndsWith(".xml"));
if (!isWordTextXml)
{
reader.CopyTo(writer);
continue;
}
using var textReader = new StreamReader(reader);
var document = global::System.Xml.Linq.XDocument.Parse(textReader.ReadToEnd());
var word = global::System.Xml.Linq.XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
foreach (var node in document.Descendants(word + "t"))
{
foreach (var item in values)
node.Value = node.Value.Replace("{{" + item.Key + "}}", item.Value ?? string.Empty);
}
using var textWriter = new StreamWriter(writer);
textWriter.Write(document.ToString(global::System.Xml.Linq.SaveOptions.DisableFormatting));
}
return output.ToArray();
}
}