Selaa lähdekoodia

Merge branch 'feat/behavior_record' of FanYanPeng/EAI-Backend into refactor

https://nqq5jc94uf.feishu.cn/wiki/RmqPwlVTyiShASkMQZBcaa7WnSb 需求已经完成
WuZiLong 5 kuukautta sitten
vanhempi
commit
aebf3d211c

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

@@ -1,14 +1,18 @@
 package com.njuzr.eaibackend.controller;
 
+import com.njuzr.eaibackend.mapper.UserMapper;
+import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.service.ExportCompositionService;
 import com.njuzr.eaibackend.service.ExportBehaviorOverviewService;
 import com.njuzr.eaibackend.service.UserService;
 import com.njuzr.eaibackend.service.AssignmentService;
+import com.njuzr.eaibackend.service.ClassService;
 import com.njuzr.eaibackend.vo.UserVO;
 import com.njuzr.eaibackend.vo.AssignmentVO;
 import freemarker.template.TemplateException;
 import org.apache.commons.lang3.tuple.Pair;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ContentDisposition;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -17,6 +21,7 @@ import org.springframework.web.bind.annotation.*;
 import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.Date;
 import java.util.List;
 
 @RestController
@@ -29,7 +34,11 @@ public class ExportController {
     @Autowired
     private UserService userService;
     @Autowired
+    private UserMapper userMapper;
+    @Autowired
     private AssignmentService assignmentService;
+    @Autowired
+    private ClassService classService;
 
     @GetMapping("/composition/word")
     public ResponseEntity<byte[]> exportWord(@RequestParam Long assignmentId, @RequestParam Long studentId) throws TemplateException, IOException {
@@ -66,18 +75,35 @@ public class ExportController {
 
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
-        // 查询学生姓名
+        // 查询学生信息
         String name = null;
+        String officialNumber = null;
         try {
-            name = userService.getNameById(studentId);
+            User user = userMapper.selectById(studentId);
+            if (user != null) {
+                name = user.getName();
+                officialNumber = user.getOfficialNumber();
+            }
+        } catch (Exception ignored) { }
+        // 查询作业名称
+        String assignmentName = null;
+        try {
+            AssignmentVO vo = assignmentService.findAssignmentById(assignmentId);
+            assignmentName = vo != null ? vo.getAssignmentName() : null;
         } catch (Exception ignored) { }
 
-        String fileName = name == null || name.isEmpty()
-                ? String.format("behavior_overview_%d_%d.xlsx", studentId, assignmentId)
-                : String.format("behavior_overview_%d_%d_%s.xlsx", studentId, assignmentId, name);
+        String fileName;
+        if (name == null || name.isEmpty() || officialNumber == null) {
+            fileName = String.format("行为数据记录_%d_%d.xlsx", studentId, assignmentId);
+        } else if (assignmentName == null || assignmentName.isEmpty()) {
+            fileName = String.format("行为数据记录_%s_%s.xlsx", officialNumber, name);
+        } else {
+            fileName = String.format("行为数据记录_%s_%s_%s.xlsx", officialNumber, name, assignmentName);
+        }
 
-        headers.setContentDispositionFormData("attachment",
-                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+        headers.setContentDisposition(ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build());
 
         return ResponseEntity.ok()
                 .headers(headers)
@@ -97,11 +123,12 @@ public class ExportController {
         } catch (Exception ignored) { }
 
         String fileName = assignmentName == null || assignmentName.isEmpty()
-                ? String.format("behavior_overview_assignment_%d.xlsx", assignmentId)
-                : String.format("behavior_overview_assignment_%d_%s.xlsx", assignmentId, assignmentName);
+                ? String.format("行为数据记录_%d.xlsx", assignmentId)
+                : String.format("行为数据记录_%d_%s.xlsx", assignmentId, assignmentName);
 
-        headers.setContentDispositionFormData("attachment",
-                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+        headers.setContentDisposition(ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build());
 
         return ResponseEntity.ok()
                 .headers(headers)
@@ -126,11 +153,89 @@ public class ExportController {
                 ? String.format("pause_statistics_assignment_%d_threshold_%d.xlsx", assignmentId, threshold)
                 : String.format("pause_statistics_assignment_%d_%s_threshold_%d.xlsx", assignmentId, assignmentName, threshold);
 
-        headers.setContentDispositionFormData("attachment",
-                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+        headers.setContentDisposition(ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build());
 
         return ResponseEntity.ok()
                 .headers(headers)
                 .body(bytes);
     }
+
+    @GetMapping("/behaviorOverview/excel/class")
+    public ResponseEntity<byte[]> exportClassBehaviorOverviewExcel(@RequestParam Long classId) {
+        byte[] bytes = exportBehaviorOverviewService.exportClassBehaviorOverviewExcel(classId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        String className = null;
+        try {
+            com.njuzr.eaibackend.po.Class cls = classService.getClassById(classId);
+            className = cls != null ? cls.getClassName() : null;
+        } catch (Exception ignored) { }
+
+        String fileName = className == null || className.isEmpty()
+                ? String.format("行为数据记录_%d.xlsx", classId)
+                : String.format("行为数据记录_%d_%s.xlsx", classId, className);
+
+        headers.setContentDisposition(ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build());
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(bytes);
+    }
+
+    @GetMapping("/class/download")
+    public ResponseEntity<byte[]> downloadClassAssignments(@RequestParam Long classId) {
+        byte[] zipBytes = exportCompositionService.downloadClassAssignments(classId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        String className = null;
+        try {
+            com.njuzr.eaibackend.po.Class cls = classService.getClassById(classId);
+            className = cls != null ? cls.getClassName() : null;
+        } catch (Exception ignored) { }
+
+        String fileName = className == null || className.isEmpty()
+                ? String.format("class_%d_%s.zip", classId, new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()))
+                : String.format("%s_%s.zip", className, new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()));
+
+        ContentDisposition contentDisposition = ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build();
+        headers.setContentDisposition(contentDisposition);
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(zipBytes);
+    }
+
+    @GetMapping("/assignment/download")
+    public ResponseEntity<byte[]> downloadAssignmentSubmissions(@RequestParam Long assignmentId) {
+        byte[] zipBytes = exportCompositionService.downloadAssignmentSubmissions(assignmentId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        String assignmentName = null;
+        try {
+            AssignmentVO vo = assignmentService.findAssignmentById(assignmentId);
+            assignmentName = vo != null ? vo.getAssignmentName() : null;
+        } catch (Exception ignored) { }
+
+        String fileName = assignmentName == null || assignmentName.isEmpty()
+                ? String.format("assignment_%d_%s.zip", assignmentId, new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()))
+                : String.format("%s_%s.zip", assignmentName, new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()));
+
+        ContentDisposition contentDisposition = ContentDisposition.builder("attachment")
+                .filename(fileName, StandardCharsets.UTF_8)
+                .build();
+        headers.setContentDisposition(contentDisposition);
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(zipBytes);
+    }
 }

+ 3 - 0
src/main/java/com/njuzr/eaibackend/mapper/BehaviorOverviewMapper.java

@@ -15,4 +15,7 @@ public interface BehaviorOverviewMapper extends MongoRepository<BehaviorOverview
 
     // 按作业查询该作业下所有学生的概览
     List<BehaviorOverview> findByAssignmentId(Long assignmentId);
+
+    // 按学生查询该学生的所有概览
+    List<BehaviorOverview> findByStudentId(Long studentId);
 }

+ 86 - 1
src/main/java/com/njuzr/eaibackend/service/ExportBehaviorOverviewService.java

@@ -3,7 +3,9 @@ package com.njuzr.eaibackend.service;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.BehaviorOverviewMapper;
 import com.njuzr.eaibackend.po.BehaviorOverview;
+import com.njuzr.eaibackend.service.ClassService;
 import com.njuzr.eaibackend.vo.BehaviorOverviewVO;
+import com.njuzr.eaibackend.vo.StudentInfoVO;
 import java.util.stream.Collectors;
 import java.util.Set;
 import java.util.Map;
@@ -18,6 +20,7 @@ import org.springframework.stereotype.Service;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.util.List;
 import com.njuzr.eaibackend.vo.PauseStatisticsVO;
 
 /**
@@ -29,12 +32,14 @@ public class ExportBehaviorOverviewService {
     private final BehaviorOverviewService behaviorOverviewService;
     private final BehaviorOverviewMapper behaviorOverviewMapper;
     private final UserService userService;
+    private final ClassService classService;
 
     @Autowired
-    public ExportBehaviorOverviewService(BehaviorOverviewService behaviorOverviewService, BehaviorOverviewMapper behaviorOverviewMapper, UserService userService) {
+    public ExportBehaviorOverviewService(BehaviorOverviewService behaviorOverviewService, BehaviorOverviewMapper behaviorOverviewMapper, UserService userService, ClassService classService) {
         this.behaviorOverviewService = behaviorOverviewService;
         this.behaviorOverviewMapper = behaviorOverviewMapper;
         this.userService = userService;
+        this.classService = classService;
     }
 
     /**
@@ -179,6 +184,86 @@ public class ExportBehaviorOverviewService {
         }
     }
 
+    /**
+     * 导出指定 classId 下所有学生的行为概览数据为 Excel
+     * Sheet1: Summary(每行一个学生概览)
+     * Sheet2..N: 每个学生一个 Events 明细表(按 studentId 命名,避免超长或重复名冲突)
+     */
+    public byte[] exportClassBehaviorOverviewExcel(Long classId) {
+        try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            // Summary 汇总
+            Sheet summary = workbook.createSheet("Summary");
+            int rowIdx = 0;
+            Row header = summary.createRow(rowIdx++);
+            String[] headers = new String[]{
+                    "name", "studentId", "insertCount", "deleteCount", "copyCount", "copyCharacterCount", "longPauseCount"
+            };
+            for (int i = 0; i < headers.length; i++) {
+                header.createCell(i).setCellValue(headers[i]);
+            }
+
+            // 获取班级下的所有学生
+            List<StudentInfoVO> students = classService.getStudentsByClassId(classId);
+            if (students != null && !students.isEmpty()) {
+                // 先批量拉取 studentId -> name 映射,避免 N 次查询
+                Set<Long> studentIds = students.stream()
+                        .map(StudentInfoVO::getStudentId)
+                        .filter(java.util.Objects::nonNull)
+                        .collect(Collectors.toSet());
+                Map<Long, String> idToName = userService.getNamesByIds(studentIds);
+                
+                for (StudentInfoVO student : students) {
+                    Long studentId = student.getStudentId();
+                    if (studentId == null) continue;
+                    
+                    // 查询该学生的所有行为概览
+                    java.util.List<BehaviorOverview> overviews = behaviorOverviewMapper.findByStudentId(studentId);
+                    if (overviews != null && !overviews.isEmpty()) {
+                        for (BehaviorOverview ov : overviews) {
+                            BehaviorOverviewVO vo = behaviorOverviewService.getBehaviorOverview(studentId, ov.getAssignmentId());
+                            Row row = summary.createRow(rowIdx++);
+                            int col = 0;
+                            String name = idToName != null ? idToName.get(studentId) : student.getStuName();
+                            row.createCell(col++).setCellValue(name == null ? "" : name);
+                            row.createCell(col++).setCellValue(vo.getStudentId() == null ? 0 : vo.getStudentId());
+                            row.createCell(col++).setCellValue(vo.getInsertCount() == null ? 0 : vo.getInsertCount());
+                            row.createCell(col++).setCellValue(vo.getDeleteCount() == null ? 0 : vo.getDeleteCount());
+                            row.createCell(col++).setCellValue(vo.getCopyCount() == null ? 0 : vo.getCopyCount());
+                            row.createCell(col++).setCellValue(vo.getCopyCharacterCount() == null ? 0 : vo.getCopyCharacterCount());
+                            row.createCell(col).setCellValue(vo.getLongPauseCount() == null ? 0 : vo.getLongPauseCount());
+
+                            // 为每个学生的每个作业创建一个事件明细sheet,添加作业ID确保名称唯一
+                            String sheetName = safeSheetName("S-" + vo.getStudentId() + "-" + (name != null ? name : "Unknown") + "-A" + vo.getAssignmentId());
+                            Sheet eventsSheet = workbook.createSheet(sheetName);
+                            createEventsSheet(eventsSheet, vo.getStudentId(), vo.getAssignmentId());
+                        }
+                    } else {
+                        // 没有行为概览数据的学生
+                        Row row = summary.createRow(rowIdx++);
+                        int col = 0;
+                        String name = idToName != null ? idToName.get(studentId) : student.getStuName();
+                        row.createCell(col++).setCellValue(name == null ? "" : name);
+                        row.createCell(col++).setCellValue(studentId);
+                        row.createCell(col++).setCellValue(0);
+                        row.createCell(col++).setCellValue(0);
+                        row.createCell(col++).setCellValue(0);
+                        row.createCell(col++).setCellValue(0);
+                        row.createCell(col).setCellValue(0);
+                    }
+                }
+            }
+
+            for (int i = 0; i < headers.length; i++) {
+                summary.autoSizeColumn(i);
+            }
+
+            workbook.write(baos);
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "导出班级行为概览Excel失败" + e.getMessage());
+        }
+    }
+
     private String safeSheetName(String name) {
         // Excel sheet 名称长度限制31,且不允许某些字符
         String n = name.replaceAll("[\\\\/:*?\\n\\r\\[\\]]", "-");

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

@@ -1,15 +1,20 @@
 package com.njuzr.eaibackend.service;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.EngagementMapper;
 import com.njuzr.eaibackend.mapper.serviceIpml.EngagementMapperServiceImpl;
 import com.njuzr.eaibackend.po.Engagement;
+import com.njuzr.eaibackend.po.User;
+import com.njuzr.eaibackend.po.Assignment;
 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;
+import com.njuzr.eaibackend.vo.StudentInfoVO;
+import com.njuzr.eaibackend.vo.EngagementHistoryVO;
 import freemarker.template.Configuration;
 import freemarker.template.TemplateException;
 import lombok.extern.slf4j.Slf4j;
@@ -39,6 +44,10 @@ public class ExportCompositionService {
     private WordExporter wordExporter;
     @Autowired
     private EngagementMapperServiceImpl engagementMapperServiceImpl;
+    @Autowired
+    private com.njuzr.eaibackend.mapper.UserMapper userMapper;
+    @Autowired
+    private com.njuzr.eaibackend.mapper.AssignmentMapper assignmentMapper;
 
     public Pair<UserVO, byte[]> exportWord(Long assignmentId, Long studentId) {
         AssignmentVO assignment = assignmentService.findAssignmentById(assignmentId);
@@ -125,4 +134,123 @@ public class ExportCompositionService {
         return content.replaceAll("\n", "<br/>");
     }
 
+    public byte[] downloadClassAssignments(Long classId) {
+        // 获取班级信息
+        com.njuzr.eaibackend.po.Class cls = classService.getClassById(classId);
+        if (cls == null) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "班级不存在");
+        }
+
+        // 获取班级所有学生
+        List<StudentInfoVO> students = classService.getStudentsByClassId(classId);
+        if (students.isEmpty()) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "班级无学生");
+        }
+
+        // 获取班级对应的课程ID
+        Long courseId = cls.getCourseId();
+        if (courseId == null) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "班级未关联课程");
+        }
+
+        // 获取课程的所有作业
+        List<Assignment> courseAssignments = assignmentMapper.selectByCourseId(courseId);
+        if (courseAssignments.isEmpty()) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "课程无作业");
+        }
+
+        // 生成ZIP文件
+        try {
+            List<ZipExport.ZipItem> zipItems = new ArrayList<>();
+            for (StudentInfoVO student : students) {
+                // 获取学生ID
+                String officialNumber = student.getOfficialNumber();
+                User user = userMapper.selectOne(new QueryWrapper<User>().eq("official_number", officialNumber));
+                if (user == null) {
+                    continue;
+                }
+                Long studentId = user.getId();
+                String studentDir = user.getOfficialNumber() + "_" + user.getName() + "/";
+                
+                // 对每个作业,获取学生的提交历史
+                for (Assignment assignment : courseAssignments) {
+                    Long assignmentId = assignment.getAssignmentId();
+                    try {
+                        // 获取作业历史记录
+                        List<EngagementHistoryVO> historyList = assignmentService.getEngagementHistory(studentId, assignmentId);
+                        for (EngagementHistoryVO history : historyList) {
+                            // 生成Word文档
+                            Map<String, Object> model = new HashMap<>();
+                            model.put("title", assignment.getAssignmentName());
+                            model.put("studentName", user.getName());
+                            model.put("officialNumber", user.getOfficialNumber());
+                            model.put("assignmentName", assignment.getAssignmentName());
+                            model.put("date", history.getSubmitTime());
+                            model.put("content", processContent(history.getTextContent()));
+                            
+                            byte[] wordBytes = wordExporter.exportCompositionWord(model);
+                            String fileName = studentDir + user.getOfficialNumber() + "_" + user.getName() + "_版本" + history.getVersion() + ".docx";
+                            zipItems.add(new ZipExport.ZipItem(fileName, wordBytes));
+                        }
+                    } catch (Exception e) {
+                        // 学生可能未提交该作业,跳过
+                        log.warn("学生 {} 未提交作业 {},跳过", studentId, assignmentId);
+                    }
+                }
+            }
+            return ZipExport.packToZip(zipItems);
+        } catch (IOException e) {
+            throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "ZIP打包失败" + e.getMessage());
+        }
+    }
+
+    public byte[] downloadAssignmentSubmissions(Long assignmentId) {
+        // 获取作业信息
+        AssignmentVO assignment = assignmentService.findAssignmentById(assignmentId);
+        if (assignment == null) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
+        }
+
+        // 获取所有提交该作业的学生
+        List<Engagement> engagements = engagementMapperServiceImpl.getEngagementByAssignmentId(assignmentId);
+        if (engagements.isEmpty()) {
+            throw MyException.create(HttpStatus.BAD_REQUEST, "无学生提交该作业");
+        }
+
+        // 生成ZIP文件
+        try {
+            List<ZipExport.ZipItem> zipItems = new ArrayList<>();
+            for (Engagement engagement : engagements) {
+                Long studentId = engagement.getStudentId();
+                // 获取学生信息
+                UserVO user = userService.searchUsers(new Page<>(1, 1), studentId, null, null, null).getRecords().get(0);
+                String studentDir = user.getOfficialNumber() + "_" + user.getName() + "/";
+                
+                // 获取作业历史记录
+                List<EngagementHistoryVO> historyList = assignmentService.getEngagementHistory(studentId, assignmentId);
+                for (EngagementHistoryVO history : historyList) {
+                    // 生成Word文档
+                    Map<String, Object> model = new HashMap<>();
+                    model.put("title", assignment.getAssignmentName());
+                    model.put("studentName", user.getName());
+                    model.put("officialNumber", user.getOfficialNumber());
+                    model.put("assignmentName", assignment.getAssignmentName());
+                    model.put("date", history.getSubmitTime());
+                    model.put("content", processContent(history.getTextContent()));
+                    
+                    byte[] wordBytes = wordExporter.exportCompositionWord(model);
+                    String fileName = studentDir + user.getOfficialNumber() + "_" + user.getName() + "_版本" + history.getVersion() + ".docx";
+                    zipItems.add(new ZipExport.ZipItem(fileName, wordBytes));
+                }
+            }
+            return ZipExport.packToZip(zipItems);
+        } catch (IOException e) {
+            throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "ZIP打包失败" + e.getMessage());
+        }
+    }
+
+    // 需要注入 ClassService
+    @Autowired
+    private ClassService classService;
+
 }

+ 17 - 0
src/main/java/com/njuzr/eaibackend/vo/BatchStudentImportDetailVO.java

@@ -0,0 +1,17 @@
+package com.njuzr.eaibackend.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class BatchStudentImportDetailVO {
+    private Integer rowIndex;
+    private String officialNumber;
+    private String stuName;
+    private String phone;
+    private String status;
+    private String message;
+}

+ 15 - 0
src/main/java/com/njuzr/eaibackend/vo/BatchStudentImportResultVO.java

@@ -0,0 +1,15 @@
+package com.njuzr.eaibackend.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class BatchStudentImportResultVO {
+    private Integer total;
+    private Integer successCount;
+    private Integer skippedCount;
+    private Integer failedCount;
+    private List<BatchStudentImportDetailVO> details = new ArrayList<>();
+}