ソースを参照

feat:新增excel格式的行为统计数据下载接口

wuzilong 1 年間 前
コミット
aae8974019

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

@@ -1,7 +1,11 @@
 package com.njuzr.eaibackend.controller;
 
 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.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;
@@ -20,6 +24,12 @@ import java.util.List;
 public class ExportController {
     @Autowired
     private ExportCompositionService exportCompositionService;
+    @Autowired
+    private ExportBehaviorOverviewService exportBehaviorOverviewService;
+    @Autowired
+    private UserService userService;
+    @Autowired
+    private AssignmentService assignmentService;
 
     @GetMapping("/composition/word")
     public ResponseEntity<byte[]> exportWord(@RequestParam Long assignmentId, @RequestParam Long studentId) throws TemplateException, IOException {
@@ -49,4 +59,52 @@ public class ExportController {
                 .contentType(MediaType.APPLICATION_OCTET_STREAM)
                 .body(zipBytes);
     }
+
+    @GetMapping("/behaviorOverview/excel")
+    public ResponseEntity<byte[]> exportBehaviorOverviewExcel(@RequestParam Long assignmentId, @RequestParam Long studentId) {
+        byte[] bytes = exportBehaviorOverviewService.exportBehaviorOverviewExcel(assignmentId, studentId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        // 查询学生姓名
+        String name = null;
+        try {
+            name = userService.getNameById(studentId);
+        } 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);
+
+        headers.setContentDispositionFormData("attachment",
+                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(bytes);
+    }
+
+    @GetMapping("/behaviorOverview/excel/assignment")
+    public ResponseEntity<byte[]> exportAssignmentBehaviorOverviewExcel(@RequestParam Long assignmentId) {
+        byte[] bytes = exportBehaviorOverviewService.exportAssignmentBehaviorOverviewExcel(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("behavior_overview_assignment_%d.xlsx", assignmentId)
+                : String.format("behavior_overview_assignment_%d_%s.xlsx", assignmentId, assignmentName);
+
+        headers.setContentDispositionFormData("attachment",
+                URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(bytes);
+    }
 }

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

@@ -2,6 +2,7 @@ package com.njuzr.eaibackend.mapper;
 
 import com.njuzr.eaibackend.po.BehaviorOverview;
 import org.springframework.data.mongodb.repository.MongoRepository;
+import java.util.List;
 import java.util.Optional;
 
 /**
@@ -11,4 +12,7 @@ import java.util.Optional;
 public interface BehaviorOverviewMapper extends MongoRepository<BehaviorOverview, String> {
     // 按学生+作业组合查询
     Optional<BehaviorOverview> findByAssignmentIdAndStudentId(Long assignmentId, Long studentId);
+
+    // 按作业查询该作业下所有学生的概览
+    List<BehaviorOverview> findByAssignmentId(Long assignmentId);
 }

+ 221 - 0
src/main/java/com/njuzr/eaibackend/service/ExportBehaviorOverviewService.java

@@ -0,0 +1,221 @@
+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.vo.BehaviorOverviewVO;
+import java.util.stream.Collectors;
+import java.util.Set;
+import java.util.Map;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * 将 behaviorOverviews 的数据导出为 Excel
+ */
+@Service
+public class ExportBehaviorOverviewService {
+
+    private final BehaviorOverviewService behaviorOverviewService;
+    private final BehaviorOverviewMapper behaviorOverviewMapper;
+    private final UserService userService;
+
+    @Autowired
+    public ExportBehaviorOverviewService(BehaviorOverviewService behaviorOverviewService, BehaviorOverviewMapper behaviorOverviewMapper, UserService userService) {
+        this.behaviorOverviewService = behaviorOverviewService;
+        this.behaviorOverviewMapper = behaviorOverviewMapper;
+        this.userService = userService;
+    }
+
+    /**
+     * 生成Excel字节流,包含两个sheet:Summary 与 Events
+     */
+    public byte[] exportBehaviorOverviewExcel(Long assignmentId, Long studentId) {
+        BehaviorOverviewVO overview = behaviorOverviewService.getBehaviorOverview(studentId, assignmentId);
+
+        try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            // Summary sheet
+            Sheet summary = workbook.createSheet("Summary");
+            createSummarySheet(summary, overview);
+
+            // Events sheet(可选:当事件较大时可能会很长)
+            Sheet eventsSheet = workbook.createSheet("Events");
+            createEventsSheet(eventsSheet, studentId, assignmentId);
+
+            workbook.write(baos);
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "导出Excel失败" + e.getMessage());
+        }
+    }
+
+    /**
+     * 导出指定 assignmentId 下所有学生的数据到一个 Excel
+     * Sheet1: Summary(每行一个学生概览)
+     * Sheet2..N: 每个学生一个 Events 明细表(按 studentId 命名,避免超长或重复名冲突)
+     */
+    public byte[] exportAssignmentBehaviorOverviewExcel(Long assignmentId) {
+        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", "assignmentId", "insertCount", "deleteCount", "copyCount", "copyCharacterCount", "longPauseCount"
+            };
+            for (int i = 0; i < headers.length; i++) {
+                header.createCell(i).setCellValue(headers[i]);
+            }
+
+            // 查询该作业下所有概览
+            java.util.List<BehaviorOverview> overviews = behaviorOverviewMapper.findByAssignmentId(assignmentId);
+            if (overviews != null) {
+                // 先批量拉取 studentId -> name 映射,避免 N 次查询
+                Set<Long> studentIds = overviews.stream()
+                        .map(BehaviorOverview::getStudentId)
+                        .filter(java.util.Objects::nonNull)
+                        .collect(Collectors.toSet());
+                Map<Long, String> idToName = userService.getNamesByIds(studentIds);
+                for (BehaviorOverview ov : overviews) {
+                    BehaviorOverviewVO vo = behaviorOverviewService.getBehaviorOverview(ov.getStudentId(), assignmentId);
+                    Row row = summary.createRow(rowIdx++);
+                    int col = 0;
+                    String name = idToName != null ? idToName.get(vo.getStudentId()) : null;
+                    row.createCell(col++).setCellValue(name == null ? "" : name);
+                    row.createCell(col++).setCellValue(vo.getStudentId() == null ? 0 : vo.getStudentId());
+                    row.createCell(col++).setCellValue(vo.getAssignmentId() == null ? 0 : vo.getAssignmentId());
+                    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
+                    String sheetName = safeSheetName("S-" + vo.getStudentId() + "-" + (name != null ? name : "Unknown"));
+                    Sheet eventsSheet = workbook.createSheet(sheetName);
+                    createEventsSheet(eventsSheet, vo.getStudentId(), assignmentId);
+                }
+            }
+
+            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\\[\\]]", "-");
+        if (n.length() > 31) {
+            n = n.substring(0, 31);
+        }
+        if (n.isEmpty()) {
+            return "Sheet";
+        }
+        return n;
+    }
+
+    private void createSummarySheet(Sheet sheet, BehaviorOverviewVO overview) {
+        int rowIdx = 0;
+        Row header = sheet.createRow(rowIdx++);
+        String[] headers = new String[]{
+                "studentId", "assignmentId", "insertCount", "deleteCount", "copyCount", "copyCharacterCount", "longPauseCount"
+        };
+        for (int i = 0; i < headers.length; i++) {
+            Cell cell = header.createCell(i);
+            cell.setCellValue(headers[i]);
+        }
+
+        Row row = sheet.createRow(rowIdx);
+        int col = 0;
+        row.createCell(col++).setCellValue(overview.getStudentId() == null ? 0 : overview.getStudentId());
+        row.createCell(col++).setCellValue(overview.getAssignmentId() == null ? 0 : overview.getAssignmentId());
+        row.createCell(col++).setCellValue(overview.getInsertCount() == null ? 0 : overview.getInsertCount());
+        row.createCell(col++).setCellValue(overview.getDeleteCount() == null ? 0 : overview.getDeleteCount());
+        row.createCell(col++).setCellValue(overview.getCopyCount() == null ? 0 : overview.getCopyCount());
+        row.createCell(col++).setCellValue(overview.getCopyCharacterCount() == null ? 0 : overview.getCopyCharacterCount());
+        row.createCell(col).setCellValue(overview.getLongPauseCount() == null ? 0 : overview.getLongPauseCount());
+
+        for (int i = 0; i < headers.length; i++) {
+            sheet.autoSizeColumn(i);
+        }
+    }
+
+    private void createEventsSheet(Sheet sheet, Long studentId, Long assignmentId) {
+        int rowIdx = 0;
+        Row header = sheet.createRow(rowIdx++);
+        String[] headers = new String[]{
+                "type", "retain", "length", "time", "insert", "docLength", "letterCount",
+                "key", "timeKey", "startStamp", "startClock", "startTime", "endStamp", "endClock", "endTime",
+                "actionTime", "pauseTime", "delete", "oldContent", "deletedContent"
+        };
+        for (int i = 0; i < headers.length; i++) {
+            header.createCell(i).setCellValue(headers[i]);
+        }
+
+        BehaviorOverview overview = behaviorOverviewMapper
+                .findByAssignmentIdAndStudentId(assignmentId, studentId)
+                .orElse(null);
+
+        if (overview != null && overview.getEvents() != null && !overview.getEvents().isEmpty()) {
+            for (BehaviorOverview.BehaviorEvent e : overview.getEvents()) {
+                Row row = sheet.createRow(rowIdx++);
+                int col = 0;
+                row.createCell(col++).setCellValue(nullableString(e.getType()));
+                row.createCell(col++).setCellValue(nullableInteger(e.getRetain()));
+                row.createCell(col++).setCellValue(nullableInteger(e.getLength()));
+                row.createCell(col++).setCellValue(nullableString(e.getTime()));
+                row.createCell(col++).setCellValue(nullableString(e.getInsert()));
+                row.createCell(col++).setCellValue(nullableInteger(e.getDocLength()));
+                row.createCell(col++).setCellValue(nullableInteger(e.getLetterCount()));
+                row.createCell(col++).setCellValue(nullableString(e.getKey()));
+                row.createCell(col++).setCellValue(nullableString(e.getTimeKey()));
+                row.createCell(col++).setCellValue(nullableLong(e.getStartStamp()));
+                row.createCell(col++).setCellValue(nullableString(e.getStartClock()));
+                row.createCell(col++).setCellValue(nullableString(e.getStartTime()));
+                row.createCell(col++).setCellValue(nullableLong(e.getEndStamp()));
+                row.createCell(col++).setCellValue(nullableString(e.getEndClock()));
+                row.createCell(col++).setCellValue(nullableString(e.getEndTime()));
+                row.createCell(col++).setCellValue(nullableLong(e.getActionTime()));
+                row.createCell(col++).setCellValue(nullableLong(e.getPauseTime()));
+                row.createCell(col++).setCellValue(nullableString(e.getDelete()));
+                row.createCell(col++).setCellValue(nullableString(e.getOldContent()));
+                row.createCell(col).setCellValue(nullableString(e.getDeletedContent()));
+            }
+        }
+
+        for (int i = 0; i < headers.length; i++) {
+            sheet.autoSizeColumn(i);
+        }
+    }
+
+    private String nullableString(String v) {
+        return v == null ? "" : v;
+    }
+
+    private double nullableInteger(Integer v) {
+        return v == null ? 0 : v;
+    }
+
+    private double nullableLong(Long v) {
+        return v == null ? 0 : v;
+    }
+
+    // 已改为批量映射方式,不再需要单查姓名方法
+}
+
+

+ 12 - 1
src/main/java/com/njuzr/eaibackend/service/UserService.java

@@ -6,9 +6,10 @@ import com.njuzr.eaibackend.dto.user.PasswordChangeDTO;
 import com.njuzr.eaibackend.dto.user.UserRegisterDTO;
 import com.njuzr.eaibackend.dto.user.UserUpdateDTO;
 import com.njuzr.eaibackend.enums.Role;
-import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.po.User;
 import com.njuzr.eaibackend.vo.UserVO;
+import java.util.Collection;
+import java.util.Map;
 
 
 /**
@@ -61,4 +62,14 @@ public interface UserService {
      * @return
      */
     boolean userExists(Integer pid);
+
+    /**
+     * 根据ID获取用户名
+     */
+    String getNameById(Long id);
+
+    /**
+     * 批量获取用户名映射
+     */
+    Map<Long, String> getNamesByIds(Collection<Long> ids);
 }

+ 21 - 0
src/main/java/com/njuzr/eaibackend/service/impl/UserServiceImpl.java

@@ -25,6 +25,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 
 @Slf4j
@@ -215,4 +216,24 @@ public class UserServiceImpl implements UserService {
         return true;
     }
 
+    @Override
+    public String getNameById(Long id) {
+        if (id == null) return null;
+        try {
+            return userMapper.selectNameById(id);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Override
+    public Map<Long, String> getNamesByIds(Collection<Long> ids) {
+        if (ids == null || ids.isEmpty()) return Collections.emptyMap();
+        QueryWrapper<User> wrapper = new QueryWrapper<>();
+        wrapper.in("id", ids);
+        List<User> users = userMapper.selectList(wrapper);
+        if (users == null || users.isEmpty()) return Collections.emptyMap();
+        return users.stream().collect(Collectors.toMap(User::getId, User::getName, (a,b)->a));
+    }
+
 }