package jnpf.fileStorage.dms; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.dmsEntity.storage.form.DmsFileUploadRegisterForm; import jnpf.exception.DataException; import jnpf.model.filecontent.FileContentImportForm; import jnpf.model.filecontent.FileContentReceipt; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.util.Locale; /** Adapts the existing reader and storage method; never invokes registration or workflow callbacks. */ @Service @RequiredArgsConstructor public class FileContentImportService { private final DmsCommonFileReader reader; private final DmsFileStorageService storage; private final DmsApprovalPageMergeService approvalPageMergeService; private final DmsQianpiyeDocxWriter qianpiyeDocxWriter; private final DmsDocxCommentCleaner commentCleaner; private final ObjectMapper json; public boolean sameContent(jnpf.model.filecontent.FileContentComparisonForm form) { try { JsonNode before = json.readTree(form.getOriginal()), after = json.readTree(form.getCandidate()); if (before == null || after == null || !before.isArray() || !after.isArray() || before.isEmpty() || before.size() != after.size() || before.size() > 100) throw new DataException("文件内容比较参数无效"); for (int i = 0; i < before.size(); i++) { if (!digest(before.get(i)).equals(digest(after.get(i)))) return false; } return true; } catch (java.io.IOException invalid) { throw new DataException("文件内容比较参数无效"); } } private String digest(JsonNode file) { // Existing local storage resolver rejects arbitrary URL fetching and traversal. return reader.withFile(file.path("url").asText(), file.toString(), "内容校验", source -> { try (InputStream input = source.getInputStream()) { return cn.hutool.crypto.digest.DigestUtil.sha256Hex(input); } catch (IOException failure) { throw new DataException("无法读取附件内容,请稍后重试"); } }); } public FileContentReceipt importContent(FileContentImportForm form) { DmsFileUploadRegisterForm stored = reader.withFile(singleFileUrl(form.getFileJson()), form.getFileJson(), form.getFileName(), file -> store(form, file)); FileContentReceipt receipt = new FileContentReceipt(); BeanUtils.copyProperties(stored, receipt); return receipt; } private DmsFileUploadRegisterForm store(FileContentImportForm form, MultipartFile file) { String approvalPage = form.getSignaturePageFiles(); MultipartFile source = isDocx(file) && form.getSignaturePageValues() != null && approvalPage != null && !approvalPage.isBlank() ? approvalPageMergeService.withoutApprovalPages(file) : file; MultipartFile renderedFile = isDocx(file) && form.getSignaturePageValues() != null ? qianpiyeDocxWriter.render(source, form.getSignaturePageValues(), form.getApprovalFlowVersionId()) : file; if (approvalPage == null || approvalPage.isBlank() || !isDocx(file)) { return storeFormal(form, renderedFile); } return reader.withFile(singleFileUrl(approvalPage), approvalPage, "签批页", page -> { MultipartFile rendered = isDocx(page) ? qianpiyeDocxWriter.render(page, form.getSignaturePageValues(), form.getApprovalFlowVersionId()) : page; MultipartFile merged = approvalPageMergeService.replaceOrPrepend(renderedFile, rendered); return storeFormal(form, merged); }); } private DmsFileUploadRegisterForm storeFormal(FileContentImportForm form, MultipartFile file) { MultipartFile formal = form.isRemoveDocxComments() && isDocx(file) ? commentCleaner.clean(file) : file; return storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), withDocumentName(formal, form.getFileName(), file.getOriginalFilename())); } // Rename only formal content, after signature-page merging and comment cleanup. // Keep the source extension so a business title cannot change the file format. private MultipartFile withDocumentName(MultipartFile file, String documentName, String sourceName) { if (documentName == null || documentName.isBlank()) { throw new DataException("文件名称不能为空"); } String name = documentName.trim(); int dot = sourceName == null ? -1 : sourceName.lastIndexOf('.'); String extension = dot < 0 ? "" : sourceName.substring(dot); if (!extension.isEmpty() && !name.toLowerCase(Locale.ROOT).endsWith(extension.toLowerCase(Locale.ROOT))) { name += extension; } return name.equals(file.getOriginalFilename()) ? file : new NamedMultipartFile(file, name); } private record NamedMultipartFile(MultipartFile delegate, String filename) implements MultipartFile { @Override public String getName() { return delegate.getName(); } @Override public String getOriginalFilename() { return filename; } @Override public String getContentType() { return delegate.getContentType(); } @Override public boolean isEmpty() { return delegate.isEmpty(); } @Override public long getSize() { return delegate.getSize(); } @Override public byte[] getBytes() throws IOException { return delegate.getBytes(); } @Override public InputStream getInputStream() throws IOException { return delegate.getInputStream(); } @Override public void transferTo(File dest) throws IOException { delegate.transferTo(dest); } @Override public void transferTo(Path dest) throws IOException { delegate.transferTo(dest); } } private boolean isDocx(MultipartFile file) { String name = file == null ? null : file.getOriginalFilename(); return name != null && name.toLowerCase(java.util.Locale.ROOT).endsWith(".docx"); } private String singleFileUrl(String fileJson) { try { JsonNode value = json.readTree(fileJson); if (value != null && value.isArray()) { if (value.size() != 1) throw new DataException("每次导入必须有且只有一个物理文件"); value = value.get(0); } JsonNode url = value == null ? null : value.get("url"); if (url == null || !url.isTextual() || url.textValue().isBlank()) throw new DataException("附件缺少URL"); return url.textValue(); } catch (JsonProcessingException e) { throw new DataException("附件格式不正确"); } } }