Browse Source

3.4.5 update-20250415,新功能开发

xiaofei 3 months ago
parent
commit
8b82147aa6
29 changed files with 630 additions and 181 deletions
  1. 9 0
      distributed-print/install/mysql/upgrade/3.4.5.sql
  2. 5 0
      distributed-print/src/main/java/com/qmth/distributed/print/api/mark/MarkAiQuestionParamController.java
  3. 19 0
      distributed-print/src/main/java/com/qmth/distributed/print/api/mark/MarkQuestionSubjectiveController.java
  4. 15 0
      teachcloud-common/src/main/java/com/qmth/teachcloud/common/bean/dto/mark/PictureConfig.java
  5. 2 0
      teachcloud-common/src/main/java/com/qmth/teachcloud/common/contant/SystemConstant.java
  6. 22 1
      teachcloud-common/src/main/java/com/qmth/teachcloud/common/entity/MarkQuestion.java
  7. 2 0
      teachcloud-common/src/main/java/com/qmth/teachcloud/common/service/SysUserService.java
  8. 19 0
      teachcloud-common/src/main/java/com/qmth/teachcloud/common/service/impl/SysUserServiceImpl.java
  9. 9 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/dto/mark/manage/MarkGroupProgressDto.java
  10. 11 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/dto/mark/manage/TrackDTO.java
  11. 5 3
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/enums/LockType.java
  12. 0 2
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/AiMarkService.java
  13. 2 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionLevelService.java
  14. 2 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionParamService.java
  15. 2 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionPointService.java
  16. 2 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkOcrStudentQuestionService.java
  17. 5 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkQuestionService.java
  18. 6 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkService.java
  19. 4 1
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkSyncService.java
  20. 5 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkTaskService.java
  21. 0 159
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/AiMarkServiceImpl.java
  22. 8 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionLevelServiceImpl.java
  23. 16 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionParamServiceImpl.java
  24. 8 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionPointServiceImpl.java
  25. 10 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkOcrStudentQuestionServiceImpl.java
  26. 70 11
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkQuestionServiceImpl.java
  27. 303 4
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkServiceImpl.java
  28. 32 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkSyncServiceImpl.java
  29. 37 0
      teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkTaskServiceImpl.java

+ 9 - 0
distributed-print/install/mysql/upgrade/3.4.5.sql

@@ -103,3 +103,12 @@ SET name='AI智能评卷统计', url='ExportAIStats', `type`='BUTTON', parent_id
 WHERE id=1227;
 ALTER TABLE basic_school ADD system_logo MEDIUMTEXT NULL COMMENT '系统logo';
 ALTER TABLE basic_school ADD tab_name varchar(200) NULL COMMENT '标签标题';
+ALTER TABLE `mark_question`
+    ADD COLUMN `clear_ocr_result` TINYINT(1) NULL DEFAULT 0 COMMENT '是否需要重新OCR识别' AFTER `person_task`,
+ADD COLUMN `enable_ai` TINYINT(1) NULL DEFAULT 1 COMMENT '是否开启/关闭AI评卷任务' AFTER `clear_ocr_result`;
+
+
+INSERT INTO `sys_privilege` (`id`, `name`, `url`, `type`, `parent_id`, `sequence`, `property`, `related`, `enable`, `default_auth`, `front_display`) VALUES ('1232', '开启/终止智能评卷', 'EnableAiMark', 'LINK', '917', '10', 'AUTH', '1233', '1', '0', '1');
+INSERT INTO `sys_privilege` (`id`, `name`, `url`, `type`, `parent_id`, `sequence`, `property`, `related`, `enable`, `default_auth`, `front_display`) VALUES ('1233', '开启/终止智能评卷', '/api/admin/mark/question/subjective/ai/enable', 'URL', '917', '28', 'AUTH', NULL, '1', '1', '1');
+INSERT INTO `sys_privilege` (`id`, `name`, `url`, `type`, `parent_id`, `sequence`, `property`, `related`, `enable`, `default_auth`, `front_display`) VALUES ('1234', '重置AI智能评卷任务', 'ResetAiMarkTask', 'LINK', '917', '10', 'AUTH', '1235', '1', '0', '1');
+INSERT INTO `sys_privilege` (`id`, `name`, `url`, `type`, `parent_id`, `sequence`, `property`, `enable`, `default_auth`, `front_display`) VALUES ('1235', '重置AI智能评卷任务', '/api/admin/mark/question/subjective/ai/reset', 'URL', '917', '29', 'AUTH', '1', '1', '1');

+ 5 - 0
distributed-print/src/main/java/com/qmth/distributed/print/api/mark/MarkAiQuestionParamController.java

@@ -18,6 +18,7 @@ import com.qmth.teachcloud.mark.entity.MarkAiQuestionPoint;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionLevelService;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionParamService;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionPointService;
+import com.qmth.teachcloud.mark.service.MarkQuestionService;
 import io.swagger.annotations.*;
 import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
@@ -55,6 +56,8 @@ public class MarkAiQuestionParamController {
 
     @Resource
     MarkAiQuestionLevelService markAiQuestionLevelService;
+    @Resource
+    private MarkQuestionService markQuestionService;
 
     @Resource
     RedisUtil redisUtil;
@@ -130,6 +133,8 @@ public class MarkAiQuestionParamController {
                 default:
                     break;
             }
+            // todo 知识点或者档位变动,更新
+//            markQuestionService.updateClearOcrResult(markAiQuestionParam.getQuestionId(), true);
             markAiQuestionParamService.saveOrUpdate(markAiQuestionParam);
         } catch (Exception e) {
             log.error(SystemConstant.LOG_ERROR, e);

+ 19 - 0
distributed-print/src/main/java/com/qmth/distributed/print/api/mark/MarkQuestionSubjectiveController.java

@@ -134,4 +134,23 @@ public class MarkQuestionSubjectiveController extends BaseController {
         return ResultUtil.ok(markGroupQuestionsDtoList);
     }
 
+    /**
+     * 评阅题目列表
+     */
+    @ApiOperation(value = "AI评卷-重置AI智能评卷任务")
+    @RequestMapping(value = "/ai/reset", method = RequestMethod.POST)
+    public Result aiTaskReset(@ApiParam(value = "题目ID", required = true) @RequestParam Long questionId) {
+        return ResultUtil.ok(markQuestionService.resetAiTask(questionId));
+    }
+
+    /**
+     * 评阅题目列表
+     */
+    @ApiOperation(value = "AI评卷-开启/终止评卷")
+    @RequestMapping(value = "/ai/enable", method = RequestMethod.POST)
+    public Result aiTaskAuto(@ApiParam(value = "题目ID", required = true) @RequestParam Long questionId,
+                             @ApiParam(value = "ai评卷状态", required = true) @RequestParam Boolean enableAi) {
+        return ResultUtil.ok(markQuestionService.enableAi(questionId, enableAi));
+    }
+
 }

+ 15 - 0
teachcloud-common/src/main/java/com/qmth/teachcloud/common/bean/dto/mark/PictureConfig.java

@@ -1,5 +1,7 @@
 package com.qmth.teachcloud.common.bean.dto.mark;
 
+import java.util.Objects;
+
 public class PictureConfig {
 
     private int i;
@@ -47,4 +49,17 @@ public class PictureConfig {
     public void setH(Double h) {
         this.h = h;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PictureConfig that = (PictureConfig) o;
+        return i == that.i && Objects.equals(x, that.x) && Objects.equals(y, that.y) && Objects.equals(w, that.w) && Objects.equals(h, that.h);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(i, x, y, w, h);
+    }
 }

+ 2 - 0
teachcloud-common/src/main/java/com/qmth/teachcloud/common/contant/SystemConstant.java

@@ -43,6 +43,8 @@ import cn.hutool.core.date.DateUtil;
  */
 public class SystemConstant {
     public static final long MARK_JPG_EXPIRE_MINUTE = 12 * 60;
+    public static final String AI_USER = "ai";
+    public static final String AI_USER_NAME = "AI智能评卷";
     private final static Logger log = LoggerFactory.getLogger(SystemConstant.class);
 
     public static final String DEFAULT_PAPER_TYPE_A = "A";

+ 22 - 1
teachcloud-common/src/main/java/com/qmth/teachcloud/common/entity/MarkQuestion.java

@@ -86,7 +86,7 @@ public class MarkQuestion extends BaseEntity implements Serializable {
 
     @ApiModelProperty(value = "题型")
     private Integer questionType;
-    
+
     private Integer paperIndex;
     
     private Integer pageIndex;
@@ -123,6 +123,11 @@ public class MarkQuestion extends BaseEntity implements Serializable {
     @ApiModelProperty(value = "是否有人评任务")
     private Boolean personTask;
 
+    @ApiModelProperty(value = "是否需要重新OCR识别")
+    private Boolean clearOcrResult;
+    @ApiModelProperty(value = "是否开启/关闭AI评卷任务")
+    private Boolean enableAi;
+
     @TableField(exist = false)
     private double markScore;
 
@@ -344,6 +349,22 @@ public class MarkQuestion extends BaseEntity implements Serializable {
         this.personTask = personTask;
     }
 
+    public Boolean getClearOcrResult() {
+        return clearOcrResult;
+    }
+
+    public void setClearOcrResult(Boolean clearOcrResult) {
+        this.clearOcrResult = clearOcrResult;
+    }
+
+    public Boolean getEnableAi() {
+        return enableAi;
+    }
+
+    public void setEnableAi(Boolean enableAi) {
+        this.enableAi = enableAi;
+    }
+
     public double getMarkScore() {
         return markScore;
     }

+ 2 - 0
teachcloud-common/src/main/java/com/qmth/teachcloud/common/service/SysUserService.java

@@ -264,4 +264,6 @@ public interface SysUserService extends IService<SysUser> {
      * @return
      */
     SysUser findByCode(Long schoolId, String code);
+
+    SysUser getAiUserBySchoolId(Long schoolId);
 }

+ 19 - 0
teachcloud-common/src/main/java/com/qmth/teachcloud/common/service/impl/SysUserServiceImpl.java

@@ -41,6 +41,7 @@ import java.security.NoSuchAlgorithmException;
 import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 /**
  * <p>
@@ -1259,4 +1260,22 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
         Objects.requireNonNull(sysUser, "未找到工号为[" + code + "]用户信息");
         return sysUser;
     }
+
+    @Override
+    public SysUser getAiUserBySchoolId(Long schoolId) {
+        SysUser sysUser = sysUserService.findBySchoolIdAndLoginName(schoolId, SystemConstant.AI_USER);
+        if (sysUser == null) {
+            sysUser = new SysUser(schoolId, SystemConstant.AI_USER, SystemConstant.AI_USER_NAME, "00000000000", true);
+            BasicSchool basicSchool = commonCacheService.schoolCache(schoolId);
+            sysUser.setPassword(basicSchool.getInitPassword());
+            SysOrg sysOrg = sysOrgService.findRootOrg(schoolId);
+            sysUser.setOrgId(sysOrg.getId());
+            this.save(sysUser);
+
+            SysRole sysRole = sysRoleService.getDefaultRoleByType(RoleTypeEnum.MARKER);
+            SysUserRole sysUserRole = new SysUserRole(sysUser.getId(), sysRole.getId());
+            sysUserRoleService.save(sysUserRole);
+        }
+        return sysUser;
+    }
 }

+ 9 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/dto/mark/manage/MarkGroupProgressDto.java

@@ -16,6 +16,7 @@ public class MarkGroupProgressDto {
     private String percent;
     private Integer arbitrateCount;
     private String questionNumber;
+    private Boolean aiMark;
 
     public Long getQuestionId() {
         return questionId;
@@ -96,4 +97,12 @@ public class MarkGroupProgressDto {
     public void setQuestionNumber(String questionNumber) {
         this.questionNumber = questionNumber;
     }
+
+    public Boolean getAiMark() {
+        return aiMark;
+    }
+
+    public void setAiMark(Boolean aiMark) {
+        this.aiMark = aiMark;
+    }
 }

+ 11 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/dto/mark/manage/TrackDTO.java

@@ -3,6 +3,7 @@ package com.qmth.teachcloud.mark.dto.mark.manage;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.qmth.teachcloud.common.contant.SystemConstant;
+import com.qmth.teachcloud.common.entity.MarkQuestion;
 import com.qmth.teachcloud.mark.entity.*;
 import com.qmth.teachcloud.mark.enums.MarkTaskStatus;
 import com.qmth.teachcloud.mark.params.MarkHeaderGroupResult;
@@ -132,6 +133,16 @@ public class TrackDTO implements Serializable {
         return track;
     }
 
+    public TrackDTO(MarkQuestion question, int number, double score, int index, int offsetX, int offsetY) {
+        setMainNumber(question.getMainNumber());
+        setSubNumber(question.getSubNumber());
+        setNumber(number);
+        setScore(score);
+        setOffsetIndex(index);
+        setOffsetX(offsetX);
+        setOffsetY(offsetY);
+    }
+
     public String getId() {
         return id;
     }

+ 5 - 3
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/enums/LockType.java

@@ -6,8 +6,8 @@ public enum LockType {
 	QUESTION_UPDATE("question_update"),
 	STUDENT("student"),
 	MARK_USER_QUESTION("mark_user_group"),
-	MARKER_RESET("marker_reset"), 
-	USER("user"), 
+	MARKER_RESET("marker_reset"),
+	USER("user"),
 	FORMAL_LIBRARY("formal_library"),
 	TRIAL_LIBRARY("trial_library"),
 	ARBITRATE("arbitrate"),
@@ -22,7 +22,9 @@ public enum LockType {
     CREATE_PDF("create_pdf"),
     EXAM_TASK_WHOLE("exam_task_whole"),
     CUSTOM_CARD_SAVE("custom_card_save"),
-    CUSTOM_MODEL_FOUR_CARD_SAVE("custom_model_four_card_save");
+    CUSTOM_MODEL_FOUR_CARD_SAVE("custom_model_four_card_save"),
+	AI_TASK_RESET("ai_task_reset"),
+	AI_MARK("ai_mark"), ;
 
 	private String name;
 

+ 0 - 2
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/AiMarkService.java

@@ -11,7 +11,5 @@ import java.util.List;
  */
 public interface AiMarkService {
 
-    void aiMark(Long examId, String paperNumber, Long questionId);
 
-    List<MarkOcrStudentQuestion> listStudentQuestionOcrResult(BasicSchool basicSchool, Long studentId, Long questionId);
 }

+ 2 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionLevelService.java

@@ -16,4 +16,6 @@ import java.util.List;
 public interface MarkAiQuestionLevelService extends IService<MarkAiQuestionLevel> {
 
     List<MarkAiQuestionLevel> listByAiQuestionId(Long id);
+
+    void deleteByAiQuestionId(Long aiQuestionId);
 }

+ 2 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionParamService.java

@@ -15,4 +15,6 @@ public interface MarkAiQuestionParamService extends IService<MarkAiQuestionParam
     boolean existMarkAiQuestionPointOrLevel(Long examId, String paperNumber, Long questionId);
 
     MarkAiQuestionParam getByExamIdAndPaperNumberAndQuestionId(Long examId, String paperNumber, Long questionId);
+
+    void deleteByExamIdAndPaperNumberAndQuestionId(Long examId, String paperNumber, Long questionId);
 }

+ 2 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkAiQuestionPointService.java

@@ -16,4 +16,6 @@ import java.util.List;
 public interface MarkAiQuestionPointService extends IService<MarkAiQuestionPoint> {
 
     List<MarkAiQuestionPoint> listByAiQuestionId(Long id);
+
+    void deleteByAiQuestionId(Long aiQuestionId);
 }

+ 2 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkOcrStudentQuestionService.java

@@ -16,4 +16,6 @@ import java.util.List;
 public interface MarkOcrStudentQuestionService extends IService<MarkOcrStudentQuestion> {
 
     List<MarkOcrStudentQuestion> listByStudentIdAndQuestionId(Long studentId, Long questionId);
+
+    void deleteByExamIdAndPaperNumberAndQuestionId(Long examId, String paperNumber, Long questionId);
 }

+ 5 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkQuestionService.java

@@ -118,4 +118,9 @@ public interface MarkQuestionService extends IService<MarkQuestion> {
     List<MarkQuestion> listByExamIdAndObjective(Long examId, boolean objective);
 
     int countUnBindMarkerQuestion(Long examId, String paperNumber);
+
+    boolean resetAiTask(Long questionId);
+
+    boolean enableAi(Long questionId, Boolean enableAi);
+    boolean updateClearOcrResult(Long questionId, Boolean clearOcrResult);
 }

+ 6 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkService.java

@@ -1,8 +1,10 @@
 package com.qmth.teachcloud.mark.service;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.qmth.teachcloud.common.entity.BasicSchool;
 import com.qmth.teachcloud.common.entity.MarkQuestion;
 import com.qmth.teachcloud.common.entity.SysUser;
+import com.qmth.teachcloud.mark.dto.ai.SheetImageDto;
 import com.qmth.teachcloud.mark.dto.mark.manage.Task;
 import com.qmth.teachcloud.mark.dto.mark.mark.MarkSettingDto;
 import com.qmth.teachcloud.mark.dto.mark.mark.MarkStatusDto;
@@ -106,4 +108,8 @@ public interface MarkService {
     void releaseByMarkQuestion(MarkQuestion markQuestion);
 
     void resetMarkedQuestionId(MarkUserQuestion markUserQuestion);
+
+    void aiMark(Long schoolId, Long examId, String paperNumber, Long questionId);
+
+    void resetAiTask(Long examId, String paperNumber, Long questionId);
 }

+ 4 - 1
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkSyncService.java

@@ -17,6 +17,9 @@ public interface MarkSyncService {
     void updateQuality(List<MarkUserQuestion> markUserGroups, String lockKey);
 
     void calcObjectiveScore(MarkPaper markPaper);
-
     void deleteMarkedByQuestion(MarkQuestion markQuestion, boolean b);
+
+    void aiTaskResetSync(MarkQuestion markQuestion, Long questionId);
+
+    void aiAutoMark(Long schoolId, MarkQuestion markQuestion);
 }

+ 5 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/MarkTaskService.java

@@ -36,6 +36,7 @@ public interface MarkTaskService extends IService<MarkTask> {
     int countByExamIdAndPaperNumberAndQuestionIdAndUserId(Long examId, String paperNumber, Long questionId, Long userId);
 
     List<MarkTask> listByExamIdAndPaperNumberAndQuestionIdAndUserIdAndStatusNotIn(Long examId, String paperNumber, Long questionId, Long userId, List<MarkTaskStatus> statusList);
+    List<MarkTask> listByExamIdAndPaperNumberAndQuestionIdAndAiMarkedAndStatusNotIn(Long examId, String paperNumber, Long questionId, Boolean aiMarked, List<MarkTaskStatus> statusList);
 
     boolean resetById(Long markTaskId, Long userId, String rejectReason, Long rejectId, Long date, MarkTaskStatus newStatus);
 
@@ -82,6 +83,8 @@ public interface MarkTaskService extends IService<MarkTask> {
     boolean updateMarkerResult(Long taskId, MarkTaskStatus marked, Long userId, MarkResultQuestion result,
                                Long now, MarkTaskStatus... inStatus);
 
+    boolean updateAiMarkResult(Long taskId, MarkTaskStatus marked, Long userId, Double markerScore, List<TrackDTO> trackList, Long spent, Long now, MarkTaskStatus statusWaiting);
+
     int countByExamIdAndPaperNumberAndQuestionIdAndTaskNumber(Long examId, String paperNumber, Long questionId, int taskNumber);
 
     int countByStudentId(Long studentId);
@@ -118,4 +121,6 @@ public interface MarkTaskService extends IService<MarkTask> {
      * @throws Exception
      */
     void exportAiMark(Long semesterId, Long examId, Long courseId, String paperNumber) throws Exception;
+
+    void updateAiMarkErrorMsg(MarkTask t);
 }

+ 0 - 159
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/AiMarkServiceImpl.java

@@ -87,165 +87,6 @@ public class AiMarkServiceImpl implements AiMarkService {
     @Resource
     LockService lockService;
 
-    @Override
-    public void aiMark(Long examId, String paperNumber, Long questionId) {
-        BasicSchool basicSchool = basicSchoolMapper.selectByExamId(examId);
-        BasicCourse basicCourse = basicCourseMapper.selectByExamIdAndPaperNumber(examId, paperNumber);
-        MarkQuestion markQuestion = markQuestionService.getById(questionId);
-        MarkAiQuestionParam markAiQuestionParam = markAiQuestionParamService.getByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, questionId);
-        List<MarkTask> markTasks = null;
-        int pageNumber = 1;
-        do {
-            Set<Long> questions = new HashSet<>(Arrays.asList(questionId));
-            markTasks = markTaskService.findAiUnMarked(new Page<>(pageNumber, 20), examId, paperNumber, questionId);
-            if (markTasks.isEmpty()) {
-                break;
-            }
-            for (MarkTask t : markTasks) {
-                if (markService.applyTask(examId, paperNumber, t.getStudentId(), null, questions, Arrays.asList(t.getId()))) {
-                    break;
-                }
-                // 考生作答
-                try {
-                    String studentQuestionAnswer = this.mergeQuestionOcrResult(this.listStudentQuestionOcrResult(basicSchool, t.getStudentId(), t.getQuestionId()));
 
-                    AutoScoreRequest request = new AutoScoreRequest();
-                    request.setSubjectName(basicCourse.getName());
-                    request.setTotalScore(markQuestion.getTotalScore());
-                    request.setIntervalScore(markAiQuestionParam.getMinScore());
-                    request.setQuestionBody(markAiQuestionParam.getMainTitle());
-                    request.setStudentAnswer(studentQuestionAnswer);
-                    request.setStandardAnswer(buildStandardAnswer(markAiQuestionParam));
-                    // AI评卷
-                    AutoScoreResult autoScoreResult = aiService.autoScore(request, AiUtil.signature(basicSchool));
-
-                } catch (Exception e) {
-                    t.setAiMarkErrorMsg(e.getMessage());
-                } finally {
-                    t.setAiMarkErrorMsg(null);
-                }
-            }
-            pageNumber++;
-            markTasks = markTaskService.findAiUnMarked(new Page<>(pageNumber, 20), examId, paperNumber, questionId);
-        } while (CollectionUtils.isNotEmpty(markTasks));
-    }
-
-    private List<StandardAnswer> buildStandardAnswer(MarkAiQuestionParam markAiQuestionParam) {
-        List<StandardAnswer> standardAnswerList = new ArrayList<>();
-        // 得分点
-        if (AiQuestionParamModeStatus.POINT.equals(markAiQuestionParam.getMode())) {
-            List<MarkAiQuestionPoint> markAiQuestionPoints = markAiQuestionPointService.listByAiQuestionId(markAiQuestionParam.getId());
-            if (CollectionUtils.isEmpty(markAiQuestionPoints)) {
-                throw ExceptionResultEnum.ERROR.exception("未设置得分点");
-            }
-            standardAnswerList = markAiQuestionPoints.stream().map(m -> {
-                StandardAnswer standardAnswer = new StandardAnswer();
-                standardAnswer.setScore(m.getScore());
-                standardAnswer.setContent(m.getAnswer());
-                return standardAnswer;
-            }).collect(Collectors.toList());
-        }
-        // 档次
-        else if (AiQuestionParamModeStatus.LEVEL.equals(markAiQuestionParam.getMode())) {
-            List<MarkAiQuestionLevel> markAiQuestionLevels = markAiQuestionLevelService.listByAiQuestionId(markAiQuestionParam.getId());
-            if (CollectionUtils.isEmpty(markAiQuestionLevels)) {
-                throw ExceptionResultEnum.ERROR.exception("未设置档次");
-            }
-            standardAnswerList = markAiQuestionLevels.stream().map(m -> {
-                StandardAnswer standardAnswer = new StandardAnswer();
-                standardAnswer.setLowScore(m.getMinScore());
-                standardAnswer.setHighScore(m.getMaxScore());
-                standardAnswer.setContent(m.getAnswer());
-                return standardAnswer;
-            }).collect(Collectors.toList());
-        } else {
-            throw ExceptionResultEnum.ERROR.exception("评分模式不存在");
-        }
-        return standardAnswerList;
-    }
-
-    private String mergeQuestionOcrResult(List<MarkOcrStudentQuestion> markOcrStudentQuestionList) {
-        if (CollectionUtils.isEmpty(markOcrStudentQuestionList)) {
-            return null;
-        }
-        return markOcrStudentQuestionList.stream().filter(m -> StringUtils.isNotBlank(m.getOcrContent())).map(MarkOcrStudentQuestion::getOcrContent).collect(Collectors.joining(SystemConstant.COMMA_OF_CHINESE));
-    }
-
-    @Override
-    public List<MarkOcrStudentQuestion> listStudentQuestionOcrResult(BasicSchool basicSchool, Long studentId, Long questionId) {
-        List<MarkOcrStudentQuestion> markOcrStudentQuestionList = markOcrStudentQuestionService.listByStudentIdAndQuestionId(studentId, questionId);
-        if (CollectionUtils.isNotEmpty(markOcrStudentQuestionList)) {
-            return markOcrStudentQuestionList;
-        }
-        MarkQuestion markQuestion = markQuestionService.getById(questionId);
-        if (markQuestion == null || StringUtils.isBlank(markQuestion.getPicList())) {
-            throw ExceptionResultEnum.ERROR.exception("大题号[" + markQuestion.getMainNumber() + "]、小题号[" + markQuestion.getSubNumber() + "]未设置评卷区");
-        }
-        // 读取考生图片
-        MarkStudent markStudent = markStudentService.getById(studentId);
-        if (markStudent == null) {
-            throw ExceptionResultEnum.ERROR.exception("未找到考生" + ",考生ID[" + studentId);
-        } else if (StringUtils.isBlank(markStudent.getSheetPath())) {
-            throw ExceptionResultEnum.ERROR.exception("未查询到原图" + ",考号[" + markStudent.getStudentCode());
-        }
-        List<File> deleteFileList = new ArrayList<>();
-        //考生题卡原图
-        try {
-            List<FilePathVo> filePathVoList = markStudent.listSheetPath();
-            Map<Integer, SheetImageDto> fileMap = new HashMap<>();
-            for (int i = 0; i < filePathVoList.size(); i++) {
-                FilePathVo filePathVo = filePathVoList.get(i);
-                File file = SystemConstant.getFileTempDirVar(SystemConstant.JPG_PREFIX);
-                try {
-                    fileUploadService.downloadFile(filePathVo.getPath(), filePathVo.getUploadType(), filePathVo.getType(), file.getPath());
-                } catch (Exception e) {
-                    throw ExceptionResultEnum.ERROR.exception("读取考生原图失败" + ",考号[" + markStudent.getStudentCode() + "。" + e.getMessage());
-                }
-                fileMap.put(i + 1, new SheetImageDto(i + 1, file, filePathVo.getMd5(), AiUtil.imageDim(file)));
-                deleteFileList.add(file);
-            }
-
-            // 题目评卷区
-            List<PictureConfig> pictureConfigList = JSON.parseArray(markQuestion.getPicList(), PictureConfig.class);
-
-            List<MarkOcrStudentQuestion> markOcrStudentQuestions = new ArrayList<>();
-            for (int i = 0; i < pictureConfigList.size(); i++) {
-                PictureConfig pictureConfig = pictureConfigList.get(i);
-                SheetImageDto sheetImageDto = fileMap.get(pictureConfig.getI());
-                File file = SystemConstant.getFileTempDirVar(SystemConstant.JPG_PREFIX);
-                AiUtil.subImg(sheetImageDto, pictureConfig, file);
-
-                // OCR识别
-                String ocrResult;
-                try {
-                    ocrResult = ocrApiClient.forImage(AiUtil.signature(basicSchool), OcrType.HANDWRITING, UploadFile.build("image", "", file));
-                } catch (Exception e) {
-                    throw ExceptionResultEnum.ERROR.exception("OCR识别失败" + ",考号[" + markStudent.getStudentCode() + "。" + e.getMessage());
-                }
-                MarkOcrStudentQuestion markOcrStudentQuestion = new MarkOcrStudentQuestion();
-                markOcrStudentQuestion.setId(SystemConstant.getDbUuid());
-                markOcrStudentQuestion.setExamId(markStudent.getExamId());
-                markOcrStudentQuestion.setPaperNumber(markStudent.getPaperNumber());
-                markOcrStudentQuestion.setStudentId(studentId);
-                markOcrStudentQuestion.setQuestionId(questionId);
-                markOcrStudentQuestion.setNumber(i + 1);
-                markOcrStudentQuestion.setMd5(sheetImageDto.getMd5());
-                markOcrStudentQuestion.setOcrContent(ocrResult);
-                markOcrStudentQuestion.setCreateTime(System.currentTimeMillis());
-                markOcrStudentQuestions.add(markOcrStudentQuestion);
-                deleteFileList.add(file);
-            }
-            markOcrStudentQuestionService.saveBatch(markOcrStudentQuestions);
-            return markOcrStudentQuestions;
-        } catch (RuntimeException e) {
-            throw ExceptionResultEnum.ERROR.exception(e.getMessage());
-        } finally {
-            if (CollectionUtils.isNotEmpty(deleteFileList)) {
-                for (File file : deleteFileList) {
-                    FileUtil.deleteFile(file);
-                }
-            }
-        }
-    }
 
 }

+ 8 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionLevelServiceImpl.java

@@ -1,6 +1,7 @@
 package com.qmth.teachcloud.mark.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.qmth.teachcloud.mark.entity.MarkAiQuestionLevel;
 import com.qmth.teachcloud.mark.mapper.MarkAiQuestionLevelMapper;
@@ -26,4 +27,11 @@ public class MarkAiQuestionLevelServiceImpl extends ServiceImpl<MarkAiQuestionLe
         queryWrapper.lambda().eq(MarkAiQuestionLevel::getAiQuestionId, id);
         return this.list(queryWrapper);
     }
+
+    @Override
+    public void deleteByAiQuestionId(Long aiQuestionId) {
+        UpdateWrapper<MarkAiQuestionLevel> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().eq(MarkAiQuestionLevel::getAiQuestionId, aiQuestionId);
+        this.remove(updateWrapper);
+    }
 }

+ 16 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionParamServiceImpl.java

@@ -10,6 +10,7 @@ import com.qmth.teachcloud.mark.mapper.MarkAiQuestionParamMapper;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionLevelService;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionParamService;
 import com.qmth.teachcloud.mark.service.MarkAiQuestionPointService;
+import com.qmth.teachcloud.mark.service.MarkPaperService;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.stereotype.Service;
 
@@ -64,4 +65,19 @@ public class MarkAiQuestionParamServiceImpl extends ServiceImpl<MarkAiQuestionPa
         queryWrapper.lambda().eq(MarkAiQuestionParam::getQuestionId, questionId);
         return this.getOne(queryWrapper);
     }
+
+    @Override
+    public void deleteByExamIdAndPaperNumberAndQuestionId(Long examId, String paperNumber, Long questionId) {
+        MarkAiQuestionParam markAiQuestionParam = this.getByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, questionId);
+        if(markAiQuestionParam != null){
+            // 删除AI智能评卷参数
+            this.removeById(markAiQuestionParam.getId());
+            // 删除知识点
+            if(AiQuestionParamModeStatus.POINT.equals(markAiQuestionParam.getMode())){
+                markAiQuestionPointService.deleteByAiQuestionId(markAiQuestionParam.getId());
+            } else if(AiQuestionParamModeStatus.LEVEL.equals(markAiQuestionParam.getMode())){
+                markAiQuestionLevelService.deleteByAiQuestionId(markAiQuestionParam.getId());
+            }
+        }
+    }
 }

+ 8 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkAiQuestionPointServiceImpl.java

@@ -1,6 +1,7 @@
 package com.qmth.teachcloud.mark.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.qmth.teachcloud.mark.entity.MarkAiQuestionPoint;
 import com.qmth.teachcloud.mark.mapper.MarkAiQuestionPointMapper;
@@ -26,4 +27,11 @@ public class MarkAiQuestionPointServiceImpl extends ServiceImpl<MarkAiQuestionPo
         queryWrapper.lambda().eq(MarkAiQuestionPoint::getAiQuestionId, id);
         return this.list(queryWrapper);
     }
+
+    @Override
+    public void deleteByAiQuestionId(Long aiQuestionId) {
+        UpdateWrapper<MarkAiQuestionPoint> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().eq(MarkAiQuestionPoint::getAiQuestionId, aiQuestionId);
+        this.remove(updateWrapper);
+    }
 }

+ 10 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkOcrStudentQuestionServiceImpl.java

@@ -1,6 +1,7 @@
 package com.qmth.teachcloud.mark.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.qmth.teachcloud.mark.entity.MarkOcrStudentQuestion;
 import com.qmth.teachcloud.mark.mapper.MarkOcrStudentQuestionMapper;
@@ -28,4 +29,13 @@ public class MarkOcrStudentQuestionServiceImpl extends ServiceImpl<MarkOcrStuden
                 .orderByAsc(MarkOcrStudentQuestion::getNumber);
         return this.list(queryWrapper);
     }
+
+    @Override
+    public void deleteByExamIdAndPaperNumberAndQuestionId(Long examId, String paperNumber, Long questionId) {
+        UpdateWrapper<MarkOcrStudentQuestion> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().eq(MarkOcrStudentQuestion::getExamId, examId)
+                .eq(MarkOcrStudentQuestion::getPaperNumber, paperNumber)
+                .eq(MarkOcrStudentQuestion::getQuestionId, questionId);
+        this.remove(updateWrapper);
+    }
 }

+ 70 - 11
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkQuestionServiceImpl.java

@@ -12,13 +12,14 @@ import com.qmth.teachcloud.common.bean.dto.mark.ObjectiveAnswerDto;
 import com.qmth.teachcloud.common.bean.dto.mark.PictureConfig;
 import com.qmth.teachcloud.common.bean.params.mark.group.QuestionPictureConfigParams;
 import com.qmth.teachcloud.common.bean.result.ExcelResult;
-import com.qmth.teachcloud.common.bean.sync.ExamTaskDataVo;
 import com.qmth.teachcloud.common.bean.vo.FilePathVo;
 import com.qmth.teachcloud.common.contant.SystemConstant;
+import com.qmth.teachcloud.common.entity.BasicSchool;
 import com.qmth.teachcloud.common.entity.MarkQuestion;
 import com.qmth.teachcloud.common.entity.SysUser;
 import com.qmth.teachcloud.common.enums.*;
 import com.qmth.teachcloud.common.enums.mark.MarkPaperAiMark;
+import com.qmth.teachcloud.common.mapper.BasicSchoolMapper;
 import com.qmth.teachcloud.common.service.TeachcloudCommonService;
 import com.qmth.teachcloud.common.util.ConvertUtil;
 import com.qmth.teachcloud.common.util.ExcelUtil;
@@ -34,11 +35,7 @@ import com.qmth.teachcloud.mark.dto.mark.manage.*;
 import com.qmth.teachcloud.mark.dto.mark.setting.MarkGroupTaskDto;
 import com.qmth.teachcloud.mark.dto.mark.setting.MarkQuestionDto;
 import com.qmth.teachcloud.mark.dto.mark.setting.MarkQuestionSubjectiveStepStatusDto;
-import com.qmth.teachcloud.mark.entity.MarkPaper;
-import com.qmth.teachcloud.mark.entity.MarkQuestionAnswer;
-import com.qmth.teachcloud.mark.entity.MarkTask;
-import com.qmth.teachcloud.mark.entity.MarkUserQuestion;
-import com.qmth.teachcloud.mark.enums.ExamType;
+import com.qmth.teachcloud.mark.entity.*;
 import com.qmth.teachcloud.mark.enums.LockType;
 import com.qmth.teachcloud.mark.enums.MarkTaskStatus;
 import com.qmth.teachcloud.mark.lock.LockService;
@@ -71,6 +68,8 @@ import java.util.stream.Stream;
 @Service
 public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, MarkQuestion> implements MarkQuestionService {
 
+    @Resource
+    private BasicSchoolMapper basicSchoolMapper;
     @Resource
     private TeachcloudCommonService teachcloudCommonService;
     @Resource
@@ -88,6 +87,8 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
     @Resource
     private MarkArbitrateHistoryService markArbitrateHistoryService;
     @Resource
+    private MarkAiQuestionParamService markAiQuestionParamService;
+    @Resource
     private MarkService markService;
     @Resource
     private MarkSyncService markSyncService;
@@ -202,6 +203,8 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
                         // 用来判断是否保存过
                         markQuestion.setUpdateId(sysUser.getId());
                         markQuestion.setUpdateTime(System.currentTimeMillis());
+                        // 是否需要清除OCR识别结果
+                        markQuestion.setClearOcrResult(!question.getObjective() && question.getTotalScore() - markQuestion.getTotalScore() != 0);
                         saveOrUpdateList.add(markQuestion);
                         markQuestionList.remove(markQuestion);
                     } else {
@@ -229,6 +232,8 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
                     }
                     markTaskService.deleteByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, markQuestion.getId());
                     markUserQuestionService.deleteByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, markQuestion.getId());
+                    // 删除AI智能评卷参数
+                    markAiQuestionParamService.deleteByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, markQuestion.getId());
                 }
 
                 markQuestionList.stream().filter(m -> m.getObjective()).forEach(m -> {
@@ -598,12 +603,24 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
     public void updatePicListByQuestionId(QuestionPictureConfigParams questionPictureConfigParams) {
         List<PictureConfig> pictureConfigs = questionPictureConfigParams.getPictureConfigs();
         if (CollectionUtils.isEmpty(pictureConfigs)) {
-            throw ExceptionResultEnum.ERROR.exception("没有评卷区数据");
+            throw ExceptionResultEnum.ERROR.exception("未设置评卷区");
+        }
+
+        for (Long questionId : questionPictureConfigParams.getQuestionIds()) {
+            MarkQuestion markQuestion = this.getById(questionId);
+            boolean clearOcrResult = false;
+            if (!markQuestion.getObjective() && markQuestion.getQuestionType() != 4) {
+                // 评卷区是否变化
+                List<PictureConfig> picList = JSON.parseArray(markQuestion.getPicList(), PictureConfig.class);
+                clearOcrResult = !CollectionUtils.isEqualCollection(pictureConfigs, picList);
+            }
+
+            UpdateWrapper<MarkQuestion> updateWrapper = new UpdateWrapper<>();
+            updateWrapper.lambda().set(MarkQuestion::getPicList, JSON.toJSONString(pictureConfigs))
+                    .set(!markQuestion.getClearOcrResult(), MarkQuestion::getClearOcrResult, clearOcrResult)
+                    .eq(MarkQuestion::getId, questionId);
+            this.update(updateWrapper);
         }
-        UpdateWrapper<MarkQuestion> updateWrapper = new UpdateWrapper<>();
-        updateWrapper.lambda().set(MarkQuestion::getPicList, JSON.toJSONString(pictureConfigs))
-                .in(MarkQuestion::getId, questionPictureConfigParams.getQuestionIds());
-        this.update(updateWrapper);
     }
 
     @Override
@@ -654,6 +671,7 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
             markGroupProgressDto.setCurrentCount(markService.applyCurrentCount(markQuestion));
             markGroupProgressDto.setPercent(Calculator.divide2String(Calculator.multiply(markGroupProgressDto.getMarkedCount(), 100), Double.valueOf(markGroupProgressDto.getTaskCount()), 2));
             markGroupProgressDto.setArbitrateCount(markArbitrateHistoryService.waitArbitrateCount(examId, paperNumber, markQuestion.getId(), null));
+            markGroupProgressDto.setAiMark(markQuestion.getAiMark() != null && !MarkPaperAiMark.NONE.equals(markQuestion.getAiMark()));
             markGroupProgressDtoList.add(markGroupProgressDto);
         }
         markGroupSummaryProgressDto.setGroupInfo(markGroupProgressDtoList);
@@ -874,4 +892,45 @@ public class MarkQuestionServiceImpl extends ServiceImpl<MarkQuestionMapper, Mar
     public int countUnBindMarkerQuestion(Long examId, String paperNumber) {
         return this.baseMapper.countUnBindMarkerQuestion(examId, paperNumber);
     }
+
+    @Override
+    public boolean resetAiTask(Long questionId) {
+        MarkQuestion markQuestion = this.getById(questionId);
+        if (markQuestion == null) {
+            throw ExceptionResultEnum.ERROR.exception("题目不存在");
+        }
+        if (lockService.trylock(LockType.AI_TASK_RESET, questionId)) {
+            markSyncService.aiTaskResetSync(markQuestion, questionId);
+            return true;
+        } else {
+            throw ExceptionResultEnum.ERROR.exception("AI评卷任务正在重置");
+        }
+    }
+
+    @Override
+    public boolean enableAi(Long questionId, Boolean enableAi) {
+        UpdateWrapper<MarkQuestion> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().set(MarkQuestion::getEnableAi, enableAi)
+                .eq(MarkQuestion::getId, questionId);
+        if (this.update(updateWrapper)) {
+            if (enableAi) {
+                MarkQuestion markQuestion = this.getById(questionId);
+                BasicSchool basicSchool = basicSchoolMapper.selectByExamId(markQuestion.getExamId());
+
+                if (lockService.trylock(LockType.AI_MARK, markQuestion.getId())) {
+                    markSyncService.aiAutoMark(basicSchool.getId(), markQuestion);
+                    return true;
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean updateClearOcrResult(Long questionId, Boolean clearOcrResult) {
+        UpdateWrapper<MarkQuestion> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().eq(MarkQuestion::getClearOcrResult, clearOcrResult)
+                .set(MarkQuestion::getId, questionId);
+        return this.update(updateWrapper);
+    }
 }

+ 303 - 4
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkServiceImpl.java

@@ -7,43 +7,60 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.metadata.OrderItem;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.qmth.boot.api.exception.ApiException;
+import com.qmth.boot.core.ai.client.OcrApiClient;
+import com.qmth.boot.core.ai.model.llm.score.AutoScoreRequest;
+import com.qmth.boot.core.ai.model.llm.score.AutoScoreResult;
+import com.qmth.boot.core.ai.model.llm.score.StandardAnswer;
+import com.qmth.boot.core.ai.model.ocr.OcrType;
+import com.qmth.boot.core.ai.service.AiService;
+import com.qmth.boot.core.retrofit.utils.UploadFile;
+import com.qmth.teachcloud.common.bean.dto.mark.PictureConfig;
+import com.qmth.teachcloud.common.bean.marking.MarkConfigItem;
+import com.qmth.teachcloud.common.bean.vo.FilePathVo;
 import com.qmth.teachcloud.common.contant.SystemConstant;
 import com.qmth.teachcloud.common.entity.BasicCourse;
+import com.qmth.teachcloud.common.entity.BasicSchool;
 import com.qmth.teachcloud.common.entity.MarkQuestion;
 import com.qmth.teachcloud.common.entity.SysUser;
 import com.qmth.teachcloud.common.enums.ExceptionResultEnum;
 import com.qmth.teachcloud.common.enums.ScorePolicy;
 import com.qmth.teachcloud.common.enums.mark.*;
+import com.qmth.teachcloud.common.mapper.BasicCourseMapper;
+import com.qmth.teachcloud.common.mapper.BasicSchoolMapper;
 import com.qmth.teachcloud.common.service.BasicCourseService;
 import com.qmth.teachcloud.common.service.BasicOperationLogService;
+import com.qmth.teachcloud.common.service.FileUploadService;
 import com.qmth.teachcloud.common.service.SysUserService;
+import com.qmth.teachcloud.common.util.FileUtil;
 import com.qmth.teachcloud.common.util.ServletUtil;
+import com.qmth.teachcloud.mark.dto.ai.SheetImageDto;
 import com.qmth.teachcloud.mark.dto.mark.ScoreItem;
 import com.qmth.teachcloud.mark.dto.mark.manage.Task;
+import com.qmth.teachcloud.mark.dto.mark.manage.TrackDTO;
 import com.qmth.teachcloud.mark.dto.mark.mark.MarkSettingDto;
 import com.qmth.teachcloud.mark.dto.mark.mark.MarkStatusDto;
 import com.qmth.teachcloud.mark.dto.mark.mark.SubmitResult;
 import com.qmth.teachcloud.mark.entity.*;
-import com.qmth.teachcloud.mark.enums.ExamType;
-import com.qmth.teachcloud.mark.enums.LockType;
-import com.qmth.teachcloud.mark.enums.MarkTaskStatus;
-import com.qmth.teachcloud.mark.enums.QuestionModel;
+import com.qmth.teachcloud.mark.enums.*;
 import com.qmth.teachcloud.mark.lock.LockService;
 import com.qmth.teachcloud.mark.params.MarkArbitrateResult;
 import com.qmth.teachcloud.mark.params.MarkResult;
 import com.qmth.teachcloud.mark.params.MarkResultQuestion;
 import com.qmth.teachcloud.mark.service.*;
+import com.qmth.teachcloud.mark.utils.AiUtil;
 import com.qmth.teachcloud.mark.utils.BigDecimalUtils;
 import com.qmth.teachcloud.mark.utils.TaskLock;
 import com.qmth.teachcloud.mark.utils.TaskLockUtil;
 import io.lettuce.core.GeoArgs.Sort;
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
+import java.io.File;
 import java.math.BigDecimal;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -57,6 +74,16 @@ public class MarkServiceImpl implements MarkService {
 
     private Map<Long, Long> markerLastUpdateTime = new ConcurrentHashMap<>();
 
+    @Resource
+    private BasicSchoolMapper basicSchoolMapper;
+    @Resource
+    private BasicCourseMapper basicCourseMapper;
+    @Resource
+    private AiService aiService;
+    @Resource
+    private FileUploadService fileUploadService;
+    @Resource
+    private MarkOcrStudentQuestionService markOcrStudentQuestionService;
     @Resource
     private BasicCourseService basicCourseService;
     @Resource
@@ -104,6 +131,12 @@ public class MarkServiceImpl implements MarkService {
     private BasicOperationLogService basicOperationLogService;
     @Resource
     private MarkAiQuestionParamService markAiQuestionParamService;
+    @Resource
+    private MarkAiQuestionPointService markAiQuestionPointService;
+    @Resource
+    private MarkAiQuestionLevelService markAiQuestionLevelService;
+    @Resource
+    private OcrApiClient ocrApiClient;
 
     /**
      * 释放某个评卷员的考生
@@ -1478,4 +1511,270 @@ public class MarkServiceImpl implements MarkService {
         }
     }
 
+
+    @Override
+    public void aiMark(Long schoolId, Long examId, String paperNumber, Long questionId) {
+        SysUser aiUser = sysUserService.getAiUserBySchoolId(schoolId);
+        if (aiUser != null) {
+            BasicSchool basicSchool = basicSchoolMapper.selectByExamId(examId);
+            BasicCourse basicCourse = basicCourseMapper.selectByExamIdAndPaperNumber(examId, paperNumber);
+            MarkQuestion markQuestion = markQuestionService.getById(questionId);
+            MarkAiQuestionParam markAiQuestionParam = markAiQuestionParamService.getByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, questionId);
+            List<MarkTask> markTasks = null;
+            int pageNumber = 1;
+            do {
+                Set<Long> questions = new HashSet<>(Arrays.asList(questionId));
+                markTasks = markTaskService.findAiUnMarked(new Page<>(pageNumber, 20), examId, paperNumber, questionId);
+                if (markTasks.isEmpty() || markQuestion.getEnableAi()) {
+                    break;
+                }
+                for (MarkTask t : markTasks) {
+                    if (this.applyTask(examId, paperNumber, t.getStudentId(), null, questions, Arrays.asList(t.getId()))) {
+                        break;
+                    }
+                    // 考生作答
+                    try {
+                        long startTime = System.currentTimeMillis();
+                        Map<Integer, SheetImageDto> fileMap = new HashMap<>();
+                        String studentQuestionAnswer = this.mergeQuestionOcrResult(this.listStudentQuestionOcrResult(basicSchool, t.getStudentId(), t.getQuestionId(), fileMap));
+
+                        AutoScoreRequest request = new AutoScoreRequest();
+                        request.setSubjectName(basicCourse.getName());
+                        request.setTotalScore(markQuestion.getTotalScore());
+                        request.setIntervalScore(markAiQuestionParam.getMinScore());
+                        request.setQuestionBody(markAiQuestionParam.getMainTitle());
+                        request.setStudentAnswer(studentQuestionAnswer);
+                        request.setStandardAnswer(buildStandardAnswer(markAiQuestionParam));
+                        // AI评卷
+                        AutoScoreResult autoScoreResult = aiService.autoScore(request, AiUtil.signature(basicSchool));
+                        long endTime = System.currentTimeMillis();
+                        submitAiTask(t, aiUser.getId(), markQuestion, autoScoreResult, buildTrack(autoScoreResult, markQuestion, fileMap), endTime - startTime);
+                    } catch (Exception e) {
+                        t.setAiMarkErrorMsg(e.getMessage());
+                    } finally {
+                        t.setAiMarkErrorMsg(null);
+                        markTaskService.updateAiMarkErrorMsg(t);
+                    }
+                }
+                pageNumber++;
+                // 获取终止评卷标记
+                markQuestion = markQuestionService.getById(questionId);
+                markTasks = markTaskService.findAiUnMarked(new Page<>(pageNumber, 20), examId, paperNumber, questionId);
+            } while (CollectionUtils.isNotEmpty(markTasks));
+        }
+    }
+
+    @Transactional
+    @Override
+    public void resetAiTask(Long examId, String paperNumber, Long questionId) {
+        MarkQuestion markQuestion = markQuestionService.getById(questionId);
+        if (markQuestion == null) {
+            return;
+        }
+        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(examId, paperNumber);
+        if (markPaper == null) {
+            return;
+        }
+        if (markPaper.getStatus() == MarkPaperStatus.FORMAL) {
+            // 遍历相关评卷任务的模式
+            List<MarkTaskStatus> statusList = Arrays.asList(MarkTaskStatus.WAITING);
+            List<MarkTask> markTaskList = markTaskService.listByExamIdAndPaperNumberAndQuestionIdAndAiMarkedAndStatusNotIn(examId, paperNumber, questionId, true, statusList);
+            for (MarkTask markTask : markTaskList) {
+                Long studentId = markTask.getStudentId();
+                markTaskService.resetById(markTask.getId(), null, null, null, null, MarkTaskStatus.WAITING);
+                markSubjectiveScoreService.deleteByStudentIdAndQuestionId(studentId, questionId);
+                markRejectHistoryService.deleteByTaskId(markTask.getId());
+                markArbitrateHistoryService.deleteByExamIdAndPaperNumberAndStudentIdAndQuestionId(examId, paperNumber, studentId, questionId);
+                lockService.waitlock(LockType.STUDENT, markTask.getStudentId());
+                markStudentService.updateSubjectiveStatusAndScore(studentId, SubjectiveStatus.UNMARK);
+                lockService.unlock(LockType.STUDENT, markTask.getStudentId());
+            }
+            //清空ocr识别结果
+            if (markQuestion.getClearOcrResult()) {
+                markOcrStudentQuestionService.deleteByExamIdAndPaperNumberAndQuestionId(examId, paperNumber, questionId);
+                markQuestionService.updateClearOcrResult(questionId, false);
+            }
+        }
+        this.updateMarkedCount(examId, paperNumber, questionId);
+    }
+
+    private List<TrackDTO> buildTrack(AutoScoreResult autoScoreResult, MarkQuestion markQuestion, Map<Integer, SheetImageDto> fileMap) {
+        List<TrackDTO> list = new ArrayList<>();
+        if (fileMap.isEmpty()) {
+            return list;
+        }
+        double[] doubles = autoScoreResult.getStepScore();
+        // 轨迹打印在第一个评卷区上
+        List<MarkConfigItem> pictureConfigList = markQuestion.getPictureConfigList();
+        MarkConfigItem markConfigItem = pictureConfigList.get(0);
+        SheetImageDto sheetImageDto = fileMap.get(markConfigItem.getI());
+        int step = 10;
+        for (int i = 0; i < doubles.length; i++) {
+            int offsetX = (int) (sheetImageDto.getWidth() * markConfigItem.getX()) + step * (i + 1);
+            int offsetY = (int) (sheetImageDto.getHeight() * markConfigItem.getY()) + step;
+            list.add(new TrackDTO(markQuestion, i + 1, doubles[i], markConfigItem.getI(), offsetX, offsetY));
+        }
+        return list;
+    }
+
+    private List<StandardAnswer> buildStandardAnswer(MarkAiQuestionParam markAiQuestionParam) {
+        List<StandardAnswer> standardAnswerList = new ArrayList<>();
+        // 得分点
+        if (AiQuestionParamModeStatus.POINT.equals(markAiQuestionParam.getMode())) {
+            List<MarkAiQuestionPoint> markAiQuestionPoints = markAiQuestionPointService.listByAiQuestionId(markAiQuestionParam.getId());
+            if (CollectionUtils.isEmpty(markAiQuestionPoints)) {
+                throw ExceptionResultEnum.ERROR.exception("未设置得分点");
+            }
+            standardAnswerList = markAiQuestionPoints.stream().map(m -> {
+                StandardAnswer standardAnswer = new StandardAnswer();
+                standardAnswer.setScore(m.getScore());
+                standardAnswer.setContent(m.getAnswer());
+                return standardAnswer;
+            }).collect(Collectors.toList());
+        }
+        // 档次
+        else if (AiQuestionParamModeStatus.LEVEL.equals(markAiQuestionParam.getMode())) {
+            List<MarkAiQuestionLevel> markAiQuestionLevels = markAiQuestionLevelService.listByAiQuestionId(markAiQuestionParam.getId());
+            if (CollectionUtils.isEmpty(markAiQuestionLevels)) {
+                throw ExceptionResultEnum.ERROR.exception("未设置档次");
+            }
+            standardAnswerList = markAiQuestionLevels.stream().map(m -> {
+                StandardAnswer standardAnswer = new StandardAnswer();
+                standardAnswer.setLowScore(m.getMinScore());
+                standardAnswer.setHighScore(m.getMaxScore());
+                standardAnswer.setContent(m.getAnswer());
+                return standardAnswer;
+            }).collect(Collectors.toList());
+        } else {
+            throw ExceptionResultEnum.ERROR.exception("评分模式不存在");
+        }
+        return standardAnswerList;
+    }
+
+    private String mergeQuestionOcrResult(List<MarkOcrStudentQuestion> markOcrStudentQuestionList) {
+        if (CollectionUtils.isEmpty(markOcrStudentQuestionList)) {
+            return null;
+        }
+        return markOcrStudentQuestionList.stream().filter(m -> StringUtils.isNotBlank(m.getOcrContent())).map(MarkOcrStudentQuestion::getOcrContent).collect(Collectors.joining(SystemConstant.COMMA_OF_CHINESE));
+    }
+
+    private List<MarkOcrStudentQuestion> listStudentQuestionOcrResult(BasicSchool basicSchool, Long studentId, Long questionId, Map<Integer, SheetImageDto> fileMap) {
+        List<MarkOcrStudentQuestion> markOcrStudentQuestionList = markOcrStudentQuestionService.listByStudentIdAndQuestionId(studentId, questionId);
+        if (CollectionUtils.isNotEmpty(markOcrStudentQuestionList)) {
+            return markOcrStudentQuestionList;
+        }
+        MarkQuestion markQuestion = markQuestionService.getById(questionId);
+        if (markQuestion == null || StringUtils.isBlank(markQuestion.getPicList())) {
+            throw ExceptionResultEnum.ERROR.exception("大题号[" + markQuestion.getMainNumber() + "]、小题号[" + markQuestion.getSubNumber() + "]未设置评卷区");
+        }
+        // 读取考生图片
+        MarkStudent markStudent = markStudentService.getById(studentId);
+        if (markStudent == null) {
+            throw ExceptionResultEnum.ERROR.exception("未找到考生" + ",考生ID[" + studentId);
+        } else if (StringUtils.isBlank(markStudent.getSheetPath())) {
+            throw ExceptionResultEnum.ERROR.exception("未查询到原图" + ",考号[" + markStudent.getStudentCode());
+        }
+        List<File> deleteFileList = new ArrayList<>();
+        //考生题卡原图
+        try {
+            List<FilePathVo> filePathVoList = markStudent.listSheetPath();
+            for (int i = 0; i < filePathVoList.size(); i++) {
+                FilePathVo filePathVo = filePathVoList.get(i);
+                File file = SystemConstant.getFileTempDirVar(SystemConstant.JPG_PREFIX);
+                try {
+                    fileUploadService.downloadFile(filePathVo.getPath(), filePathVo.getUploadType(), filePathVo.getType(), file.getPath());
+                } catch (Exception e) {
+                    throw ExceptionResultEnum.ERROR.exception("读取考生原图失败" + ",考号[" + markStudent.getStudentCode() + "。" + e.getMessage());
+                }
+                fileMap.put(i + 1, new SheetImageDto(i + 1, file, filePathVo.getMd5(), AiUtil.imageDim(file)));
+                deleteFileList.add(file);
+            }
+
+            // 题目评卷区
+            List<PictureConfig> pictureConfigList = JSON.parseArray(markQuestion.getPicList(), PictureConfig.class);
+
+            List<MarkOcrStudentQuestion> markOcrStudentQuestions = new ArrayList<>();
+            for (int i = 0; i < pictureConfigList.size(); i++) {
+                PictureConfig pictureConfig = pictureConfigList.get(i);
+                SheetImageDto sheetImageDto = fileMap.get(pictureConfig.getI());
+                File file = SystemConstant.getFileTempDirVar(SystemConstant.JPG_PREFIX);
+                AiUtil.subImg(sheetImageDto, pictureConfig, file);
+
+                // OCR识别
+                String ocrResult;
+                try {
+                    ocrResult = ocrApiClient.forImage(AiUtil.signature(basicSchool), OcrType.HANDWRITING, UploadFile.build("image", "", file));
+                } catch (Exception e) {
+                    throw ExceptionResultEnum.ERROR.exception("OCR识别失败" + ",考号[" + markStudent.getStudentCode() + "。" + e.getMessage());
+                }
+                MarkOcrStudentQuestion markOcrStudentQuestion = new MarkOcrStudentQuestion();
+                markOcrStudentQuestion.setId(SystemConstant.getDbUuid());
+                markOcrStudentQuestion.setExamId(markStudent.getExamId());
+                markOcrStudentQuestion.setPaperNumber(markStudent.getPaperNumber());
+                markOcrStudentQuestion.setStudentId(studentId);
+                markOcrStudentQuestion.setQuestionId(questionId);
+                markOcrStudentQuestion.setNumber(i + 1);
+                markOcrStudentQuestion.setMd5(sheetImageDto.getMd5());
+                markOcrStudentQuestion.setOcrContent(ocrResult);
+                markOcrStudentQuestion.setCreateTime(System.currentTimeMillis());
+                markOcrStudentQuestions.add(markOcrStudentQuestion);
+                deleteFileList.add(file);
+            }
+            markOcrStudentQuestionService.saveBatch(markOcrStudentQuestions);
+            return markOcrStudentQuestions;
+        } catch (RuntimeException e) {
+            throw ExceptionResultEnum.ERROR.exception(e.getMessage());
+        } finally {
+            if (CollectionUtils.isNotEmpty(deleteFileList)) {
+                for (File file : deleteFileList) {
+                    FileUtil.deleteFile(file);
+                }
+            }
+        }
+    }
+
+    private boolean submitAiTask(MarkTask task, Long userId, MarkQuestion markQuestion, AutoScoreResult result, List<TrackDTO> trackDTOList, long spent) {
+        // 非待评任务
+        if (!MarkTaskStatus.WAITING.equals(task.getStatus())) {
+            return false;
+        }
+        // 是否多评情况下已处理过该考生评卷任务
+        if (markTaskService.countByStudentIdAndMarkerIdAndIdNotEqual(task.getStudentId(), task.getExamId(),
+                task.getPaperNumber(), task.getQuestionId(), userId, task.getId()) > 0) {
+            return false;
+        }
+        // 尝试提交评卷结果
+        Long now = System.currentTimeMillis();
+        //更新阅卷任务状态为已阅卷[阅卷分数,给分明细,阅卷时间,评卷时长]
+        if (!markTaskService.updateAiMarkResult(task.getId(), MarkTaskStatus.MARKED, userId, result.getTotalScore(), trackDTOList, spent, now, MarkTaskStatus.WAITING)) {
+            // 条件不符更新失败,直接返回
+            return false;
+        }
+        // 判断多评模式下是否需要仲裁
+        MarkArbitrateHistory history = null;
+        if (markQuestion.getArbitrateThreshold() != null && markQuestion.getArbitrateThreshold() > 0) {
+            // 多评模式
+            List<MarkTask> list = markTaskService.findByStudentIdAndQuestionIdAndStatus(task.getStudentId(), task.getQuestionId(), MarkTaskStatus.MARKED);
+            for (MarkTask other : list) {
+                // 本评卷任务或组长已打分,则跳过该任务
+                if (other.getId().equals(task.getId()) || other.getHeaderScore() != null) {
+                    continue;
+                }
+                // 分差超过阀值
+                if (Math.abs(other.getMarkerScore() - result.getTotalScore()) > markQuestion.getArbitrateThreshold()) {
+                    history = buildArbitrateHistory(task, now);
+                    break;
+                }
+            }
+        }
+        if (history != null) {
+            // 保存仲裁记录
+            markArbitrateHistoryService.save(history);
+            // 触发仲裁后续状态更新
+            markTaskService.updateStatusByStudentIdAndQuestionId(task.getStudentId(), task.getQuestionId(), MarkTaskStatus.WAIT_ARBITRATE);
+            // 未评完
+            resetStudentStatus(task.getStudentId());
+        }
+        return true;
+    }
 }

+ 32 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkSyncServiceImpl.java

@@ -2,6 +2,7 @@ package com.qmth.teachcloud.mark.service.impl;
 
 import com.qmth.teachcloud.common.entity.MarkQuestion;
 import com.qmth.teachcloud.mark.entity.MarkPaper;
+import com.qmth.teachcloud.mark.entity.MarkTask;
 import com.qmth.teachcloud.mark.entity.MarkUserQuestion;
 import com.qmth.teachcloud.mark.enums.LockType;
 import com.qmth.teachcloud.mark.lock.LockService;
@@ -96,4 +97,35 @@ public class MarkSyncServiceImpl implements MarkSyncService {
             lockService.unlock(LockType.QUESTION_UPDATE, markQuestion.getId());
         }
     }
+
+    @Async
+    @Override
+    public void aiTaskResetSync(MarkQuestion markQuestion, Long questionId) {
+        Long examId = markQuestion.getExamId();
+        String paperNumber = markQuestion.getPaperNumber();
+        try {
+            lockService.waitlock(LockType.EXAM_SUBJECT, examId, paperNumber);
+            lockService.waitlock(LockType.QUESTION, questionId);
+            markService.resetAiTask(examId, paperNumber, questionId);
+        } catch (Exception e) {
+            log.error("reset ai task error", e);
+        } finally {
+            lockService.unlock(LockType.QUESTION, questionId);
+            lockService.unlock(LockType.EXAM_SUBJECT, examId, paperNumber);
+        }
+    }
+
+    @Async
+    @Override
+    public void aiAutoMark(Long schoolId, MarkQuestion markQuestion) {
+        try {
+            if (markQuestion != null) {
+                markService.aiMark(schoolId, markQuestion.getExamId(), markQuestion.getPaperNumber(), markQuestion.getId());
+            }
+        } catch (Exception e) {
+            log.error("ai auto mark error", e);
+        } finally {
+            lockService.unlock(LockType.AI_MARK, markQuestion.getId());
+        }
+    }
 }

+ 37 - 0
teachcloud-mark/src/main/java/com/qmth/teachcloud/mark/service/impl/MarkTaskServiceImpl.java

@@ -143,6 +143,17 @@ public class MarkTaskServiceImpl extends ServiceImpl<MarkTaskMapper, MarkTask> i
         return this.list(queryWrapper);
     }
 
+    @Override
+    public List<MarkTask> listByExamIdAndPaperNumberAndQuestionIdAndAiMarkedAndStatusNotIn(Long examId, String paperNumber, Long questionId, Boolean aiMarked, List<MarkTaskStatus> statusList) {
+        QueryWrapper<MarkTask> queryWrapper = new QueryWrapper<>();
+        queryWrapper.lambda().eq(MarkTask::getExamId, examId)
+                .eq(MarkTask::getPaperNumber, paperNumber)
+                .eq(MarkTask::getQuestionId, questionId)
+                .eq(MarkTask::getAiMarked, aiMarked)
+                .notIn(MarkTask::getStatus, statusList);
+        return this.list(queryWrapper);
+    }
+
     @Override
     public boolean resetById(Long markTaskId, Long userId, String rejectReason, Long rejectId, Long date, MarkTaskStatus newStatus) {
         UpdateWrapper<MarkTask> updateWrapper = new UpdateWrapper<>();
@@ -159,6 +170,7 @@ public class MarkTaskServiceImpl extends ServiceImpl<MarkTaskMapper, MarkTask> i
                 .set(MarkTask::getHeaderTrackList, null)
                 .set(MarkTask::getHeaderTagList, null)
                 .set(MarkTask::getRejectReason, rejectReason)
+                .set(MarkTask::getAiMarkErrorMsg, null)
                 .eq(MarkTask::getId, markTaskId);
         return this.update(updateWrapper);
     }
@@ -353,6 +365,23 @@ public class MarkTaskServiceImpl extends ServiceImpl<MarkTaskMapper, MarkTask> i
         return this.update(updateWrapper);
     }
 
+    @Override
+    public boolean updateAiMarkResult(Long taskId, MarkTaskStatus status, Long userId, Double markerScore, List<TrackDTO> trackList, Long spent, Long now, MarkTaskStatus statusWaiting) {
+        UpdateWrapper<MarkTask> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().eq(MarkTask::getId, taskId)
+                .eq(MarkTask::getStatus, statusWaiting)
+                .set(MarkTask::getStatus, status)
+                .set(MarkTask::getUserId, userId)
+                .set(MarkTask::getMarkerScore, markerScore)
+                .set(MarkTask::getMarkerTrackList, JSON.toJSONString(trackList))
+                .set(MarkTask::getMarkerTime, now)
+                .set(MarkTask::getMarkerSpent, spent)
+                .set(MarkTask::getHeaderId, null)
+                .set(MarkTask::getHeaderTime, null)
+                .set(MarkTask::getRejectReason, null);
+        return this.update(updateWrapper);
+    }
+
     @Override
     public int countByExamIdAndPaperNumberAndQuestionIdAndTaskNumber(Long examId, String paperNumber, Long questionId, int taskNumber) {
         QueryWrapper<MarkTask> queryWrapper = new QueryWrapper<>();
@@ -510,4 +539,12 @@ public class MarkTaskServiceImpl extends ServiceImpl<MarkTaskMapper, MarkTask> i
         List<MarkAiExport> markAiExportList = this.baseMapper.exportAiMark(semesterId, examId, courseId, paperNumber, dpr);
         ExcelUtil.excelExport("AI智能评卷导出", MarkAiExport.class, markAiExportList, response);
     }
+
+    @Override
+    public void updateAiMarkErrorMsg(MarkTask t) {
+        UpdateWrapper<MarkTask> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.lambda().set(MarkTask::getAiMarkErrorMsg, t.getAiMarkErrorMsg())
+                .eq(MarkTask::getId, t.getId());
+        this.update(updateWrapper);
+    }
 }