namespace SimpleAdmin.Application; public class OnboardingContractDocumentService : IOnboardingContractDocumentService { private readonly ISqlSugarClient _db; private readonly IFileService _fileService; public OnboardingContractDocumentService(ISqlSugarClient db, IFileService fileService) { _db = db; _fileService = fileService; } public async Task Generate(OnboardingGenerateContractInput input) { var contract = await _db.Queryable().InSingleAsync(input.TaskContractId) ?? throw Oops.Bah("任务合同不存在"); var task = await _db.Queryable().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().InSingleAsync(contract.TemplateVersionId) ?? throw Oops.Bah("合同版本不存在"); var source = await _db.Queryable().InSingleAsync(version.SourceDocxFileId) ?? throw Oops.Bah("合同源文件不存在"); var variables = await _db.Queryable() .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 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); 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(); } }