Просмотр исходного кода

Merge branch 'refs/heads/refactor' into feat/ai_evaluation

baiqi 1 год назад
Родитель
Сommit
cea67e6e29

+ 9 - 0
application.yaml

@@ -92,3 +92,12 @@ proxy:
   enabled: false
   host: 20.111.54.16
   port: 8123
+
+springdoc:
+  api-docs:
+    enabled: true
+    path: /doc/v3/api-docs
+  swagger-ui:
+    enabled: true
+    path: /doc/swagger/swagger-ui.html
+    packagesToScan: com.njuzr.eaibackend.controller

+ 23 - 0
pom.xml

@@ -187,6 +187,29 @@
             <version>3.14.2</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.1.0</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>
+
+        <!-- ZIP压缩 -->
+        <dependency>
+            <groupId>net.lingala.zip4j</groupId>
+            <artifactId>zip4j</artifactId>
+            <version>2.11.5</version>
+        </dependency>
 
     </dependencies>
 

+ 48 - 47
src/main/java/com/njuzr/eaibackend/config/JWTAuthenticationFilter.java

@@ -44,62 +44,33 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
         this.userService = userService;
     }
 
-    // TODO 原登录jwtTokenUtil.parseToken(token)获得的是学号,门户是手机号
+    // 原登录jwtTokenUtil.parseToken(token)获得的是学号,门户是手机号
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
             throws ServletException, IOException {
         String token = request.getHeader("Authorization");
-        log.info("从Header中解析出Token:" + token);
+        log.info("doFilterInternal get token: {}", token);
 
         try {
-            if (token != null) {
-                // 注意提取到的是手机号
-                String phone = jwtTokenUtil.parseToken(token);
-                log.info("解析出phone:" + phone);
-                Map<String, Object> userInfoMap = jwtTokenUtil.parseClaim(token).getBody().get("user_info", Map.class);
-
-                // 不为空则认为是门户请求
-                if(userInfoMap != null){
-                    log.info("解析出的用户信息:{}", userInfoMap.toString());
-                    //pid:门户用户id
-                    Integer pid = Integer.valueOf(userInfoMap.get("id").toString());
-                    String name = userInfoMap.get("name").toString();
-                    String email = userInfoMap.get("email").toString();
-                    String username = userInfoMap.get("username").toString();
-                    Role role = Role.valueOf(userInfoMap.get("role").toString());
-                    // 如果EAI数据库中没有该用户,则添加该用户
-                    if (!userService.userExists(pid)) {
-                        log.info("用户不存在,添加用户{}", phone);
-                        UserRegisterDTO reg = new UserRegisterDTO();
-                        reg.setPid(pid);
-                        reg.setOfficialEmail(email);
-                        reg.setOfficialNumber(username);
-                        reg.setPhone(phone);
-                        reg.setRole(role);
-                        reg.setName(name);
-                        reg.setPassword("no_password");
-                        userService.createUserByPortal(reg);
-                    } else {
-                        log.info("用户存在,更新用户{}", phone);
-                        // 否则更新用户信息
-                        UserUpdateDTO update = new UserUpdateDTO();
-                        update.setContentEmail(email);
-                        // 同时为账号添加对应权限
-                        update.setRole(role);
-//                        update.setPhone(phone);
-                        update.setOfficialNumber(username);
-                        userService.updateUserByPid(pid, update);
-                    }
-                }
-                UserDetails userDetails = userDetailService.loadUserByUsername(phone);
-                log.info("解析出UserDetails:" + userDetails);
+            if(token == null) {
+                chain.doFilter(request, response);
+            }
 
-                // 设置principal和权限
-                UsernamePasswordAuthenticationToken authentication =
-                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
-                SecurityContextHolder.getContext().setAuthentication(authentication);
+            String phone = jwtTokenUtil.parseToken(token);
+            Map<String, Object> userInfoMap = jwtTokenUtil.parseClaim(token).getBody().get("user_info", Map.class);
+            // 不为空则认为是门户请求,此时同步用户信息
+            if(userInfoMap != null){
+                syncUserInfo(userInfoMap, phone);
             }
 
+            UserDetails userDetails = userDetailService.loadUserByUsername(phone);
+            log.info("doFilterInternal userDetails:{}", userDetails);
+
+            // 设置principal和权限
+            UsernamePasswordAuthenticationToken authentication =
+                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
             chain.doFilter(request, response);
          } catch (CredentialsExpiredException e) {
             // 将异常传递给AuthenticationEntryPoint处理(必要的步骤)
@@ -107,4 +78,34 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter {
         }
 
     }
+
+    private void syncUserInfo(Map<String, Object> userInfoMap, String phone) {
+        log.info("syncUserInfo user info in token:{}", userInfoMap.toString());
+        Integer pid = Integer.valueOf(userInfoMap.get("id").toString());
+        String name = userInfoMap.get("name").toString();
+        String email = userInfoMap.get("email").toString();
+        String username = userInfoMap.get("username").toString();
+        Role role = Role.valueOf(userInfoMap.get("role").toString());
+        // 如果EAI数据库中没有该用户,则添加该用户
+        if (!userService.userExists(pid)) {
+            log.info("syncUserInfo user don't exist, add: {}", phone);
+            UserRegisterDTO reg = new UserRegisterDTO();
+            reg.setPid(pid);
+            reg.setOfficialEmail(email);
+            reg.setOfficialNumber(username);
+            reg.setPhone(phone);
+            reg.setRole(role);
+            reg.setName(name);
+            reg.setPassword("no_password");
+            userService.createUserByPortal(reg);
+        } else {
+            log.info("syncUserInfo user exist, update: {}", phone);
+            UserUpdateDTO update = new UserUpdateDTO();
+            update.setContentEmail(email);
+            update.setRole(role);
+            update.setPhone(phone);
+            update.setOfficialNumber(username);
+            userService.updateUserByPid(pid, update);
+        }
+    }
 }

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

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

+ 24 - 0
src/main/java/com/njuzr/eaibackend/config/SpringDocConfig.java

@@ -0,0 +1,24 @@
+package com.njuzr.eaibackend.config;
+
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * spring doc配置
+ */
+@Configuration
+public class SpringDocConfig {
+    @Bean
+    public OpenAPI restfulOpenAPI() {
+        return new OpenAPI()
+                .info(new Info().title("SEEAI专业英语写作平台API")
+                        .description("Spring Boot3 Restful API")
+                        .version("V1.0.0")
+                        .license(new License().name("访问SpringDoc官方网站").url("http://springdoc.org")));
+    }
+ 
+}

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

@@ -0,0 +1,52 @@
+package com.njuzr.eaibackend.controller;
+
+import com.njuzr.eaibackend.service.ExportCompositionService;
+import com.njuzr.eaibackend.vo.UserVO;
+import freemarker.template.TemplateException;
+import org.apache.commons.lang3.tuple.Pair;
+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.*;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@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 {
+
+        Pair<UserVO, byte[]> pair = exportCompositionService.exportWord(assignmentId, studentId);
+        UserVO student = pair.getLeft();
+        byte[] wordBytes = pair.getRight();
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        headers.setContentDispositionFormData("attachment",
+                URLEncoder.encode(String.format("%s_%s.%s", student.getName(), student.getOfficialNumber(), "docx"), StandardCharsets.UTF_8));
+
+        return ResponseEntity.ok()
+                .headers(headers)
+                .body(wordBytes);
+    }
+
+    @GetMapping("/composition/zip")
+    public ResponseEntity<byte[]> exportBatch(@RequestParam Long assignmentId, @RequestParam(required = false) List<Long> studentIds) {
+
+        byte[] zipBytes = exportCompositionService.exportBatchToZip(studentIds, assignmentId);
+
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION,
+                        "attachment; filename=compositions.zip")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(zipBytes);
+    }
+}

+ 12 - 7
src/main/java/com/njuzr/eaibackend/mapper/BehaviorRecordMapper.java

@@ -1,13 +1,18 @@
 package com.njuzr.eaibackend.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.njuzr.eaibackend.po.BehaviorRecord;
-import org.apache.ibatis.annotations.Mapper;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.domain.Sort;
+import java.util.List;
 
 /**
- * @author BaiBai
- * @date 2025-02-10 17:02
- */
-@Mapper
-public interface BehaviorRecordMapper extends BaseMapper<BehaviorRecord> {
+ - * @author BaiBai
+ - * @date 2025-02-10 17:02
+ - */
+public interface BehaviorRecordMapper extends MongoRepository<BehaviorRecord, String> {
+    // 按学生+作业组合查询
+    List<BehaviorRecord> findByAssignmentIdAndStudentId(Long assignmentId, Long studentId, Sort sort);
+//    Page<BehaviorRecord> findByStudentIdAndAssignmentId(Long assignmentId, Long userId, Pageable pageable);
 }

+ 3 - 1
src/main/java/com/njuzr/eaibackend/mapper/EngagementMapper.java

@@ -3,7 +3,9 @@ package com.njuzr.eaibackend.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.njuzr.eaibackend.po.Engagement;
 import org.apache.ibatis.annotations.Mapper;
-// TODO:(柏琪)
+
+import java.util.List;
+
 /**
  * @author 柏琪
  * @date 2024-07-27 13:57

+ 21 - 0
src/main/java/com/njuzr/eaibackend/mapper/serviceIpml/EngagementMapperServiceImpl.java

@@ -0,0 +1,21 @@
+package com.njuzr.eaibackend.mapper.serviceIpml;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.njuzr.eaibackend.mapper.EngagementMapper;
+import com.njuzr.eaibackend.po.Engagement;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class EngagementMapperServiceImpl extends ServiceImpl<EngagementMapper, Engagement> {
+
+    public List<Engagement> getEngagementByAssignmentId(Long assignmentId) {
+        return this.lambdaQuery()
+                .eq(Engagement::getAssignmentId, assignmentId)
+                .list();
+    }
+
+
+}

+ 73 - 15
src/main/java/com/njuzr/eaibackend/po/BehaviorRecord.java

@@ -1,23 +1,81 @@
 package com.njuzr.eaibackend.po;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.CompoundIndexes;
+import org.springframework.data.mongodb.core.mapping.Field;
 
 import java.util.Date;
+import java.util.List;
 
-/**
- * @author BaiBai
- * @date 2025-02-10 16:55
- */
 @Data
-@TableName("behavior_record")
+@Document(collection = "behaviorRecords")
+@CompoundIndexes({
+        // 复合索引:assignment_id + student_id + created_at (降序)
+        @CompoundIndex(
+                name = "assignment_student_created_idx",
+                def = "{'assignment_id': 1, 'student_id': 1, 'created_at': -1}",
+                background = true    // 后台构建,避免阻塞操作
+        )
+})
 public class BehaviorRecord {
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private Long StudentId;
-    private Long assignmentId;
-    private String recordFileUrl;
-    private Date time;
-}
+
+    @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 = new Date(); // 记录创建时间
+
+    private List<BehaviorEvent> events; // 行为事件集合
+
+    /**
+     * 嵌套行为事件类
+     */
+    @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; // 删除的内容
+    }
+
+}

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

@@ -0,0 +1,128 @@
+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.mapper.serviceIpml.EngagementMapperServiceImpl;
+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;
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.Pair;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class ExportCompositionService {
+
+    @Autowired
+    private Configuration freemarkerConfig;
+    @Autowired
+    private EngagementMapper engagementMapper;
+    @Autowired
+    private AssignmentService assignmentService;
+    @Autowired
+    private UserService userService;
+    @Autowired
+    private WordExporter wordExporter;
+    @Autowired
+    private EngagementMapperServiceImpl engagementMapperServiceImpl;
+
+    public Pair<UserVO, 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 = buildWordInfoModel(assignment, user, engagement);
+
+        return Pair.of(user, wordExporter.exportCompositionWord(model));
+
+    }
+
+    private Map<String, Object> buildWordInfoModel(AssignmentVO assignment, UserVO user, EngagementVO engagement) {
+        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", new Date());
+        model.put("content", processContent(engagement.getTextContent()));
+        return model;
+    }
+
+    public byte[] exportBatchToZip(List<Long> studentIds, Long assignmentId) {
+        if(assignmentId == null){
+            throw MyException.create(HttpStatus.BAD_REQUEST, "assignmentId不能为空");
+        }
+
+        if (CollectionUtils.isEmpty(studentIds)) {
+            List<Engagement> engagements = engagementMapperServiceImpl.getEngagementByAssignmentId(assignmentId);
+            if (CollectionUtils.isEmpty(engagements)) {
+                return null;
+            }
+            studentIds = engagements.stream()
+                    .map(Engagement::getStudentId)
+                    .toList();
+        }
+        return exportBatchToZipByStudentIds(studentIds, assignmentId);
+    }
+
+    private byte[] exportBatchToZipByStudentIds(List<Long> studentIds, Long assignmentId) {
+        List<ZipExport.ZipItem> zipItems = studentIds.stream()
+                .map(studentId -> {
+                    try {
+                        Pair<UserVO, byte[]> pair = exportWord(assignmentId, studentId);
+                        UserVO student = pair.getLeft();
+                        byte[] wordBytes = pair.getRight();
+                        String fileName = String.format("%s_%s.%s", student.getName(), student.getOfficialNumber(), "docx");
+                        return new ZipExport.ZipItem(fileName, wordBytes);
+                    } catch (Exception e) {
+                        log.error("exportBatchToZip failed, studentId: {}, reason: {}", studentId, e.getMessage());
+                        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 {
+
+        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/>"));
+
+        return wordExporter.exportCompositionWord(model);
+    }
+
+    private String processContent(String content) {
+        return content.replaceAll("\n", "<br/>");
+    }
+
+}

+ 53 - 21
src/main/java/com/njuzr/eaibackend/service/impl/BehaviorRecordServiceImpl.java

@@ -15,7 +15,8 @@ import org.springframework.web.multipart.MultipartFile;
 import java.io.IOException;
 import java.util.Date;
 import java.util.List;
-
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.data.domain.Sort;
 /**
  * @author BaiBai
  * @date 2025-02-10 17:03
@@ -25,39 +26,70 @@ import java.util.List;
 public class BehaviorRecordServiceImpl implements BehaviorRecordService {
 
     private final BehaviorRecordMapper behaviorRecordMapper;
-
-    private final OssUtil ossUtil;
+    private final ObjectMapper objectMapper;
 
     @Autowired
     public BehaviorRecordServiceImpl(BehaviorRecordMapper behaviorRecordMapper, OssUtil ossUtil) {
         this.behaviorRecordMapper = behaviorRecordMapper;
-        this.ossUtil = ossUtil;
+        this.objectMapper = new ObjectMapper();
     }
 
+    @Override
     public List<BehaviorRecord> find(Long assignmentId, Long studentId) {
-        QueryWrapper<BehaviorRecord> queryWrapper = new QueryWrapper<>();
-        queryWrapper.eq("assignment_id", assignmentId);
-        queryWrapper.eq("student_id", studentId);
-        queryWrapper.orderByDesc("time");
-        return behaviorRecordMapper.selectList(queryWrapper);
+        // 创建一个按 created_at 降序排序的 Sort 对象,它不会引起额外的排序开销(因为索引已经支持);显式指定排序确保代码的兼容性
+        Sort sort = Sort.by(Sort.Direction.DESC, "created_at");
+        return behaviorRecordMapper.findByAssignmentIdAndStudentId(assignmentId, studentId, sort);
     }
 
+//    public String upload(Long assignmentId, Long studentId, MultipartFile file) {
+//        try {
+//            Date now = new Date();
+//            String filePath = "uploads/" + assignmentId + "_" + studentId + "_" + now.getTime() + file.getOriginalFilename();
+//            String urlStr = ossUtil.uploadFile(file.getInputStream(), filePath);
+//            String url = urlStr.split("\\?")[0];
+//            BehaviorRecord behaviorRecord = new BehaviorRecord();
+//            behaviorRecord.setAssignmentId(assignmentId);
+//            behaviorRecord.setStudentId(studentId);
+//            behaviorRecord.setRecordFileUrl(url);
+//            behaviorRecord.setTime(now);
+//            behaviorRecordMapper.insert(behaviorRecord);
+//            return url;
+//        } catch (IOException e) {
+//            log.error(e.getMessage());
+//            throw MyException.create(HttpStatus.BAD_REQUEST, "文件上传失败");
+//        }
+//    }
+
+
     public String upload(Long assignmentId, Long studentId, MultipartFile file) {
         try {
-            Date now = new Date();
-            String filePath = "uploads/" + assignmentId + "_" + studentId + "_" + now.getTime() + file.getOriginalFilename();
-            String urlStr = ossUtil.uploadFile(file.getInputStream(), filePath);
-            String url = urlStr.split("\\?")[0];
-            BehaviorRecord behaviorRecord = new BehaviorRecord();
-            behaviorRecord.setAssignmentId(assignmentId);
-            behaviorRecord.setStudentId(studentId);
-            behaviorRecord.setRecordFileUrl(url);
-            behaviorRecord.setTime(now);
-            behaviorRecordMapper.insert(behaviorRecord);
-            return url;
+            // 解析JSON文件
+            List<BehaviorRecord.BehaviorEvent> events = parseBehaviorEvents(file);
+
+            // 创建行为记录对象
+            BehaviorRecord record = new BehaviorRecord();
+            record.setStudentId(studentId);
+            record.setAssignmentId(assignmentId);
+            record.setEvents(events);
+
+            // 保存到MongoDB
+            behaviorRecordMapper.save(record);
+            return "行为数据保存成功";
         } catch (IOException e) {
             log.error(e.getMessage());
-            throw MyException.create(HttpStatus.BAD_REQUEST, "文件上传失败");
+            throw MyException.create(HttpStatus.BAD_REQUEST, "行为数据保存失败");
         }
     }
+
+    private List<BehaviorRecord.BehaviorEvent> parseBehaviorEvents(MultipartFile file)
+            throws IOException {
+        // 使用Jackson解析JSON数组
+        return objectMapper.readValue(
+                file.getInputStream(),
+                objectMapper.getTypeFactory().constructCollectionType(
+                        List.class,
+                        BehaviorRecord.BehaviorEvent.class
+                )
+        );
+    }
 }

+ 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;
+    }
+}

+ 9 - 0
src/main/resources/application-dev.yaml

@@ -67,3 +67,12 @@ proxy:
   enabled: false
   host: 20.111.54.16
   port: 8123
+
+springdoc:
+  api-docs:
+    enabled: true
+    path: /doc/v3/api-docs
+  swagger-ui:
+    enabled: true
+    path: /doc/swagger/swagger-ui.html
+    packagesToScan: com.njuzr.eaibackend.controller

+ 8 - 1
src/main/resources/application-prod.yaml

@@ -67,4 +67,11 @@ proxy:
   host: 20.111.54.16
   port: 8123
 
-
+springdoc:
+  api-docs:
+    enabled: false
+    path: /doc/v3/api-docs
+  swagger-ui:
+    enabled: false
+    path: /doc/swagger/swagger-ui.html
+    packagesToScan: com.njuzr.eaibackend.controller

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

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>姓名: ${studentName}</p>
+<p>学号: ${officialNumber}</p>
+<div>
+    ${content}
+</div>
+</body>
+</html>