package jnpf.elnService.template; import feign.Response; import jnpf.elnEntity.ElnDocumentFileVo; import jnpf.exception.DataException; import jnpf.file.FileApi; import jnpf.file.FileUploadApi; import jnpf.model.upload.UploadFileModel; import jnpf.util.JsonUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.List; @Slf4j @Service public class ElnTemplateFileService { private static final String STORAGE_TYPE = "annex"; private static final long MAX_DOCX_BYTES = 50L * 1024L * 1024L; private final FileApi fileApi; private final FileUploadApi fileUploadApi; public ElnTemplateFileService(FileApi fileApi, FileUploadApi fileUploadApi) { this.fileApi = fileApi; this.fileUploadApi = fileUploadApi; } public String copyDocx(String sourceAttachments, String targetTemplateId) { String targetFileId = targetTemplateId + ".docx"; try { String sourceFileId = uniqueDocxFileId(sourceAttachments); if (targetFileId.equals(sourceFileId)) { throw new DataException("模板新旧文件不能相同"); } byte[] sourceBytes = download(sourceFileId); String folder = fileApi.getPath(STORAGE_TYPE); if (!StringUtils.hasText(folder)) { throw new DataException("模板文件目录不可用"); } MultipartFile multipart = new InMemoryMultipartFile(targetFileId, sourceBytes); if (!fileUploadApi.replaceFile(multipart, folder, targetFileId)) { throw new DataException("模板文件复制失败"); } byte[] targetBytes = download(targetFileId); if (sourceBytes.length != targetBytes.length || !MessageDigest.isEqual(sha256(sourceBytes), sha256(targetBytes))) { throw new DataException("模板文件回读校验失败"); } ElnDocumentFileVo target = new ElnDocumentFileVo(); target.setFileId(targetFileId); target.setName(targetFileId); target.setFileExtension("docx"); target.setFileSize((long) targetBytes.length); return JsonUtil.getObjectToString(Collections.singletonList(target)); } catch (DataException e) { log.error("模板文件复制失败,targetTemplateId={}, targetFileId={}", targetTemplateId, targetFileId); throw e; } catch (RuntimeException e) { log.error("模板文件复制失败,targetTemplateId={}, targetFileId={}", targetTemplateId, targetFileId); throw new DataException("模板文件复制失败"); } } private String uniqueDocxFileId(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); if (files == null) { throw new DataException("模板附件无效"); } String fileId = null; for (ElnDocumentFileVo file : files) { if (file != null && "docx".equalsIgnoreCase(file.getFileExtension()) && StringUtils.hasText(file.getFileId())) { if (fileId != null) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } fileId = file.getFileId().trim(); } } if (fileId == null) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } return fileId; } catch (DataException e) { throw e; } catch (RuntimeException e) { throw new DataException("模板附件无效"); } } private byte[] download(String fileId) { if (!fileId.toLowerCase().endsWith(".docx")) { throw new DataException("模板文件扩展名必须为 docx"); } try (Response response = fileUploadApi.downFile(new UploadFileModel(STORAGE_TYPE, fileId))) { if (response == null || response.status() < 200 || response.status() >= 300 || response.body() == null) { throw new DataException("模板文件下载失败"); } Integer declaredLength = response.body().length(); if (declaredLength != null && (declaredLength == 0 || declaredLength > MAX_DOCX_BYTES)) { throw new DataException("模板文件大小无效"); } try (InputStream input = response.body().asInputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { byte[] buffer = new byte[8192]; int total = 0; int read; while ((read = input.read(buffer)) != -1) { total += read; if (total > MAX_DOCX_BYTES) { throw new DataException("模板文件超过 50MB 限制"); } output.write(buffer, 0, read); } if (total == 0) { throw new DataException("模板文件不能为空"); } return output.toByteArray(); } } catch (DataException e) { throw e; } catch (IOException | RuntimeException e) { throw new DataException("模板文件下载失败"); } } private static byte[] sha256(byte[] bytes) { try { return MessageDigest.getInstance("SHA-256").digest(bytes); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("JVM 不支持 SHA-256", e); } } private static final class InMemoryMultipartFile implements MultipartFile { private final String fileName; private final byte[] content; private InMemoryMultipartFile(String fileName, byte[] content) { this.fileName = fileName; this.content = content; } @Override public String getName() { return "multipartFile"; } @Override public String getOriginalFilename() { return fileName; } @Override public String getContentType() { return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; } @Override public boolean isEmpty() { return content.length == 0; } @Override public long getSize() { return content.length; } @Override public byte[] getBytes() { return content; } @Override public InputStream getInputStream() { return new ByteArrayInputStream(content); } @Override public void transferTo(File destination) throws IOException { Files.write(destination.toPath(), content); } } }