Parcourir la source

feat:新增behaviorOverviews集合存放整合处理之后的行为数据,新增对应的接口

wuzilong il y a 1 an
Parent
commit
f37d71d967

+ 12 - 0
src/main/java/com/njuzr/eaibackend/config/ThreadPoolConfig.java

@@ -76,6 +76,18 @@ public class ThreadPoolConfig {
         return executor;
     }
 
+    @Bean("behaviorOverviewTaskExecutor")
+    public ThreadPoolTaskExecutor behaviorOverviewTaskExecutor() {
+        ThreadPoolTaskExecutor executor = getExecutor();
+        executor.setCorePoolSize(20);
+        executor.setMaxPoolSize(50);
+        executor.setQueueCapacity(100);
+        executor.setThreadNamePrefix("behavior-overview-");
+        executor.initialize();
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
     private static ThreadPoolTaskExecutor getExecutor() {
         return new ThreadPoolTaskExecutor() {
             @Override

+ 36 - 1
src/main/java/com/njuzr/eaibackend/controller/BehaviorRecordController.java

@@ -1,6 +1,8 @@
 package com.njuzr.eaibackend.controller;
 
 import com.njuzr.eaibackend.service.BehaviorRecordService;
+import com.njuzr.eaibackend.service.BehaviorOverviewService;
+import com.njuzr.eaibackend.vo.BehaviorOverviewVO;
 import com.njuzr.eaibackend.vo.TextAnalysisVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -17,10 +19,13 @@ import org.springframework.web.multipart.MultipartFile;
 @RequestMapping("/api/behaviorRecord")
 public class BehaviorRecordController {
     private final BehaviorRecordService behaviorRecordService;
+    private final BehaviorOverviewService behaviorOverviewService;
 
     @Autowired
-    public BehaviorRecordController(BehaviorRecordService behaviorRecordService) {
+    public BehaviorRecordController(BehaviorRecordService behaviorRecordService, 
+            BehaviorOverviewService behaviorOverviewService) {
         this.behaviorRecordService = behaviorRecordService;
+        this.behaviorOverviewService = behaviorOverviewService;
     }
 
     @GetMapping()
@@ -46,6 +51,14 @@ public class BehaviorRecordController {
         return MyResponse.success(vo);
     }
 
+    @GetMapping("/overview")
+    public MyResponse getBehaviorOverview(
+            @RequestParam Long studentId,
+            @RequestParam Long assignmentId) {
+        BehaviorOverviewVO vo = behaviorOverviewService.getBehaviorOverview(studentId, assignmentId);
+        return MyResponse.success(vo);
+    }
+
     @PostMapping()
     public MyResponse addBehaviorRecord(
             @AuthenticationPrincipal(expression = "id") Long studentId,
@@ -54,4 +67,26 @@ public class BehaviorRecordController {
     ){
         return MyResponse.success(behaviorRecordService.upload(assignmentId, studentId, file));
     }
+
+    /**
+     * 手动迁移整理所有behaviorRecords数据到behaviorOverviews集合
+     */
+    @PostMapping("/migrate/all")
+    public MyResponse migrateBehaviorRecordsToOverviews() {
+        log.info("开始执行全量数据迁移请求");
+        String result = behaviorOverviewService.migrateBehaviorRecordsToOverviews();
+        return MyResponse.success(result);
+    }
+
+    /**
+     * 迁移整理指定学生和作业的behaviorRecords数据到behaviorOverviews集合
+     */
+    @PostMapping("/migrate/specific")
+    public MyResponse migrateBehaviorRecordsToOverviews(
+            @RequestParam Long studentId,
+            @RequestParam Long assignmentId) {
+        log.info("开始执行指定数据迁移请求: studentId={}, assignmentId={}", studentId, assignmentId);
+        String result = behaviorOverviewService.migrateBehaviorRecordsToOverviews(studentId, assignmentId);
+        return MyResponse.success(result);
+    }
 }

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

@@ -0,0 +1,14 @@
+package com.njuzr.eaibackend.mapper;
+
+import com.njuzr.eaibackend.po.BehaviorOverview;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import java.util.Optional;
+
+/**
+ * @author WuZilong
+ * @date 2025-08-09 17:02
+ */
+public interface BehaviorOverviewMapper extends MongoRepository<BehaviorOverview, String> {
+    // 按学生+作业组合查询
+    Optional<BehaviorOverview> findByAssignmentIdAndStudentId(Long assignmentId, Long studentId);
+}

+ 93 - 0
src/main/java/com/njuzr/eaibackend/po/BehaviorOverview.java

@@ -0,0 +1,93 @@
+package com.njuzr.eaibackend.po;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.CompoundIndexes;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Auther: WuZilong
+ * @Date: 2025/8/9 14:51
+ * @Description:
+ */
+@Data
+@Document(collection = "behaviorOverviews")
+@CompoundIndexes({
+        // 复合唯一索引:确保 student_id + assignment_id 的业务唯一性
+        @CompoundIndex(
+                name = "student_assignment_unique",
+                def = "{'student_id': 1, 'assignment_id': 1}",
+                unique = true,
+                background = true
+        )
+})
+public class BehaviorOverview {
+
+    @Id
+    private String id; // MongoDB自动生成的ObjectId
+
+    @Field("student_id")
+    private Long studentId; // 学生ID
+
+    @Field("assignment_id")
+    private Long assignmentId; // 作业ID
+
+    @Field("created_at")
+//    @Indexed
+    private Date createdAt; // (student_id,assignment_id)第一条记录的创建时间
+
+    private List<BehaviorEvent> events; // 行为事件集合
+    
+    // 统计信息字段
+    private Integer insertCount; // type为insert的次数
+    private Integer deleteCount; // type为delete的次数
+    private Integer copyCount; // type为copy的次数
+    private Integer copyCharacterCount; // type为copy的所有条目中,insert字段对应的字符总数
+    private Integer longPauseCount; // pauseTime大于2000ms的次数
+
+    /**
+     * 嵌套行为事件类
+     */
+    @Data
+    public static class BehaviorEvent {
+        // 基础事件信息(按JSON顺序排列)
+        private String type;           // 事件类型(cursor_move/insert/delete等)
+        private Integer retain;        // 光标位置
+        private Integer length;        // 操作长度
+        private String time;           // 事件时间(字符串格式)
+
+        // 文本操作相关
+        private String insert;         // 插入的文本内容
+        private Integer docLength;     // 文档总长度
+        private Integer letterCount;   // 字母计数
+
+        // 键盘输入相关
+        private String key;            // 按键值
+        private String timeKey;        // 按键时间
+
+        // 时间戳信息
+        private Long startStamp;     // 开始时间戳
+        private String startClock;     // 开始时钟时间
+        private String startTime;      // 开始时间字符串
+
+        // 结束时间信息
+        private Long endStamp;       // 结束时间戳
+        private String endClock;       // 结束时钟时间
+        private String endTime;        // 结束时间字符串
+
+        // 时间统计
+        private Long actionTime;     // 操作耗时(毫秒)
+        private Long pauseTime;      // 停留时间(毫秒)
+
+        // 删除操作相关
+        private String delete;         // 删除操作类型
+        private String oldContent;     // 旧内容
+        private String deletedContent; // 删除的内容
+    }
+
+}

+ 34 - 0
src/main/java/com/njuzr/eaibackend/service/BehaviorOverviewService.java

@@ -0,0 +1,34 @@
+package com.njuzr.eaibackend.service;
+
+import com.njuzr.eaibackend.po.BehaviorRecord;
+import com.njuzr.eaibackend.vo.BehaviorOverviewVO;
+
+import java.util.List;
+
+/**
+ * BehaviorOverview 业务服务接口
+ * @author WuZilong
+ * @date 2025-08-10
+ */
+public interface BehaviorOverviewService {
+    
+    /**
+     * 异步更新BehaviorOverview集合
+     */
+    void updateBehaviorOverviewAsync(Long assignmentId, Long studentId, List<BehaviorRecord.BehaviorEvent> events);
+    
+    /**
+     * 手动迁移所有behaviorRecords数据到behaviorOverviews集合
+     */
+    String migrateBehaviorRecordsToOverviews();
+    
+    /**
+     * 迁移指定学生和作业的behaviorRecords数据到behaviorOverviews集合
+     */
+    String migrateBehaviorRecordsToOverviews(Long studentId, Long assignmentId);
+    
+    /**
+     * 获取BehaviorOverview统计数据
+     */
+    BehaviorOverviewVO getBehaviorOverview(Long studentId, Long assignmentId);
+}

+ 3 - 0
src/main/java/com/njuzr/eaibackend/service/BehaviorRecordService.java

@@ -2,6 +2,7 @@ package com.njuzr.eaibackend.service;
 
 import com.njuzr.eaibackend.po.BehaviorRecord;
 import com.njuzr.eaibackend.vo.BehaviorRecordVO;
+import com.njuzr.eaibackend.vo.BehaviorOverviewVO;
 import com.njuzr.eaibackend.vo.TextAnalysisVO;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -21,4 +22,6 @@ public interface BehaviorRecordService {
      * 查询MongoDB中textAnalyses分析结果
      */
     TextAnalysisVO getTextAnalysis(Long studentId, Long assignmentId);
+
+
 }

+ 439 - 0
src/main/java/com/njuzr/eaibackend/service/impl/BehaviorOverviewServiceImpl.java

@@ -0,0 +1,439 @@
+package com.njuzr.eaibackend.service.impl;
+
+import com.njuzr.eaibackend.mapper.BehaviorRecordMapper;
+import com.njuzr.eaibackend.mapper.BehaviorOverviewMapper;
+import com.njuzr.eaibackend.po.BehaviorRecord;
+import com.njuzr.eaibackend.po.BehaviorOverview;
+import com.njuzr.eaibackend.service.BehaviorOverviewService;
+import com.njuzr.eaibackend.vo.BehaviorOverviewVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.data.domain.Sort;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * BehaviorOverview 业务服务实现类
+ * @author WuZilong   
+ * @date 2025-08-10
+ */
+@Service
+@Slf4j
+public class BehaviorOverviewServiceImpl implements BehaviorOverviewService {
+
+    private final BehaviorRecordMapper behaviorRecordMapper;
+    private final BehaviorOverviewMapper behaviorOverviewMapper;
+
+    @Autowired
+    public BehaviorOverviewServiceImpl(BehaviorRecordMapper behaviorRecordMapper, 
+            BehaviorOverviewMapper behaviorOverviewMapper) {
+        this.behaviorRecordMapper = behaviorRecordMapper;
+        this.behaviorOverviewMapper = behaviorOverviewMapper;
+    }
+
+    /**
+     * 异步更新BehaviorOverview集合
+     */
+    @Override
+    @Async("behaviorOverviewTaskExecutor")
+    public void updateBehaviorOverviewAsync(Long assignmentId, Long studentId, List<BehaviorRecord.BehaviorEvent> events) {
+        try {
+            log.info("开始异步更新BehaviorOverview: studentId={}, assignmentId={}, eventsCount={}", 
+                    studentId, assignmentId, events.size());
+            
+            long startTime = System.currentTimeMillis();
+            updateBehaviorOverview(assignmentId, studentId, events);
+            long endTime = System.currentTimeMillis();
+            
+            log.info("异步更新BehaviorOverview完成: studentId={}, assignmentId={}, 耗时={}ms", 
+                    studentId, assignmentId, (endTime - startTime));
+        } catch (Exception e) {
+            log.error("异步更新BehaviorOverview失败: studentId={}, assignmentId={}, error={}", 
+                    studentId, assignmentId, e.getMessage(), e);
+            // 这里可以添加失败重试逻辑、死信队列等处理方案
+        }
+    }
+    
+    /**
+     * 更新BehaviorOverview集合
+     */
+    private void updateBehaviorOverview(Long assignmentId, Long studentId, List<BehaviorRecord.BehaviorEvent> newEvents) {
+        // 查找现有的BehaviorOverview记录
+        Optional<BehaviorOverview> existingOverview = behaviorOverviewMapper
+                .findByAssignmentIdAndStudentId(assignmentId, studentId);
+        
+        BehaviorOverview overview;
+        Long timestampOffset = 0L; // 时间戳偏移量
+        
+        if (existingOverview.isPresent()) {
+            // 更新现有记录 - 不修改createdAt字段
+            overview = existingOverview.get();
+            // 合并新事件到现有事件列表
+            if (overview.getEvents() == null) {
+                overview.setEvents(new ArrayList<>());
+            }
+            
+            // 计算时间戳偏移量
+            timestampOffset = calculateTimestampOffsetForUpdate(overview.getEvents(), newEvents);
+            
+            // 添加新事件(应用时间戳偏移量)
+            overview.getEvents().addAll(convertToOverviewEventsWithOffset(newEvents, timestampOffset));
+            
+            // 🚀 使用增量更新统计数据(性能优化)
+            incrementalUpdateStatistics(overview, newEvents);
+        } else {
+            // 创建新记录 - 只有在这里才设置createdAt字段
+            overview = new BehaviorOverview();
+            overview.setStudentId(studentId);
+            overview.setAssignmentId(assignmentId);
+            overview.setCreatedAt(new Date()); // 记录(student_id,assignment_id)第一条记录的创建时间
+            overview.setEvents(convertToOverviewEventsWithOffset(newEvents, 0L));
+            
+            // 💡 新建记录时使用增量更新(从0开始累加)
+            incrementalUpdateStatistics(overview, newEvents);
+        }
+        
+        // 保存或更新
+        behaviorOverviewMapper.save(overview);
+    }
+    
+    /**
+     * 计算时间戳偏移量(用于更新现有Overview)
+     */
+    private Long calculateTimestampOffsetForUpdate(List<BehaviorOverview.BehaviorEvent> existingEvents, 
+                                                   List<BehaviorRecord.BehaviorEvent> newEvents) {
+        // 获取现有事件中最后一个非null的endStamp
+        Long lastEndStamp = getLastEndStampFromOverviewEvents(existingEvents);
+        if (lastEndStamp == null) {
+            return 0L; // 如果没有有效的endStamp,不进行偏移
+        }
+        
+        // 获取新事件中第一个非null的startStamp
+        Long firstStartStamp = getFirstStartStampFromRecordEvents(newEvents);
+        if (firstStartStamp == null) {
+            return 0L; // 如果没有有效的startStamp,不进行偏移
+        }
+        
+        // 如果新事件的第一个startStamp小于现有事件的最后endStamp,需要偏移
+        if (firstStartStamp < lastEndStamp) {
+            return lastEndStamp;
+        }
+        
+        return 0L; // 时间戳连续,无需偏移
+    }
+    
+    /**
+     * 获取Overview事件列表中最后一个非null的endStamp
+     */
+    private Long getLastEndStampFromOverviewEvents(List<BehaviorOverview.BehaviorEvent> events) {
+        if (events == null || events.isEmpty()) {
+            return null;
+        }
+        
+        for (int i = events.size() - 1; i >= 0; i--) {
+            BehaviorOverview.BehaviorEvent event = events.get(i);
+            if (event.getEndStamp() != null) {
+                return event.getEndStamp();
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * 获取Record事件列表中第一个非null的startStamp
+     */
+    private Long getFirstStartStampFromRecordEvents(List<BehaviorRecord.BehaviorEvent> events) {
+        if (events == null || events.isEmpty()) {
+            return null;
+        }
+        
+        for (BehaviorRecord.BehaviorEvent event : events) {
+            if (event.getStartStamp() != null) {
+                return event.getStartStamp();
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * 将BehaviorRecord.BehaviorEvent转换为BehaviorOverview.BehaviorEvent(带时间戳偏移量)
+     */
+    private List<BehaviorOverview.BehaviorEvent> convertToOverviewEventsWithOffset(List<BehaviorRecord.BehaviorEvent> recordEvents, Long timestampOffset) {
+        List<BehaviorOverview.BehaviorEvent> overviewEvents = new ArrayList<>();
+        
+        for (BehaviorRecord.BehaviorEvent recordEvent : recordEvents) {
+            BehaviorOverview.BehaviorEvent overviewEvent = new BehaviorOverview.BehaviorEvent();
+            
+            // 复制基础字段
+            overviewEvent.setType(recordEvent.getType());
+            overviewEvent.setRetain(recordEvent.getRetain());
+            overviewEvent.setLength(recordEvent.getLength());
+            overviewEvent.setTime(recordEvent.getTime());
+            overviewEvent.setInsert(recordEvent.getInsert());
+            overviewEvent.setDocLength(recordEvent.getDocLength());
+            overviewEvent.setLetterCount(recordEvent.getLetterCount());
+            overviewEvent.setKey(recordEvent.getKey());
+            overviewEvent.setTimeKey(recordEvent.getTimeKey());
+            overviewEvent.setStartClock(recordEvent.getStartClock());
+            overviewEvent.setStartTime(recordEvent.getStartTime());
+            overviewEvent.setEndClock(recordEvent.getEndClock());
+            overviewEvent.setEndTime(recordEvent.getEndTime());
+            overviewEvent.setActionTime(recordEvent.getActionTime());
+            overviewEvent.setPauseTime(recordEvent.getPauseTime());
+            overviewEvent.setDelete(recordEvent.getDelete());
+            overviewEvent.setOldContent(recordEvent.getOldContent());
+            overviewEvent.setDeletedContent(recordEvent.getDeletedContent());
+            
+            // 应用时间戳偏移量
+            if (recordEvent.getStartStamp() != null) {
+                overviewEvent.setStartStamp(recordEvent.getStartStamp() + timestampOffset);
+            } else {
+                overviewEvent.setStartStamp(null);
+            }
+            
+            if (recordEvent.getEndStamp() != null) {
+                overviewEvent.setEndStamp(recordEvent.getEndStamp() + timestampOffset);
+            } else {
+                overviewEvent.setEndStamp(null);
+            }
+            
+            overviewEvents.add(overviewEvent);
+        }
+        
+        return overviewEvents;
+    }
+    
+    /**
+     * 将BehaviorRecord.BehaviorEvent转换为BehaviorOverview.BehaviorEvent
+     */
+    private List<BehaviorOverview.BehaviorEvent> convertToOverviewEvents(List<BehaviorRecord.BehaviorEvent> recordEvents) {
+        return convertToOverviewEventsWithOffset(recordEvents, 0L);
+    }
+    
+    /**
+     * 增量更新统计数据
+     */
+    private void incrementalUpdateStatistics(BehaviorOverview overview, List<BehaviorRecord.BehaviorEvent> newEvents) {
+        if (newEvents == null || newEvents.isEmpty()) {
+            return;
+        }
+        
+        // 只计算新增事件的统计
+        int newInsertCount = 0;
+        int newDeleteCount = 0;
+        int newCopyCount = 0;
+        int newCopyCharacterCount = 0;
+        int newLongPauseCount = 0;
+        
+        // 只遍历新增的事件
+        for (BehaviorRecord.BehaviorEvent event : newEvents) {
+            String type = event.getType();
+            if ("insert".equals(type)) {
+                newInsertCount++;
+            } else if ("delete".equals(type)) {
+                newDeleteCount++;
+            } else if ("copy".equals(type)) {
+                newCopyCount++;
+                // 计算copy操作的字符数
+                if (event.getInsert() != null) {
+                    newCopyCharacterCount += event.getInsert().length();
+                }
+            }
+            
+            // 计算pauseTime大于2000ms的次数
+            if (event.getPauseTime() != null && event.getPauseTime() > 2000) {
+                newLongPauseCount++;
+            }
+        }
+        
+        // 累加到现有统计数据上(处理null值)
+        overview.setInsertCount(safeAdd(overview.getInsertCount(), newInsertCount));
+        overview.setDeleteCount(safeAdd(overview.getDeleteCount(), newDeleteCount));
+        overview.setCopyCount(safeAdd(overview.getCopyCount(), newCopyCount));
+        overview.setCopyCharacterCount(safeAdd(overview.getCopyCharacterCount(), newCopyCharacterCount));
+        overview.setLongPauseCount(safeAdd(overview.getLongPauseCount(), newLongPauseCount));
+        
+        log.debug("增量更新统计: +insert={}, +delete={}, +copy={}, +copyChar={}, +longPause={}", 
+                newInsertCount, newDeleteCount, newCopyCount, newCopyCharacterCount, newLongPauseCount);
+    }
+    
+    /**
+     * 安全的数字相加,处理null值
+     */
+    private Integer safeAdd(Integer existing, Integer addition) {
+        return (existing != null ? existing : 0) + (addition != null ? addition : 0);
+    }
+
+    @Override
+    public String migrateBehaviorRecordsToOverviews() {
+        try {
+            log.info("开始迁移所有behaviorRecords数据到behaviorOverviews集合");
+            long startTime = System.currentTimeMillis();
+            
+            // 获取所有BehaviorRecord记录
+            List<BehaviorRecord> allRecords = behaviorRecordMapper.findAll();
+            log.info("找到{}条behaviorRecords记录需要迁移", allRecords.size());
+            
+            // 按(studentId, assignmentId)分组
+            Map<String, List<BehaviorRecord>> groupedRecords = allRecords.stream()
+                    .collect(Collectors.groupingBy(record -> 
+                            record.getStudentId() + "_" + record.getAssignmentId()));
+            
+            int processedGroups = 0;
+            int totalEvents = 0;
+            
+            for (Map.Entry<String, List<BehaviorRecord>> entry : groupedRecords.entrySet()) {
+                String key = entry.getKey();
+                List<BehaviorRecord> records = entry.getValue();
+                
+                // 解析studentId和assignmentId
+                String[] parts = key.split("_");
+                Long studentId = Long.valueOf(parts[0]);
+                Long assignmentId = Long.valueOf(parts[1]);
+                
+                // 迁移这一组数据
+                migrateBehaviorRecordsToOverviews(studentId, assignmentId);
+                processedGroups++;
+                
+                // 统计事件数量
+                int eventsCount = records.stream()
+                        .mapToInt(record -> record.getEvents() != null ? record.getEvents().size() : 0)
+                        .sum();
+                totalEvents += eventsCount;
+                
+                log.info("已处理第{}组: studentId={}, assignmentId={}, 记录数={}, 事件数={}", 
+                        processedGroups, studentId, assignmentId, records.size(), eventsCount);
+            }
+            
+            long endTime = System.currentTimeMillis();
+            String message = String.format("迁移完成!处理了%d组数据,总计%d个事件,耗时%dms", 
+                    processedGroups, totalEvents, (endTime - startTime));
+            
+            log.info(message);
+            return message;
+            
+        } catch (Exception e) {
+            log.error("迁移behaviorRecords数据失败: {}", e.getMessage(), e);
+            return "迁移失败: " + e.getMessage();
+        }
+    }
+
+    @Override
+    public String migrateBehaviorRecordsToOverviews(Long studentId, Long assignmentId) {
+        try {
+            log.info("开始迁移指定数据: studentId={}, assignmentId={}", studentId, assignmentId);
+            
+            // 检查behaviorOverviews中是否已存在该记录
+            Optional<BehaviorOverview> existingOverview = behaviorOverviewMapper
+                    .findByAssignmentIdAndStudentId(assignmentId, studentId);
+            
+            if (existingOverview.isPresent()) {
+                String message = String.format("BehaviorOverview已存在: studentId=%d, assignmentId=%d,跳过迁移", 
+                        studentId, assignmentId);
+                log.info(message);
+                return message;
+            }
+            
+            // 获取该(studentId, assignmentId)的所有BehaviorRecord记录,按创建时间排序
+            Sort sort = Sort.by(Sort.Direction.ASC, "created_at");
+            List<BehaviorRecord> records = behaviorRecordMapper.findByAssignmentIdAndStudentId(assignmentId, studentId, sort);
+            
+            if (records.isEmpty()) {
+                String message = String.format("未找到相关behaviorRecords数据: studentId=%d, assignmentId=%d", 
+                        studentId, assignmentId);
+                log.info(message);
+                return message;
+            }
+            
+            // 创建新的BehaviorOverview
+            BehaviorOverview overview = new BehaviorOverview();
+            overview.setStudentId(studentId);
+            overview.setAssignmentId(assignmentId);
+            overview.setCreatedAt(records.get(0).getCreatedAt()); // 使用第一条记录的创建时间
+            overview.setEvents(new ArrayList<>());
+            
+            // 依次处理每条记录,确保时间戳连续性
+            for (BehaviorRecord record : records) {
+                if (record.getEvents() != null && !record.getEvents().isEmpty()) {
+                    // 计算时间戳偏移量
+                    Long timestampOffset = calculateTimestampOffsetForUpdate(overview.getEvents(), record.getEvents());
+                    
+                    // 添加事件(应用时间戳偏移量)
+                    overview.getEvents().addAll(convertToOverviewEventsWithOffset(record.getEvents(), timestampOffset));
+                    
+                    // 🚀 使用增量更新统计数据
+                    incrementalUpdateStatistics(overview, record.getEvents());
+                }
+            }
+            
+            // 保存到behaviorOverviews集合
+            behaviorOverviewMapper.save(overview);
+            
+            String message = String.format("迁移成功: studentId=%d, assignmentId=%d, 记录数=%d, 事件数=%d", 
+                    studentId, assignmentId, records.size(), overview.getEvents().size());
+            log.info(message);
+            return message;
+            
+        } catch (Exception e) {
+            String errorMessage = String.format("迁移指定数据失败: studentId=%d, assignmentId=%d, error=%s", 
+                    studentId, assignmentId, e.getMessage());
+            log.error(errorMessage, e);
+            return errorMessage;
+        }
+    }
+
+    @Override
+    public BehaviorOverviewVO getBehaviorOverview(Long studentId, Long assignmentId) {
+        try {
+            log.info("获取BehaviorOverview统计数据: studentId={}, assignmentId={}", studentId, assignmentId);
+            
+            // 查询behaviorOverviews集合中的数据
+            Optional<BehaviorOverview> overviewOptional = behaviorOverviewMapper
+                    .findByAssignmentIdAndStudentId(assignmentId, studentId);
+            
+            if (overviewOptional.isPresent()) {
+                BehaviorOverview overview = overviewOptional.get();
+                
+                // 转换为VO对象
+                BehaviorOverviewVO vo = new BehaviorOverviewVO();
+                vo.setStudentId(overview.getStudentId());
+                vo.setAssignmentId(overview.getAssignmentId());
+                vo.setInsertCount(overview.getInsertCount());
+                vo.setDeleteCount(overview.getDeleteCount());
+                vo.setCopyCount(overview.getCopyCount());
+                vo.setCopyCharacterCount(overview.getCopyCharacterCount());
+                vo.setLongPauseCount(overview.getLongPauseCount());
+                
+                log.info("成功获取BehaviorOverview数据: studentId={}, assignmentId={}, insertCount={}, deleteCount={}, copyCount={}, copyCharacterCount={}, longPauseCount={}", 
+                        studentId, assignmentId, vo.getInsertCount(), vo.getDeleteCount(), vo.getCopyCount(), vo.getCopyCharacterCount(), vo.getLongPauseCount());
+                
+                return vo;
+            } else {
+                log.info("未找到BehaviorOverview数据: studentId={}, assignmentId={}", studentId, assignmentId);
+                return createEmptyBehaviorOverviewVO(studentId, assignmentId);
+            }
+            
+        } catch (Exception e) {
+            log.error("获取BehaviorOverview数据失败: studentId={}, assignmentId={}, error={}", 
+                    studentId, assignmentId, e.getMessage(), e);
+            return createEmptyBehaviorOverviewVO(studentId, assignmentId);
+        }
+    }
+    
+    /**
+     * 创建空的BehaviorOverviewVO对象
+     */
+    private BehaviorOverviewVO createEmptyBehaviorOverviewVO(Long studentId, Long assignmentId) {
+        BehaviorOverviewVO vo = new BehaviorOverviewVO();
+        vo.setStudentId(studentId);
+        vo.setAssignmentId(assignmentId);
+        return vo;
+    }
+}

+ 12 - 4
src/main/java/com/njuzr/eaibackend/service/impl/BehaviorRecordServiceImpl.java

@@ -1,10 +1,10 @@
 package com.njuzr.eaibackend.service.impl;
 
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.BehaviorRecordMapper;
 import com.njuzr.eaibackend.po.BehaviorRecord;
 import com.njuzr.eaibackend.service.BehaviorRecordService;
+import com.njuzr.eaibackend.service.BehaviorOverviewService;
 import com.njuzr.eaibackend.utils.OssUtil;
 import com.njuzr.eaibackend.vo.BehaviorRecordVO;
 import com.njuzr.eaibackend.po.TextAnalysis;
@@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
 import java.io.IOException;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -39,13 +40,16 @@ import java.util.Map;
 public class BehaviorRecordServiceImpl implements BehaviorRecordService {
 
     private final BehaviorRecordMapper behaviorRecordMapper;
+    private final BehaviorOverviewService behaviorOverviewService;
     private final ObjectMapper objectMapper;
     private final MongoTemplate mongoTemplate;
 
     @Autowired
-    public BehaviorRecordServiceImpl(BehaviorRecordMapper behaviorRecordMapper, OssUtil ossUtil,
+    public BehaviorRecordServiceImpl(BehaviorRecordMapper behaviorRecordMapper, 
+            BehaviorOverviewService behaviorOverviewService, OssUtil ossUtil,
             MongoTemplate mongoTemplate) {
         this.behaviorRecordMapper = behaviorRecordMapper;
+        this.behaviorOverviewService = behaviorOverviewService;
         this.objectMapper = new ObjectMapper();
         this.mongoTemplate = mongoTemplate;
     }
@@ -166,11 +170,15 @@ public class BehaviorRecordServiceImpl implements BehaviorRecordService {
             record.setAssignmentId(assignmentId);
             record.setEvents(events);
 
-            // 保存到MongoDB
+            // 保存到MongoDB (behaviorRecords集合) 
             behaviorRecordMapper.save(record);
+            
+            // 异步更新BehaviorOverview - 通过新服务调用确保异步生效
+            behaviorOverviewService.updateBehaviorOverviewAsync(assignmentId, studentId, events);
+            
             return "行为数据保存成功";
         } catch (IOException e) {
-            log.error(e.getMessage());
+            log.error("行为数据保存失败,数据解析处理错误: {}", e.getMessage());
             throw MyException.create(HttpStatus.BAD_REQUEST, "行为数据保存失败");
         }
     }

+ 20 - 0
src/main/java/com/njuzr/eaibackend/vo/BehaviorOverviewVO.java

@@ -0,0 +1,20 @@
+package com.njuzr.eaibackend.vo;
+
+import lombok.Data;
+
+/**
+ * @Auther: WuZilong
+ * @Date: 2025/8/9 22:45
+ * @Description: 用于前端展示 插入行为 删除行为 复制行为 复制字符数等等
+ */
+@Data
+public class BehaviorOverviewVO {
+    private Long studentId; // 学生ID
+    private Long assignmentId; // 作业ID
+    // 统计信息字段(设置默认值为0,避免前端处理null)
+    private Integer insertCount = 0; // type为insert的次数
+    private Integer deleteCount = 0; // type为delete的次数
+    private Integer copyCount = 0; // type为copy的次数
+    private Integer copyCharacterCount = 0; // type为copy的所有条目中,insert字段对应的字符总数
+    private Integer longPauseCount = 0; // pauseTime大于2000ms的次数
+}