ソースを参照

feat:批量导出框架搭建完成

baiqi 1 年間 前
コミット
01337627fa

+ 7 - 0
pom.xml

@@ -199,6 +199,13 @@
             <version>1.15.3</version>
         </dependency>
 
+        <!-- ZIP压缩 -->
+        <dependency>
+            <groupId>net.lingala.zip4j</groupId>
+            <artifactId>zip4j</artifactId>
+            <version>2.11.5</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 13 - 0
src/main/java/com/njuzr/eaibackend/controller/ExportController.java

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.io.IOException;
 import java.net.URLEncoder;
+import java.util.List;
 
 @RestController
 @RequestMapping("/api/export")
@@ -34,4 +35,16 @@ public class ExportController {
                 .headers(headers)
                 .body(wordBytes);
     }
+
+    @GetMapping("/composition/zip")
+    public ResponseEntity<byte[]> exportBatch(@RequestParam Long assignmentId, @RequestParam Long studentId) {
+
+        byte[] zipBytes = exportCompositionService.exportBatchToZip(List.of(studentId, 222L), assignmentId);
+
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION,
+                        "attachment; filename=compositions.zip")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(zipBytes);
+    }
 }

+ 33 - 113
src/main/java/com/njuzr/eaibackend/service/ExportCompositionService.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.EngagementMapper;
 import com.njuzr.eaibackend.po.Engagement;
+import com.njuzr.eaibackend.utils.WordExporter;
+import com.njuzr.eaibackend.utils.ZipExport;
 import com.njuzr.eaibackend.vo.AssignmentVO;
 import com.njuzr.eaibackend.vo.EngagementVO;
 import com.njuzr.eaibackend.vo.UserVO;
@@ -25,9 +27,8 @@ import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Service
@@ -41,8 +42,10 @@ public class ExportCompositionService {
     private AssignmentService assignmentService;
     @Autowired
     private UserService userService;
+    @Autowired
+    private WordExporter wordExporter;
 
-    public byte[] exportWord(Long assignmentId, Long studentId) {
+    public byte[] exportWordReal(Long assignmentId, Long studentId) {
         AssignmentVO assignment = assignmentService.findAssignmentById(assignmentId);
         EngagementVO engagement = assignmentService.getEngagement(studentId, assignmentId);
         UserVO user = userService.searchUsers(new Page<>(1,1), studentId, null, null, null).getRecords().get(0);
@@ -54,29 +57,36 @@ public class ExportCompositionService {
         model.put("date", new Date());
         model.put("content", processContent(engagement.getTextContent()));
 
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        try {
-            // 加载模板并渲染
-            Template template = freemarkerConfig.getTemplate("composition.ftl");
-            String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
-
-            // 创建Word文档
-            XWPFDocument document = new XWPFDocument();
-            parseHtmlToWord(document, htmlContent); // 关键改造点
-
-            document.write(out);
-            document.close();
-        } catch (Exception e){
-            log.error("exportWord failed. reason: {}", e.getMessage());
-            throw MyException.create(HttpStatus.BAD_REQUEST, "作业导出失败,请重试");
-        }
+        return wordExporter.exportCompositionWord(model);
+
+    }
 
-        return out.toByteArray();
+    public byte[] exportBatchToZip(List<Long> studentIds, Long assignmentId) {
+        List<ZipExport.ZipItem> zipItems = studentIds.stream()
+                .map(id -> {
+                    try {
+                        byte[] wordBytes = exportWord(assignmentId, id);
+                        String fileName = String.format("%s_%d.docx",
+                                "作业", id);
+                        log.info("导出成功 studentId={}", id);
+                        return new ZipExport.ZipItem(fileName, wordBytes);
+                    } catch (Exception e) {
+                        log.error("导出失败 studentId={}", id, e);
+                        return null;
+                    }
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
 
+        try {
+            return ZipExport.packToZip(zipItems);
+        } catch (IOException e) {
+            throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "ZIP打包失败"+e.getMessage());
+        }
     }
 
 
-    public byte[] exportWordTest(Long assignmentId, Long studentId) throws IOException, TemplateException {
+    public byte[] exportWord(Long assignmentId, Long studentId) throws IOException, TemplateException {
 
         String assignmentName = "My Favorite Season";
         String content = "My favorite season is autumn. The weather is cool and comfortable, and the leaves turn golden, making the scenery beautiful. I enjoy walking in the park and listening to the sound of rustling leaves. Autumn is also a harvest season, with fruits like apples and pears ripe for picking.\n" +
@@ -90,101 +100,11 @@ public class ExportCompositionService {
         model.put("date", new Date());
         model.put("content", content.replaceAll("\n", "<br/>"));
 
-        // 3. 加载模板并渲染
-        Template template = freemarkerConfig.getTemplate("composition.ftl");
-        String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
-
-        // 3. 创建Word文档
-        XWPFDocument document = new XWPFDocument();
-        parseHtmlToWord(document, htmlContent); // 关键改造点
-
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        document.write(out);
-        document.close();
-
-        return out.toByteArray();
+        return wordExporter.exportCompositionWord(model);
     }
 
     private String processContent(String content) {
         return content.replaceAll("\n", "<br/>");
     }
 
-    /**
-     * 将HTML内容解析为Word格式(支持基础HTML标签)
-     */
-    private void parseHtmlToWord(XWPFDocument document, String html) {
-        // 使用JSoup解析HTML
-        Element body = Jsoup.parse(html).body();
-
-        // 递归处理所有子节点
-        for (Node node : body.childNodes()) {
-            processNode(document, node);
-        }
-    }
-
-    /**
-     * 递归处理HTML节点
-     */
-    private void processNode(XWPFDocument doc, Node node) {
-        if (node instanceof TextNode) {
-            // 文本节点直接添加
-            XWPFParagraph para = doc.createParagraph();
-            XWPFRun run = para.createRun();
-            run.setText(((TextNode) node).text());
-        } else if (node instanceof Element) {
-            Element element = (Element) node;
-            switch (element.tagName().toLowerCase()) {
-                case "h1":
-                case "h2":
-                case "h3":
-                case "h4":
-                case "h5":
-                case "h6":
-                    // 处理标题
-                    XWPFParagraph heading = doc.createParagraph();
-                    heading.setStyle("Heading" + element.tagName().charAt(1));
-                    XWPFRun headingRun = heading.createRun();
-                    headingRun.setText(element.text());
-                    headingRun.setBold(true);
-                    break;
-                case "p":
-                    // 处理段落
-                    XWPFParagraph para = doc.createParagraph();
-                    XWPFRun run = para.createRun();
-                    run.setText(element.text());
-                    break;
-                case "b":
-                case "strong":
-                    // 加粗文本
-                    XWPFParagraph boldPara = doc.createParagraph();
-                    XWPFRun boldRun = boldPara.createRun();
-                    boldRun.setText(element.text());
-                    boldRun.setBold(true);
-                    break;
-                case "i":
-                case "em":
-                    // 斜体文本
-                    XWPFParagraph italicPara = doc.createParagraph();
-                    XWPFRun italicRun = italicPara.createRun();
-                    italicRun.setText(element.text());
-                    italicRun.setItalic(true);
-                    break;
-                case "ul":
-                case "ol":
-                    // 处理列表
-                    for (Element li : element.children()) {
-                        XWPFParagraph listPara = doc.createParagraph();
-                        XWPFRun listRun = listPara.createRun();
-                        listRun.setText("• " + li.text()); // 简单列表符号
-                    }
-                    break;
-                default:
-                    // 默认处理(递归子节点)
-                    for (Node child : node.childNodes()) {
-                        processNode(doc, child);
-                    }
-            }
-        }
-    }
-
 }

+ 127 - 0
src/main/java/com/njuzr/eaibackend/utils/WordExporter.java

@@ -0,0 +1,127 @@
+package com.njuzr.eaibackend.utils;
+
+import com.njuzr.eaibackend.exception.MyException;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class WordExporter {
+
+    @Autowired
+    private Configuration freemarkerConfig;
+
+    public byte[] exportCompositionWord(Map<String, Object> model) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            // 加载模板并渲染
+            Template template = freemarkerConfig.getTemplate("composition.ftl");
+            String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
+
+            // 创建Word文档
+            XWPFDocument document = new XWPFDocument();
+            parseHtmlToWord(document, htmlContent); // 关键改造点
+
+            document.write(out);
+            document.close();
+        } catch (Exception e){
+            log.error("exportWord failed. reason: {}", e.getMessage());
+            throw MyException.create(HttpStatus.BAD_REQUEST, "作业导出失败,请重试");
+        }
+
+        return out.toByteArray();
+    }
+
+    /**
+     * 将HTML内容解析为Word格式(支持基础HTML标签)
+     */
+    private void parseHtmlToWord(XWPFDocument document, String html) {
+        // 使用JSoup解析HTML
+        Element body = Jsoup.parse(html).body();
+
+        // 递归处理所有子节点
+        for (Node node : body.childNodes()) {
+            processNode(document, node);
+        }
+    }
+
+    /**
+     * 递归处理HTML节点
+     */
+    private void processNode(XWPFDocument doc, Node node) {
+        if (node instanceof TextNode) {
+            // 文本节点直接添加
+            XWPFParagraph para = doc.createParagraph();
+            XWPFRun run = para.createRun();
+            run.setText(((TextNode) node).text());
+        } else if (node instanceof Element) {
+            Element element = (Element) node;
+            switch (element.tagName().toLowerCase()) {
+                case "h1":
+                case "h2":
+                case "h3":
+                case "h4":
+                case "h5":
+                case "h6":
+                    // 处理标题
+                    XWPFParagraph heading = doc.createParagraph();
+                    heading.setStyle("Heading" + element.tagName().charAt(1));
+                    XWPFRun headingRun = heading.createRun();
+                    headingRun.setText(element.text());
+                    headingRun.setBold(true);
+                    break;
+                case "p":
+                    // 处理段落
+                    XWPFParagraph para = doc.createParagraph();
+                    XWPFRun run = para.createRun();
+                    run.setText(element.text());
+                    break;
+                case "b":
+                case "strong":
+                    // 加粗文本
+                    XWPFParagraph boldPara = doc.createParagraph();
+                    XWPFRun boldRun = boldPara.createRun();
+                    boldRun.setText(element.text());
+                    boldRun.setBold(true);
+                    break;
+                case "i":
+                case "em":
+                    // 斜体文本
+                    XWPFParagraph italicPara = doc.createParagraph();
+                    XWPFRun italicRun = italicPara.createRun();
+                    italicRun.setText(element.text());
+                    italicRun.setItalic(true);
+                    break;
+                case "ul":
+                case "ol":
+                    // 处理列表
+                    for (Element li : element.children()) {
+                        XWPFParagraph listPara = doc.createParagraph();
+                        XWPFRun listRun = listPara.createRun();
+                        listRun.setText("• " + li.text()); // 简单列表符号
+                    }
+                    break;
+                default:
+                    // 默认处理(递归子节点)
+                    for (Node child : node.childNodes()) {
+                        processNode(doc, child);
+                    }
+            }
+        }
+    }
+}

+ 43 - 0
src/main/java/com/njuzr/eaibackend/utils/ZipExport.java

@@ -0,0 +1,43 @@
+package com.njuzr.eaibackend.utils;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.io.*;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class ZipExport {
+    private static final int BUFFER_SIZE = 1024 * 4; // 4KB缓冲区
+
+    /**
+     * 将多个字节数组(如Word文件)打包为ZIP
+     */
+    public static byte[] packToZip(List<ZipItem> items) throws IOException {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+             ZipOutputStream zos = new ZipOutputStream(baos)) {
+            
+            for (ZipItem item : items) {
+                addToZip(zos, item.getFileName(), item.getContent());
+            }
+            zos.finish();
+            return baos.toByteArray();
+        }
+    }
+
+    private static void addToZip(ZipOutputStream zos, String fileName, byte[] content) 
+            throws IOException {
+        ZipEntry entry = new ZipEntry(fileName);
+        zos.putNextEntry(entry);
+        zos.write(content);
+        zos.closeEntry();
+    }
+
+    @Data
+    @AllArgsConstructor
+    public static class ZipItem {
+        private final String fileName;
+        private final byte[] content;
+    }
+}