Преглед изворни кода

feat:实现作业导出word

baiqi пре 1 година
родитељ
комит
1f28ddb75e

+ 11 - 0
pom.xml

@@ -187,6 +187,17 @@
             <version>3.14.2</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-freemarker</artifactId>
+        </dependency>
+
+        <!-- Jsoup HTML解析 -->
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+            <version>1.15.3</version>
+        </dependency>
 
     </dependencies>
 

+ 2 - 0
src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java

@@ -58,6 +58,8 @@ public class SecurityConfig {
                                 "/api/user/verifyCode",
                                 "/api/onlyoffice/**",
                                 "/api/assignment/**",
+                                "/api/export/**",
+                                "/api/test",
                                 "api/auth/loginByPortal").permitAll() // 允许公开访问的路径
                         .anyRequest().authenticated() // 其他所有请求需要认证
                 )

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

@@ -0,0 +1,37 @@
+package com.njuzr.eaibackend.controller;
+
+import com.njuzr.eaibackend.service.ExportCompositionService;
+import freemarker.template.TemplateException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+
+@RestController
+@RequestMapping("/api/export")
+public class ExportController {
+    @Autowired
+    private ExportCompositionService exportCompositionService;
+
+    @GetMapping("/composition/word")
+    public ResponseEntity<byte[]> exportWord(@RequestParam Long assignmentId, @RequestParam Long studentId) throws TemplateException, IOException {
+
+        byte[] wordBytes = exportCompositionService.exportWord(1L,1L);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        headers.setContentDispositionFormData("attachment",
+                URLEncoder.encode("作业" + ".docx", "UTF-8"));
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(wordBytes);
+    }
+}

+ 190 - 0
src/main/java/com/njuzr/eaibackend/service/ExportCompositionService.java

@@ -0,0 +1,190 @@
+package com.njuzr.eaibackend.service;
+
+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.vo.AssignmentVO;
+import com.njuzr.eaibackend.vo.EngagementVO;
+import com.njuzr.eaibackend.vo.UserVO;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+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.Service;
+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;
+
+@Slf4j
+@Service
+public class ExportCompositionService {
+
+    @Autowired
+    private Configuration freemarkerConfig;
+    @Autowired
+    private EngagementMapper engagementMapper;
+    @Autowired
+    private AssignmentService assignmentService;
+    @Autowired
+    private UserService userService;
+
+    public byte[] exportWord(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);
+
+        Map<String, Object> model = new HashMap<>();
+        model.put("title", assignment.getAssignmentName());
+        model.put("studentName", user.getName());
+        model.put("assignmentName", assignment.getAssignmentName());
+        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 out.toByteArray();
+
+    }
+
+
+    public byte[] exportWordTest(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" +
+                "During this season, my family often goes hiking or has picnics. The Mid-Autumn Festival, when we eat mooncakes and admire the full moon, is my favorite holiday. Autumn reminds me of warmth and joy.\n" +
+                "In short, autumn is not only pleasant but also full of happy memories. That’s why I love it most.";
+
+        Map<String, Object> model = new HashMap<>();
+        model.put("title", "title_"+assignmentName);
+        model.put("studentName", "柏琪");
+        model.put("assignmentName", assignmentName);
+        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();
+    }
+
+    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);
+                    }
+            }
+        }
+    }
+
+}

+ 12 - 0
src/main/resources/templates/composition.ftl

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<h1>${title}</h1>
+<p>学生: ${studentName}</p>
+<p>班级: ${assignmentName}</p>
+<p>日期: ${date?string('yyyy-MM-dd')}</p>
+<div>
+    ${content}
+</div>
+</body>
+</html>