xiatian 3 meses atrás
commit
320c21ed67
92 arquivos alterados com 7826 adições e 0 exclusões
  1. 53 0
      .gitignore
  2. 53 0
      db/am_db.sql
  3. BIN
      file/question-import.xlsx
  4. BIN
      file/score-import.xlsx
  5. BIN
      file/student-import.xlsx
  6. 6 0
      file/启动服务.bat
  7. 30 0
      file/说明.txt
  8. 148 0
      pom.xml
  9. 21 0
      src/main/java/cn/com/qmth/am/AmApplication.java
  10. 22 0
      src/main/java/cn/com/qmth/am/bean/AiMarkingDto.java
  11. 26 0
      src/main/java/cn/com/qmth/am/bean/AnswerImageDto.java
  12. 26 0
      src/main/java/cn/com/qmth/am/bean/AutoScoreEnRequest.java
  13. 36 0
      src/main/java/cn/com/qmth/am/bean/DataKey.java
  14. 40 0
      src/main/java/cn/com/qmth/am/bean/ImageSlice.java
  15. 39 0
      src/main/java/cn/com/qmth/am/bean/ImportResult.java
  16. 35 0
      src/main/java/cn/com/qmth/am/bean/OcrDto.java
  17. 35 0
      src/main/java/cn/com/qmth/am/bean/StudentInfo.java
  18. 64 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreDto.java
  19. 45 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreImageDto.java
  20. 25 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreInfo.java
  21. 27 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatContent.java
  22. 20 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatReq.java
  23. 17 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatResult.java
  24. 25 0
      src/main/java/cn/com/qmth/am/bean/ds/DsChoice.java
  25. 26 0
      src/main/java/cn/com/qmth/am/bean/ds/ImageContent.java
  26. 20 0
      src/main/java/cn/com/qmth/am/bean/ds/ImageUrl.java
  27. 33 0
      src/main/java/cn/com/qmth/am/bean/ds/MarkingReq.java
  28. 4 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrContent.java
  29. 30 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrMessage.java
  30. 28 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrReq.java
  31. 17 0
      src/main/java/cn/com/qmth/am/bean/ds/TextContent.java
  32. 22 0
      src/main/java/cn/com/qmth/am/config/FillMetaObjectHandler.java
  33. 60 0
      src/main/java/cn/com/qmth/am/config/InitData.java
  34. 15 0
      src/main/java/cn/com/qmth/am/config/MyBatisPlusConfig.java
  35. 154 0
      src/main/java/cn/com/qmth/am/config/SysProperty.java
  36. 48 0
      src/main/java/cn/com/qmth/am/consumer/MarkingConsumer.java
  37. 60 0
      src/main/java/cn/com/qmth/am/consumer/OcrConsumer.java
  38. 387 0
      src/main/java/cn/com/qmth/am/controller/AdminController.java
  39. 9 0
      src/main/java/cn/com/qmth/am/dao/QuestionDao.java
  40. 14 0
      src/main/java/cn/com/qmth/am/dao/StudentScoreDao.java
  41. 125 0
      src/main/java/cn/com/qmth/am/entity/QuestionEntity.java
  42. 178 0
      src/main/java/cn/com/qmth/am/entity/StudentScoreEntity.java
  43. 45 0
      src/main/java/cn/com/qmth/am/entity/base/BaseEntity.java
  44. 27 0
      src/main/java/cn/com/qmth/am/entity/base/IdEntity.java
  45. 34 0
      src/main/java/cn/com/qmth/am/enums/DataStatus.java
  46. 32 0
      src/main/java/cn/com/qmth/am/enums/DataType.java
  47. 30 0
      src/main/java/cn/com/qmth/am/enums/ImportFileName.java
  48. 21 0
      src/main/java/cn/com/qmth/am/enums/LockType.java
  49. 19 0
      src/main/java/cn/com/qmth/am/enums/ModelTypeBak.java
  50. 16 0
      src/main/java/cn/com/qmth/am/handle/ImageSliceListTypeHandler.java
  51. 66 0
      src/main/java/cn/com/qmth/am/handle/ListTypeHandler.java
  52. 15 0
      src/main/java/cn/com/qmth/am/handle/StandardAnswerListTypeHandler.java
  53. 54 0
      src/main/java/cn/com/qmth/am/multithread/AopTargetUtils.java
  54. 162 0
      src/main/java/cn/com/qmth/am/multithread/Basket.java
  55. 83 0
      src/main/java/cn/com/qmth/am/multithread/Consumer.java
  56. 11 0
      src/main/java/cn/com/qmth/am/multithread/EndObject.java
  57. 167 0
      src/main/java/cn/com/qmth/am/multithread/Producer.java
  58. 102 0
      src/main/java/cn/com/qmth/am/multithread/consumer/LocalOcrConsumer.java
  59. 31 0
      src/main/java/cn/com/qmth/am/multithread/producer/LocalOcrProducer.java
  60. 15 0
      src/main/java/cn/com/qmth/am/service/DsMarkingService.java
  61. 10 0
      src/main/java/cn/com/qmth/am/service/OcrService.java
  62. 23 0
      src/main/java/cn/com/qmth/am/service/QuestionService.java
  63. 51 0
      src/main/java/cn/com/qmth/am/service/StudentScoreService.java
  64. 32 0
      src/main/java/cn/com/qmth/am/service/StudentService.java
  65. 169 0
      src/main/java/cn/com/qmth/am/service/impl/DsMarkingServiceImpl.java
  66. 66 0
      src/main/java/cn/com/qmth/am/service/impl/OcrServiceImpl.java
  67. 481 0
      src/main/java/cn/com/qmth/am/service/impl/QuestionServiceImpl.java
  68. 912 0
      src/main/java/cn/com/qmth/am/service/impl/StudentScoreServiceImpl.java
  69. 303 0
      src/main/java/cn/com/qmth/am/service/impl/StudentServiceImpl.java
  70. 97 0
      src/main/java/cn/com/qmth/am/task/AiMarkingJob.java
  71. 125 0
      src/main/java/cn/com/qmth/am/task/OcrJob.java
  72. 36 0
      src/main/java/cn/com/qmth/am/task/QuestionImportJob.java
  73. 36 0
      src/main/java/cn/com/qmth/am/task/StudentImportJob.java
  74. 36 0
      src/main/java/cn/com/qmth/am/task/StudentScoreImportJob.java
  75. 40 0
      src/main/java/cn/com/qmth/am/utils/BatchSetDataUtil.java
  76. 200 0
      src/main/java/cn/com/qmth/am/utils/Calculator.java
  77. 899 0
      src/main/java/cn/com/qmth/am/utils/FileUtil.java
  78. 51 0
      src/main/java/cn/com/qmth/am/utils/FormFilePart.java
  79. 44 0
      src/main/java/cn/com/qmth/am/utils/FreeMarkerUtil.java
  80. 108 0
      src/main/java/cn/com/qmth/am/utils/HttpMethod.java
  81. 376 0
      src/main/java/cn/com/qmth/am/utils/ImageUtil.java
  82. 96 0
      src/main/java/cn/com/qmth/am/utils/JsonHelper.java
  83. 88 0
      src/main/java/cn/com/qmth/am/utils/MD5Util.java
  84. 277 0
      src/main/java/cn/com/qmth/am/utils/OKHttpUtil.java
  85. 64 0
      src/main/java/cn/com/qmth/am/utils/ResouceUtil.java
  86. 63 0
      src/main/java/cn/com/qmth/am/utils/SSLSocketClient.java
  87. 74 0
      src/main/java/cn/com/qmth/am/utils/SpringContextHolder.java
  88. 53 0
      src/main/resources/application.properties
  89. 5 0
      src/main/resources/mapper/QuestionMapper.xml
  90. 7 0
      src/main/resources/mapper/StudentScoreMapper.xml
  91. 23 0
      src/main/resources/templates/ds_marking.ftl
  92. 28 0
      src/main/resources/templates/ds_marking_translation.ftl

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+*.class
+*.log
+
+
+### Eclipse & STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+/logs/
+/log/
+/data/
+
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+
+### VS Code ###
+.vscode
+node_modules
+package-lock.json
+yarn.lock
+
+
+### Package Files ###
+*.zip
+*.war
+*.ear
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+target/
+
+.flattened-pom.xml
+.DS_Store
+

+ 53 - 0
db/am_db.sql

@@ -0,0 +1,53 @@
+CREATE DATABASE IF NOT EXISTS ai_marking_stmms_db default character set utf8mb4 collate utf8mb4_general_ci;
+
+CREATE USER IF NOT EXISTS 'ai_marking_stmms'@'%' IDENTIFIED BY 'ai_marking_stmms';
+
+GRANT ALL PRIVILEGES  ON `ai_marking_stmms_db`.* TO 'ai_marking_stmms'@'localhost' IDENTIFIED BY 'ai_marking_stmms';
+
+FLUSH  PRIVILEGES;
+
+USE ai_marking_stmms_db;
+
+DROP TABLE IF EXISTS `am_question`;
+CREATE TABLE `am_question` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `create_time` datetime DEFAULT NULL,
+  `update_time` datetime DEFAULT NULL,
+  `exam_id` bigint NOT NULL,
+  `subject_code` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+  `subject_name` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+  `title` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+  `main_number` int NOT NULL,
+  `sub_number` int NOT NULL,
+  `full_score` double NOT NULL,
+  `answer` longtext COLLATE utf8mb4_bin DEFAULT NULL,
+  `content` longtext COLLATE utf8mb4_bin NOT NULL,
+  `image_slice` varchar(1000) COLLATE utf8mb4_bin NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `IDX_QUESTION_01` (`exam_id`, `subject_code`, `main_number`, `sub_number`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+
+
+DROP TABLE IF EXISTS `am_student_score`;
+CREATE TABLE `am_student_score` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `create_time` datetime DEFAULT NULL,
+  `update_time` datetime DEFAULT NULL,
+  `question_id` bigint NOT NULL,
+  `exam_id` bigint NOT NULL,
+  `subject_code` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+  `student_code` varchar(255) COLLATE utf8mb4_bin NOT NULL,
+  `main_number` int NOT NULL,
+  `sub_number` 	 varchar(255) NOT NULL,
+  `answer_status` varchar(255) NOT NULL,
+  `score_status` varchar(255) NOT NULL,
+  `answer` longtext COLLATE utf8mb4_bin DEFAULT NULL,
+  `ai_score` double DEFAULT NULL,
+  `score_ratio` double DEFAULT NULL,
+  `marking_score` double DEFAULT NULL,
+  `err_msg` varchar(2000) COLLATE utf8mb4_bin DEFAULT NULL,
+  `score_none` bit(1) DEFAULT NULL,
+  `step_score` varchar(500) COLLATE utf8mb4_bin DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `IDX_STUDENT_SCORE_01` (`exam_id`, `subject_code`, `student_code`,`question_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

BIN
file/question-import.xlsx


BIN
file/score-import.xlsx


BIN
file/student-import.xlsx


+ 6 - 0
file/启动服务.bat

@@ -0,0 +1,6 @@
+
+chcp 65001
+
+call %QMTH_HOME%\common\jdk\bin\java -server -Xms2g -Xmx2g -jar -Dfile.encoding=utf-8 ai-marking.jar
+
+pause

+ 30 - 0
file/说明.txt

@@ -0,0 +1,30 @@
+首次运行前先执行 初始化数据库.bat
+
+执行  启动服务.bat 来启动服务,启动后不能关闭窗口。
+
+data目录:需导入的Excel文件放在此目录下,系统定时读取,读取成功或失败后移动到data下的success或failed目录下,生成相应的信息文件。data目录可配置修改。
+
+question-import.xlsx为小题导入文件,student-import.xlsx为考生导入文件,score-import.xlsx为人评分导入文件(需要在前两个Excel导入处理完毕后再导入此文件),
+文件名皆不可修改。
+
+application.properties为配置文件
+
+配置文件可修改内容:
+	com.qmth.llm.server:评分接口调用地址 示例 https://solar.qmth.com.cn
+	com.qmth.ocr.server:ocr接口调用地址 示例 https://solar.qmth.com.cn
+	com.qmth.solar.access-key:授权秘钥
+	com.qmth.solar.access-secret:授权秘钥
+	am.ocr-task.enable:是否开启ocr任务true/false
+	am.marking-task.enable:是否开启评分任务true/false
+	am.data-type:数据来源类型,可填MARKING_CLOUD | TEACH_CLOUD,分别对应云阅卷、知学知考。
+	am.image-server:图片文件访问域名 示例:https://file.markingcloud.com
+	am.data-dir:excel导入文件读取目录。示例./data
+
+接口:
+	数据详情查询
+		参数examId 考试id 必填参数
+		示例:http://localhost:8083/api/admin/info?examId=1
+	数据重置(重置后需重新导入人评分数)
+		参数examId 考试id  必填参数
+		参数subjectCode 科目代码(知学知考为课程id) 非必填参数
+		示例:http://localhost:8083/api/admin/info?examId=1&subjectCode=1

+ 148 - 0
pom.xml

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>cn.com.qmth.am</groupId>
+	<artifactId>ai-marking-stmms</artifactId>
+	<version>1.0.0</version>
+	<packaging>jar</packaging>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>2.3.12.RELEASE</version>
+		<relativePath />
+	</parent>
+    <properties>
+        <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
+        <mybatis-plus.version>3.4.3.3</mybatis-plus.version>
+        <maven-compiler-version>3.8.1</maven-compiler-version>
+        <maven-surefire-version>2.22.2</maven-surefire-version>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <qmth-boot-version>1.0.5</qmth-boot-version>
+    </properties>
+
+	<dependencies>
+		<dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>starter-api</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        		<dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-ai</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>data-mybatis-plus</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-schedule</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.jeffreyning</groupId>
+            <artifactId>mybatisplus-plus</artifactId>
+            <version>1.5.1-RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-solar</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-fss</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-cache</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-retrofit</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-concurrent</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>tools-poi</artifactId>
+            <version>${qmth-boot-version}</version>
+        </dependency>
+		<dependency>
+			<groupId>com.qmth.boot</groupId>
+			<artifactId>tools-freemarker</artifactId>
+			<version>${qmth-boot-version}</version>
+		</dependency>
+
+        <!-- Swagger jars start -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>2.0.9</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>1.5.24</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>1.5.24</version>
+        </dependency>
+        <!-- Swagger jars end -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.83</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+            <version>4.4</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.8.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-text</artifactId>
+            <version>1.10.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-math3</artifactId>
+            <version>3.6.1</version>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<finalName>${project.artifactId}</finalName>
+
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<configuration>
+					<includeSystemScope>true</includeSystemScope>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 21 - 0
src/main/java/cn/com/qmth/am/AmApplication.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.am;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import com.github.jeffreyning.mybatisplus.conf.EnableMPP;
+
+@EnableMPP
+@SpringBootApplication
+@EnableScheduling
+@Configuration
+@MapperScan("cn.com.qmth.am.dao")
+public class AmApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(AmApplication.class, args);
+    }
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/am/bean/AiMarkingDto.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.am.bean;
+
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+public class AiMarkingDto {
+	private StudentScoreEntity scoreInfo;
+	private Integer retry=0;
+	public StudentScoreEntity getScoreInfo() {
+		return scoreInfo;
+	}
+	public void setScoreInfo(StudentScoreEntity scoreInfo) {
+		this.scoreInfo = scoreInfo;
+	}
+	public Integer getRetry() {
+		return retry;
+	}
+	public void setRetry(Integer retry) {
+		this.retry = retry;
+	}
+
+
+}

+ 26 - 0
src/main/java/cn/com/qmth/am/bean/AnswerImageDto.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.am.bean;
+
+public class AnswerImageDto {
+	private Integer pageIndex;
+	private byte[] image;
+	private String suff;
+	public Integer getPageIndex() {
+		return pageIndex;
+	}
+	public void setPageIndex(Integer pageIndex) {
+		this.pageIndex = pageIndex;
+	}
+	public byte[] getImage() {
+		return image;
+	}
+	public void setImage(byte[] image) {
+		this.image = image;
+	}
+	public String getSuff() {
+		return suff;
+	}
+	public void setSuff(String suff) {
+		this.suff = suff;
+	}
+	
+}

+ 26 - 0
src/main/java/cn/com/qmth/am/bean/AutoScoreEnRequest.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.am.bean;
+
+import javax.validation.constraints.NotNull;
+
+import org.springframework.validation.annotation.Validated;
+
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+
+/**
+ * 自动判分请求参数
+ */
+@Validated
+public class AutoScoreEnRequest extends AutoScoreRequest {
+
+    @NotNull(message = "题目名称不能为空")
+    private String questionTitle;
+
+    public String getQuestionTitle() {
+        return questionTitle;
+    }
+
+    public void setQuestionTitle(String questionTitle) {
+        this.questionTitle = questionTitle;
+    }
+
+}

+ 36 - 0
src/main/java/cn/com/qmth/am/bean/DataKey.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.am.bean;
+
+import java.util.Objects;
+
+public class DataKey {
+	private int index;
+	private String key;
+	public int getIndex() {
+		return index;
+	}
+	public void setIndex(int index) {
+		this.index = index;
+	}
+	public String getKey() {
+		return key;
+	}
+	public void setKey(String key) {
+		this.key = key;
+	}
+	@Override
+	public int hashCode() {
+		return Objects.hash(key);
+	}
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		DataKey other = (DataKey) obj;
+		return Objects.equals(key, other.key);
+	}
+	
+}

+ 40 - 0
src/main/java/cn/com/qmth/am/bean/ImageSlice.java

@@ -0,0 +1,40 @@
+package cn.com.qmth.am.bean;
+
+public class ImageSlice {
+	private Integer x;
+	private Integer y;
+	private Integer w;
+	private Integer h;
+	private Integer i;
+	public Integer getX() {
+		return x;
+	}
+	public void setX(Integer x) {
+		this.x = x;
+	}
+	public Integer getY() {
+		return y;
+	}
+	public void setY(Integer y) {
+		this.y = y;
+	}
+	public Integer getW() {
+		return w;
+	}
+	public void setW(Integer w) {
+		this.w = w;
+	}
+	public Integer getH() {
+		return h;
+	}
+	public void setH(Integer h) {
+		this.h = h;
+	}
+	public Integer getI() {
+		return i;
+	}
+	public void setI(Integer i) {
+		this.i = i;
+	}
+	
+}

+ 39 - 0
src/main/java/cn/com/qmth/am/bean/ImportResult.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.am.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImportResult {
+
+    private String countInfo;
+
+    private List<String> errMsg;
+
+    public String getCountInfo() {
+        return countInfo;
+    }
+
+    public void setCountInfo(String countInfo) {
+        this.countInfo = countInfo;
+    }
+
+    public List<String> getErrMsg() {
+        return errMsg;
+    }
+
+    public void setErrMsg(List<String> errMsg) {
+        this.errMsg = errMsg;
+    }
+
+	public ImportResult() {
+		super();
+	}
+
+	public ImportResult(String err) {
+		super();
+		errMsg=new ArrayList<>();
+		errMsg.add(err);
+	}
+
+    
+}

+ 35 - 0
src/main/java/cn/com/qmth/am/bean/OcrDto.java

@@ -0,0 +1,35 @@
+package cn.com.qmth.am.bean;
+
+import java.io.File;
+
+import com.qmth.boot.core.solar.model.OrgInfo;
+
+public class OcrDto {
+
+    private File file;
+
+    private OrgInfo org;
+
+    public File getFile() {
+        return file;
+    }
+
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+    public OrgInfo getOrg() {
+        return org;
+    }
+
+    public void setOrg(OrgInfo org) {
+        this.org = org;
+    }
+
+    public OcrDto(File file, OrgInfo org) {
+        super();
+        this.file = file;
+        this.org = org;
+    }
+
+}

+ 35 - 0
src/main/java/cn/com/qmth/am/bean/StudentInfo.java

@@ -0,0 +1,35 @@
+package cn.com.qmth.am.bean;
+
+public class StudentInfo {
+
+    private Long examId;
+
+    private String subjectCode;
+
+    private String studentCode;
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public String getSubjectCode() {
+        return subjectCode;
+    }
+
+    public void setSubjectCode(String subjectCode) {
+        this.subjectCode = subjectCode;
+    }
+
+    public String getStudentCode() {
+        return studentCode;
+    }
+
+    public void setStudentCode(String studentCode) {
+        this.studentCode = studentCode;
+    }
+
+}

+ 64 - 0
src/main/java/cn/com/qmth/am/bean/StudentScoreDto.java

@@ -0,0 +1,64 @@
+package cn.com.qmth.am.bean;
+
+public class StudentScoreDto {
+
+	private Long examId;
+	private String subjectCode;
+	private String studentCode;
+	private Integer mainNumber;
+	private String subNumber;
+	
+	//人评分
+	private Double markingScore;
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public String getSubjectCode() {
+		return subjectCode;
+	}
+
+	public void setSubjectCode(String subjectCode) {
+		this.subjectCode = subjectCode;
+	}
+
+	public String getStudentCode() {
+		return studentCode;
+	}
+
+	public void setStudentCode(String studentCode) {
+		this.studentCode = studentCode;
+	}
+
+	public Integer getMainNumber() {
+		return mainNumber;
+	}
+
+	public void setMainNumber(Integer mainNumber) {
+		this.mainNumber = mainNumber;
+	}
+
+	public String getSubNumber() {
+		return subNumber;
+	}
+
+	public void setSubNumber(String subNumber) {
+		this.subNumber = subNumber;
+	}
+
+	public Double getMarkingScore() {
+		return markingScore;
+	}
+
+	public void setMarkingScore(Double markingScore) {
+		this.markingScore = markingScore;
+	}
+
+
+
+}

+ 45 - 0
src/main/java/cn/com/qmth/am/bean/StudentScoreImageDto.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.am.bean;
+
+public class StudentScoreImageDto {
+
+    private Long studentScoreId;
+
+    private byte[] image;
+
+    private String suff;
+
+    private Integer retry = 0;
+
+    public Long getStudentScoreId() {
+        return studentScoreId;
+    }
+
+    public void setStudentScoreId(Long studentScoreId) {
+        this.studentScoreId = studentScoreId;
+    }
+
+    public byte[] getImage() {
+        return image;
+    }
+
+    public void setImage(byte[] image) {
+        this.image = image;
+    }
+
+    public Integer getRetry() {
+        return retry;
+    }
+
+    public void setRetry(Integer retry) {
+        this.retry = retry;
+    }
+
+    public String getSuff() {
+        return suff;
+    }
+
+    public void setSuff(String suff) {
+        this.suff = suff;
+    }
+
+}

+ 25 - 0
src/main/java/cn/com/qmth/am/bean/StudentScoreInfo.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean;
+
+public class StudentScoreInfo {
+
+    private Long questionId;
+
+    private String studentCode;
+
+    public Long getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Long questionId) {
+        this.questionId = questionId;
+    }
+
+    public String getStudentCode() {
+        return studentCode;
+    }
+
+    public void setStudentCode(String studentCode) {
+        this.studentCode = studentCode;
+    }
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatContent.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.am.bean.ds;
+
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+
+public class ChatContent {
+
+    private ChatRole role;
+
+    private String content;
+
+    public ChatRole getRole() {
+        return role;
+    }
+
+    public void setRole(ChatRole role) {
+        this.role = role;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+}

+ 20 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatReq.java

@@ -0,0 +1,20 @@
+package cn.com.qmth.am.bean.ds;
+
+public class ChatReq {
+
+    private String model;
+
+    public ChatReq(String modelType) {
+        super();
+        this.model = modelType;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+}

+ 17 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatResult.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.List;
+
+public class ChatResult {
+
+    private List<DsChoice> choices;
+
+    public List<DsChoice> getChoices() {
+        return choices;
+    }
+
+    public void setChoices(List<DsChoice> choices) {
+        this.choices = choices;
+    }
+
+}

+ 25 - 0
src/main/java/cn/com/qmth/am/bean/ds/DsChoice.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean.ds;
+
+public class DsChoice {
+
+    private ChatContent message;
+
+    private String finish_reason;
+
+    public ChatContent getMessage() {
+        return message;
+    }
+
+    public void setMessage(ChatContent message) {
+        this.message = message;
+    }
+
+    public String getFinish_reason() {
+        return finish_reason;
+    }
+
+    public void setFinish_reason(String finish_reason) {
+        this.finish_reason = finish_reason;
+    }
+
+}

+ 26 - 0
src/main/java/cn/com/qmth/am/bean/ds/ImageContent.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.am.bean.ds;
+
+public class ImageContent extends OcrContent {
+
+    private String type = "image_url";
+
+    private ImageUrl image_url;
+
+    public ImageContent(String image_url) {
+        super();
+        this.image_url = new ImageUrl(image_url);
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public ImageUrl getImage_url() {
+        return image_url;
+    }
+
+    public void setImage_url(ImageUrl image_url) {
+        this.image_url = image_url;
+    }
+
+}

+ 20 - 0
src/main/java/cn/com/qmth/am/bean/ds/ImageUrl.java

@@ -0,0 +1,20 @@
+package cn.com.qmth.am.bean.ds;
+
+public class ImageUrl {
+
+    private String url;
+
+    public ImageUrl(String url) {
+        super();
+        this.url = url;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+}

+ 33 - 0
src/main/java/cn/com/qmth/am/bean/ds/MarkingReq.java

@@ -0,0 +1,33 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+
+public class MarkingReq extends ChatReq {
+
+    private List<ChatContent> messages;
+
+    public MarkingReq(String modelType) {
+        super(modelType);
+    }
+
+    public List<ChatContent> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<ChatContent> messages) {
+        this.messages = messages;
+    }
+
+    public void addMsg(ChatRole role, String msg) {
+        if (messages == null) {
+            messages = new ArrayList<>();
+        }
+        ChatContent cc = new ChatContent();
+        cc.setRole(role);
+        cc.setContent(msg);
+        messages.add(cc);
+    }
+}

+ 4 - 0
src/main/java/cn/com/qmth/am/bean/ds/OcrContent.java

@@ -0,0 +1,4 @@
+package cn.com.qmth.am.bean.ds;
+
+public class OcrContent {
+}

+ 30 - 0
src/main/java/cn/com/qmth/am/bean/ds/OcrMessage.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+
+public class OcrMessage {
+
+    private ChatRole role;
+
+    private List<OcrContent> content;
+
+    public OcrMessage(String base64) {
+        super();
+        this.role = ChatRole.user;
+        content = new ArrayList<>();
+        content.add(new ImageContent(base64));
+        content.add(new TextContent());
+    }
+
+    public ChatRole getRole() {
+        return role;
+    }
+
+    public List<OcrContent> getContent() {
+        return content;
+    }
+
+}

+ 28 - 0
src/main/java/cn/com/qmth/am/bean/ds/OcrReq.java

@@ -0,0 +1,28 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class OcrReq extends ChatReq {
+
+    private List<OcrMessage> messages;
+
+    public OcrReq(String modelType) {
+        super(modelType);
+    }
+
+    public List<OcrMessage> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<OcrMessage> messages) {
+        this.messages = messages;
+    }
+
+    public void addMsg(OcrMessage cc) {
+        if (messages == null) {
+            messages = new ArrayList<>();
+        }
+        messages.add(cc);
+    }
+}

+ 17 - 0
src/main/java/cn/com/qmth/am/bean/ds/TextContent.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.am.bean.ds;
+
+public class TextContent extends OcrContent {
+
+    private String type = "text";
+
+    private String text = "只输出图片中的内容";
+
+    public String getType() {
+        return type;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/am/config/FillMetaObjectHandler.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.am.config;
+
+import java.util.Date;
+
+import org.apache.ibatis.reflection.MetaObject;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+
+public class FillMetaObjectHandler implements MetaObjectHandler {
+
+	@Override
+	public void insertFill(MetaObject metaObject) {
+		this.setFieldValByName("createTime", new Date(), metaObject);
+		this.setFieldValByName("updateTime", new Date(), metaObject);
+	}
+
+	@Override
+	public void updateFill(MetaObject metaObject) {
+		this.setFieldValByName("updateTime", new Date(), metaObject);
+	}
+
+}

+ 60 - 0
src/main/java/cn/com/qmth/am/config/InitData.java

@@ -0,0 +1,60 @@
+package cn.com.qmth.am.config;
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import com.qmth.boot.core.solar.model.OrgInfo;
+import com.qmth.boot.core.solar.service.SolarService;
+
+import cn.com.qmth.am.service.StudentScoreService;
+
+@Component
+public class InitData implements CommandLineRunner {
+
+    private static final Logger log = LoggerFactory.getLogger(InitData.class);
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private SolarService solarService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    // @Autowired
+    // private OcrService ocrService;
+
+    @Override
+    public void run(String... args) throws Exception {
+        List<OrgInfo> orgs = null;
+        try {
+            orgs = solarService.getOrgList();
+        } catch (Exception e) {
+            log.error("激活授权失败");
+            System.exit(1);
+        }
+        if (CollectionUtils.isEmpty(orgs)) {
+            log.error("授权信息有误");
+            System.exit(1);
+        }
+        File dataDir = new File(sysProperty.getDataDir());
+        if (!dataDir.exists()) {
+            dataDir.mkdir();
+        }
+        resetTaskStatus();
+        // ocrService.ocr();
+    }
+
+    private void resetTaskStatus() {
+        studentScoreService.resetStatus();
+    }
+
+}

+ 15 - 0
src/main/java/cn/com/qmth/am/config/MyBatisPlusConfig.java

@@ -0,0 +1,15 @@
+package cn.com.qmth.am.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+
+    @Bean
+    public FillMetaObjectHandler metaObjectHandler() {
+        return new FillMetaObjectHandler();
+    }
+
+}

+ 154 - 0
src/main/java/cn/com/qmth/am/config/SysProperty.java

@@ -0,0 +1,154 @@
+package cn.com.qmth.am.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.am.enums.DataType;
+
+@Component
+public class SysProperty {
+
+    @Value("${com.qmth.solar.app-version}")
+    private String version;
+
+    @Value("${am.data-dir}")
+    private String dataDir;
+
+    @Value("${am.ocr-task.enable}")
+    private Boolean ocrTaskEnable;
+
+    @Value("${am.marking-task.enable}")
+    private Boolean markingTaskEnable;
+
+    @Value("${am.image-server}")
+    private String imageServer;
+
+    @Value("${am.data-type}")
+    private DataType dataType;
+
+    @Value("${am.marking-thread-count:2}")
+    private Integer threadCount;
+
+    @Value("${am.marking-marking-model:none}")
+    private String markingModel;
+
+    @Value("${am.marking-marking-key:none}")
+    private String markingKey;
+
+    @Value("${am.marking-marking-server:none}")
+    private String markingServer;
+
+    @Value("${am.marking-ocr-model:none}")
+    private String ocrModel;
+
+    @Value("${am.marking-ocr-key:none}")
+    private String ocrKey;
+
+    @Value("${am.marking-ocr-server:none}")
+    private String ocrServer;
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getImageServer() {
+        return imageServer;
+    }
+
+    public void setImageServer(String imageServer) {
+        this.imageServer = imageServer;
+    }
+
+    public DataType getDataType() {
+        return dataType;
+    }
+
+    public void setDataType(DataType dataType) {
+        this.dataType = dataType;
+    }
+
+    public String getDataDir() {
+        return dataDir;
+    }
+
+    public void setDataDir(String dataDir) {
+        this.dataDir = dataDir;
+    }
+
+    public Boolean getOcrTaskEnable() {
+        return ocrTaskEnable;
+    }
+
+    public void setOcrTaskEnable(Boolean ocrTaskEnable) {
+        this.ocrTaskEnable = ocrTaskEnable;
+    }
+
+    public Boolean getMarkingTaskEnable() {
+        return markingTaskEnable;
+    }
+
+    public void setMarkingTaskEnable(Boolean markingTaskEnable) {
+        this.markingTaskEnable = markingTaskEnable;
+    }
+
+    public String getMarkingModel() {
+        return markingModel;
+    }
+
+    public void setMarkingModel(String markingModel) {
+        this.markingModel = markingModel;
+    }
+
+    public String getMarkingKey() {
+        return markingKey;
+    }
+
+    public void setMarkingKey(String markingKey) {
+        this.markingKey = markingKey;
+    }
+
+    public String getMarkingServer() {
+        return markingServer;
+    }
+
+    public void setMarkingServer(String markingServer) {
+        this.markingServer = markingServer;
+    }
+
+    public String getOcrModel() {
+        return ocrModel;
+    }
+
+    public void setOcrModel(String ocrModel) {
+        this.ocrModel = ocrModel;
+    }
+
+    public String getOcrKey() {
+        return ocrKey;
+    }
+
+    public void setOcrKey(String ocrKey) {
+        this.ocrKey = ocrKey;
+    }
+
+    public String getOcrServer() {
+        return ocrServer;
+    }
+
+    public void setOcrServer(String ocrServer) {
+        this.ocrServer = ocrServer;
+    }
+
+    public Integer getThreadCount() {
+        return threadCount;
+    }
+
+    public void setThreadCount(Integer threadCount) {
+        this.threadCount = threadCount;
+    }
+
+}

+ 48 - 0
src/main/java/cn/com/qmth/am/consumer/MarkingConsumer.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.am.consumer;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.service.StudentScoreService;
+
+@Scope("prototype")
+@Service
+public class MarkingConsumer implements Runnable {
+
+    private CountDownLatch endGate;
+
+    private StudentScoreEntity score;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Override
+    public void run() {
+        try {
+            studentScoreService.aiMarking(score);
+        } finally {
+            endGate.countDown();
+        }
+    }
+
+    public CountDownLatch getEndGate() {
+        return endGate;
+    }
+
+    public void setEndGate(CountDownLatch endGate) {
+        this.endGate = endGate;
+    }
+
+    public StudentScoreEntity getScore() {
+        return score;
+    }
+
+    public void setScore(StudentScoreEntity score) {
+        this.score = score;
+    }
+
+}

+ 60 - 0
src/main/java/cn/com/qmth/am/consumer/OcrConsumer.java

@@ -0,0 +1,60 @@
+package cn.com.qmth.am.consumer;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.service.StudentService;
+
+@Scope("prototype")
+@Service
+public class OcrConsumer implements Runnable {
+
+    private CountDownLatch endGate;
+
+    private StudentScoreEntity score;
+
+    private Map<Long, QuestionEntity> quetions;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Override
+    public void run() {
+        try {
+            studentService.buildImage(score, quetions);
+        } finally {
+            endGate.countDown();
+        }
+    }
+
+    public CountDownLatch getEndGate() {
+        return endGate;
+    }
+
+    public void setEndGate(CountDownLatch endGate) {
+        this.endGate = endGate;
+    }
+
+    public StudentScoreEntity getScore() {
+        return score;
+    }
+
+    public void setScore(StudentScoreEntity score) {
+        this.score = score;
+    }
+
+    public Map<Long, QuestionEntity> getQuetions() {
+        return quetions;
+    }
+
+    public void setQuetions(Map<Long, QuestionEntity> quetions) {
+        this.quetions = quetions;
+    }
+
+}

+ 387 - 0
src/main/java/cn/com/qmth/am/controller/AdminController.java

@@ -0,0 +1,387 @@
+package cn.com.qmth.am.controller;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.math.BigDecimal;
+import java.util.*;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.alibaba.fastjson.util.IOUtils;
+import com.qmth.boot.api.annotation.Aac;
+import com.qmth.boot.api.constant.ApiConstant;
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.bean.DataKey;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StudentScoreService;
+import cn.com.qmth.am.service.StudentService;
+import cn.com.qmth.am.utils.Calculator;
+import io.swagger.annotations.ApiOperation;
+
+@RestController
+@RequestMapping(ApiConstant.DEFAULT_URI_PREFIX + "/admin")
+@Aac(strict = false, auth = false)
+public class AdminController {
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @ApiOperation(value = "分析数据")
+    @RequestMapping(value = "fenxi", method = RequestMethod.GET)
+    public void fenxi(HttpServletResponse response, @RequestParam Long examId,
+            @RequestParam(required = false) Boolean exZero, @RequestParam(required = false) Integer count,
+            @RequestParam(required = false) Integer score) {
+        StringBuilder sb = new StringBuilder();
+        List<QuestionEntity> qs = questionService.findByExamId(examId);
+        if (CollectionUtils.isEmpty(qs)) {
+            sb.append("试题数:0");
+            returnJson(sb.toString(), response);
+            return;
+        }
+        sb.append("试题数:" + qs.size() + "\r\n");
+        for (QuestionEntity q : qs) {
+            List<StudentScoreEntity> scores = studentScoreService.findBy(examId, q.getSubjectCode(), q.getMainNumber(),
+                    q.getSubNumber(), exZero, count, score);
+            if (CollectionUtils.isEmpty(scores)) {
+                sb.append(q.getSubjectCode() + "|" + q.getMainNumber() + "|" + q.getSubNumber() + "| 相关系数:- \r\n");
+            } else {
+                double[] a = new double[scores.size()];
+                double[] b = new double[scores.size()];
+                int i = 0;
+                for (StudentScoreEntity s : scores) {
+                    a[i] = s.getAiScore();
+                    b[i] = s.getMarkingScore();
+                    i++;
+                }
+                try {
+                    double ret = new BigDecimal(Calculator.correlation(a, b)).setScale(2, BigDecimal.ROUND_HALF_UP)
+                            .doubleValue();
+                    double avg1 = new BigDecimal(Calculator.mean(a)).setScale(2, BigDecimal.ROUND_HALF_UP)
+                            .doubleValue();
+                    double avg2 = new BigDecimal(Calculator.mean(b)).setScale(2, BigDecimal.ROUND_HALF_UP)
+                            .doubleValue();
+                    sb.append(q.getSubjectCode() + "|" + q.getMainNumber() + "|" + q.getSubNumber() + "| 题目满分:"
+                            + q.getFullScore() + "| 相关系数:" + ret + "| 人评均分:" + avg2 + "| 机评均分:" + avg1 + "| 题数:"
+                            + scores.size() + " \r\n");
+                    fill(scores, sb, q.getFullScore(), score);
+                    fillMarkingCount(scores, sb);
+                    fillAiCount(scores, sb);
+                } catch (Exception e) {
+                    sb.append(
+                            q.getSubjectCode() + "|" + q.getMainNumber() + "|" + q.getSubNumber() + "| 相关系数出错:- \r\n");
+                }
+            }
+        }
+        returnJson(sb.toString(), response);
+    }
+
+    private void fill(List<StudentScoreEntity> scores, StringBuilder sb, Double questionScore, Integer score) {
+        int total = scores.size();
+        int st = 0;
+        Map<DataKey, Integer> ret = new HashMap<>();
+        for (StudentScoreEntity s : scores) {
+            DataKey k = getKey(s.getAiScore() - s.getMarkingScore());
+            Integer tem = ret.get(k);
+            if (tem == null) {
+                tem = 0;
+            }
+            ret.put(k, tem + 1);
+        }
+        List<DataKey> ks = new ArrayList<>(ret.keySet());
+        ks.sort(new Comparator<DataKey>() {
+
+            @Override
+            public int compare(DataKey o1, DataKey o2) {
+                int c1 = o1.getIndex();
+                int c2 = o2.getIndex();
+                if (c1 < c2) {
+                    return -1;
+                } else if (c1 > c2) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        });
+        double roundedValue = Math.round(questionScore * 0.3);
+        if (score != null) {
+            roundedValue = score;
+        }
+        sb.append("      差值分档: \r\n");
+        for (DataKey k : ks) {
+            Integer c = ret.get(k);
+            if (k.getIndex() < roundedValue) {
+                st = st + c;
+            }
+            sb.append("        " + k.getKey() + "  " + c + "  " + Calculator.percentage(c, total, 2) + " \r\n");
+        }
+        sb.append("        " + roundedValue + "分差值:" + st + "  " + Calculator.percentage(st, total, 2) + " \r\n");
+    }
+
+    private void fillMarkingCount(List<StudentScoreEntity> scores, StringBuilder sb) {
+        int total = scores.size();
+        Map<DataKey, Integer> ret = new HashMap<>();
+        for (StudentScoreEntity s : scores) {
+            DataKey k = getKey(s.getMarkingScore());
+            Integer tem = ret.get(k);
+            if (tem == null) {
+                tem = 0;
+            }
+            ret.put(k, tem + 1);
+        }
+        List<DataKey> ks = new ArrayList<>(ret.keySet());
+        ks.sort(new Comparator<DataKey>() {
+
+            @Override
+            public int compare(DataKey o1, DataKey o2) {
+                int c1 = o1.getIndex();
+                int c2 = o2.getIndex();
+                if (c1 < c2) {
+                    return -1;
+                } else if (c1 > c2) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        });
+        sb.append("      人评分档: \r\n");
+        for (DataKey k : ks) {
+            Integer c = ret.get(k);
+            sb.append("            " + k.getKey() + "  " + c + "  " + Calculator.percentage(c, total, 2) + " \r\n");
+        }
+    }
+
+    private void fillAiCount(List<StudentScoreEntity> scores, StringBuilder sb) {
+        int total = scores.size();
+        Map<DataKey, Integer> ret = new HashMap<>();
+        for (StudentScoreEntity s : scores) {
+            DataKey k = getKey(s.getAiScore());
+            Integer tem = ret.get(k);
+            if (tem == null) {
+                tem = 0;
+            }
+            ret.put(k, tem + 1);
+        }
+        List<DataKey> ks = new ArrayList<>(ret.keySet());
+        ks.sort(new Comparator<DataKey>() {
+
+            @Override
+            public int compare(DataKey o1, DataKey o2) {
+                int c1 = o1.getIndex();
+                int c2 = o2.getIndex();
+                if (c1 < c2) {
+                    return -1;
+                } else if (c1 > c2) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        });
+        sb.append("      机评分档: \r\n");
+        for (DataKey k : ks) {
+            Integer c = ret.get(k);
+            sb.append("            " + k.getKey() + "  " + c + "  " + Calculator.percentage(c, total, 2) + " \r\n");
+        }
+    }
+
+    private static DataKey getKey(double s) {
+        if (s < 0) {
+            s = 0 - s;
+        }
+        if (s == 0) {
+            DataKey r = new DataKey();
+            r.setKey("[0,0]");
+            r.setIndex(-1);
+            return r;
+        }
+        int i = 0;
+        for (;;) {
+            if (i < s && s <= i + 1) {
+                DataKey r = new DataKey();
+                r.setKey("(" + i + "," + (i + 1) + "]");
+                r.setIndex(i);
+                return r;
+            }
+            i++;
+        }
+    }
+
+    @ApiOperation(value = "进度详情")
+    @RequestMapping(value = "info", method = RequestMethod.GET)
+    public void info(HttpServletResponse response, @RequestParam Long examId) {
+        StringBuilder sb = new StringBuilder();
+        List<QuestionEntity> qs = questionService.findByExamId(examId);
+        int qstotal = 0;
+        int qsCourse = 0;
+        if (CollectionUtils.isNotEmpty(qs)) {
+            Set<String> cset = new HashSet<>();
+            qstotal = qs.size();
+            for (QuestionEntity q : qs) {
+                cset.add(q.getSubjectCode());
+            }
+            qsCourse = cset.size();
+        }
+        sb.append("ocr任务是否开启:" + (sysProperty.getOcrTaskEnable() ? "是" : "否") + "\r\n");
+        sb.append("评分任务是否开启:" + (sysProperty.getMarkingTaskEnable() ? "是" : "否") + "\r\n");
+        sb.append("试卷科目总数:" + qsCourse + "\r\n");
+        sb.append("试卷小题总数:" + qstotal + "\r\n");
+        // int total = studentService.countBy(examId);
+        // if (total == 0) {
+        // sb.append("考生总数:0");
+        // returnJson(sb.toString(), response);
+        // return;
+        // }
+        // sb.append("考生总数:" + total + "\r\n");
+        int qtotal = studentScoreService.countBy(examId, null);
+        if (qtotal == 0) {
+            sb.append("试题总数:0");
+            returnJson(sb.toString(), response);
+            return;
+        }
+        sb.append("考生试题总数:" + qtotal + "\r\n");
+
+        int qocrsuc = studentScoreService.countOcrBy(examId, DataStatus.SUCCESS);
+        sb.append("考生试题OCR成功总数:" + qocrsuc + "\r\n");
+        int qocrfailed = studentScoreService.countOcrBy(examId, DataStatus.FAILED);
+        sb.append("考生试题OCR失败总数:" + qocrfailed + "\r\n");
+
+        int qsuc = studentScoreService.countBy(examId, DataStatus.SUCCESS);
+        sb.append("考生试题评分成功总数:" + qsuc + "\r\n");
+        int qfailed = studentScoreService.countBy(examId, DataStatus.FAILED);
+        sb.append("考生试题评分失败总数:" + qfailed + "\r\n");
+        returnJson(sb.toString(), response);
+    }
+
+    @ApiOperation(value = "上传导入文件")
+    @RequestMapping(value = "upload", method = RequestMethod.POST)
+    public String upload(@RequestParam MultipartFile file) {
+        if (ImportFileName.getByName(file.getOriginalFilename()) == null) {
+            return "上传失败,文件名错误";
+        }
+        File old = new File(sysProperty.getDataDir() + "/" + file.getOriginalFilename());
+        if (old.exists()) {
+            return "上传失败,有正在处理的文件";
+        }
+        InputStream in = null;
+        try {
+            in = file.getInputStream();
+            FileUtils.copyInputStreamToFile(file.getInputStream(), old);
+        } catch (IOException e) {
+            return "上传失败," + e.getMessage();
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return "上传成功";
+    }
+
+    @ApiOperation(value = "重置数据")
+    @RequestMapping(value = "reset", method = RequestMethod.GET)
+    public String reset(@RequestParam Long examId, @RequestParam(required = false) String subjectCode) {
+        boolean lock1 = concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().tryLock();
+        if (!lock1) {
+            return "重置失败,有任务正在处理,请稍后 再试";
+        }
+        boolean lock2 = concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().tryLock();
+        if (!lock2) {
+            return "重置失败,有任务正在处理,请稍后 再试";
+        }
+        try {
+            studentService.reset(examId, subjectCode);
+            return "重置成功";
+        } finally {
+            if (lock2) {
+                concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().unlock();
+            }
+            if (lock1) {
+                concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    @ApiOperation(value = "删除数据")
+    @RequestMapping(value = "clear", method = RequestMethod.GET)
+    public String clear(@RequestParam Long examId, @RequestParam(required = false) String subjectCode) {
+        boolean lock1 = concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().tryLock();
+        if (!lock1) {
+            return "删除失败,有任务正在处理,请稍后 再试";
+        }
+        boolean lock2 = concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().tryLock();
+        if (!lock2) {
+            return "删除失败,有任务正在处理,请稍后 再试";
+        }
+        try {
+            studentService.clear(examId, subjectCode);
+            return "删除成功";
+        } finally {
+            if (lock2) {
+                concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().unlock();
+            }
+            if (lock1) {
+                concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    @ApiOperation(value = "机评开关")
+    @RequestMapping(value = "marking/status", method = RequestMethod.GET)
+    public String markingStatus(@RequestParam Boolean enable) {
+        sysProperty.setMarkingTaskEnable(enable);
+        return "设置成功:" + enable;
+    }
+
+    @ApiOperation(value = "ocr开关")
+    @RequestMapping(value = "ocr/status", method = RequestMethod.GET)
+    public String ocrStatus(@RequestParam Boolean enable) {
+        sysProperty.setOcrTaskEnable(enable);
+        return "设置成功:" + enable;
+    }
+
+    private void returnJson(String body, HttpServletResponse response) {
+        response.setContentType("application/json;charset=utf-8");
+        PrintWriter writer = null;
+        try {
+            writer = response.getWriter();
+            writer.write(body);
+        } catch (IOException e) {
+        } finally {
+            IOUtils.close(writer);
+        }
+    }
+}

+ 9 - 0
src/main/java/cn/com/qmth/am/dao/QuestionDao.java

@@ -0,0 +1,9 @@
+package cn.com.qmth.am.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import cn.com.qmth.am.entity.QuestionEntity;
+
+public interface QuestionDao extends BaseMapper<QuestionEntity> {
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/am/dao/StudentScoreDao.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.am.dao;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import cn.com.qmth.am.bean.StudentScoreInfo;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+public interface StudentScoreDao extends BaseMapper<StudentScoreEntity> {
+
+    List<StudentScoreInfo> getAllList();
+
+}

+ 125 - 0
src/main/java/cn/com/qmth/am/entity/QuestionEntity.java

@@ -0,0 +1,125 @@
+package cn.com.qmth.am.entity;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.qmth.boot.core.ai.model.llm.StandardAnswer;
+
+import cn.com.qmth.am.bean.ImageSlice;
+import cn.com.qmth.am.entity.base.IdEntity;
+import cn.com.qmth.am.handle.ImageSliceListTypeHandler;
+import cn.com.qmth.am.handle.StandardAnswerListTypeHandler;
+
+@TableName(value = "am_question", autoResultMap = true)
+public class QuestionEntity extends IdEntity {
+
+    private static final long serialVersionUID = -6261302618070108336L;
+
+    private Long examId;
+
+    private String subjectCode;
+
+    private String subjectName;
+
+    private Integer mainNumber;
+
+    private String subNumber;
+
+    // 满分
+    private Double fullScore;
+
+    // 试题名称
+    private String title;
+
+    // 试题内容
+    private String content;
+
+    // 试题答案
+    @TableField(value = "answer", typeHandler = StandardAnswerListTypeHandler.class)
+    private List<StandardAnswer> answer;
+
+    @TableField(value = "image_slice", typeHandler = ImageSliceListTypeHandler.class)
+    private List<ImageSlice> imageSlice;
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public String getSubjectCode() {
+        return subjectCode;
+    }
+
+    public void setSubjectCode(String subjectCode) {
+        this.subjectCode = subjectCode;
+    }
+
+    public Integer getMainNumber() {
+        return mainNumber;
+    }
+
+    public void setMainNumber(Integer mainNumber) {
+        this.mainNumber = mainNumber;
+    }
+
+    public String getSubNumber() {
+        return subNumber;
+    }
+
+    public void setSubNumber(String subNumber) {
+        this.subNumber = subNumber;
+    }
+
+    public Double getFullScore() {
+        return fullScore;
+    }
+
+    public void setFullScore(Double fullScore) {
+        this.fullScore = fullScore;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public List<StandardAnswer> getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(List<StandardAnswer> answer) {
+        this.answer = answer;
+    }
+
+    public List<ImageSlice> getImageSlice() {
+        return imageSlice;
+    }
+
+    public void setImageSlice(List<ImageSlice> imageSlice) {
+        this.imageSlice = imageSlice;
+    }
+
+    public String getSubjectName() {
+        return subjectName;
+    }
+
+    public void setSubjectName(String subjectName) {
+        this.subjectName = subjectName;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+}

+ 178 - 0
src/main/java/cn/com/qmth/am/entity/StudentScoreEntity.java

@@ -0,0 +1,178 @@
+package cn.com.qmth.am.entity;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+
+import cn.com.qmth.am.entity.base.IdEntity;
+import cn.com.qmth.am.enums.DataStatus;
+
+@TableName(value = "am_student_score", autoResultMap = true)
+public class StudentScoreEntity extends IdEntity {
+
+    private static final long serialVersionUID = -6261302618070108336L;
+
+    private Long questionId;
+
+    private Long examId;
+
+    private String subjectCode;
+
+    private String studentCode;
+
+    private Integer mainNumber;
+
+    private String subNumber;
+
+    // 机评分
+    private Double aiScore;
+
+    // 人评分
+    private Double markingScore;
+
+    // 机评得分率
+    private Double scoreRatio;
+
+    // ocr状态
+    private DataStatus answerStatus;
+
+    // 评分状态
+    private DataStatus scoreStatus;
+
+    // ocr结果
+    private String answer;
+
+    // 错误信息
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private String errMsg;
+
+    // 机评返回null
+    private Boolean scoreNone;
+
+    @TableField(updateStrategy = FieldStrategy.IGNORED, typeHandler = JacksonTypeHandler.class)
+    private List<Double> stepScore;
+
+    public Integer getMainNumber() {
+        return mainNumber;
+    }
+
+    public void setMainNumber(Integer mainNumber) {
+        this.mainNumber = mainNumber;
+    }
+
+    public String getSubNumber() {
+        return subNumber;
+    }
+
+    public void setSubNumber(String subNumber) {
+        this.subNumber = subNumber;
+    }
+
+    public Double getAiScore() {
+        return aiScore;
+    }
+
+    public void setAiScore(Double aiScore) {
+        this.aiScore = aiScore;
+    }
+
+    public Double getMarkingScore() {
+        return markingScore;
+    }
+
+    public void setMarkingScore(Double markingScore) {
+        this.markingScore = markingScore;
+    }
+
+    public Double getScoreRatio() {
+        return scoreRatio;
+    }
+
+    public void setScoreRatio(Double scoreRatio) {
+        this.scoreRatio = scoreRatio;
+    }
+
+    public String getErrMsg() {
+        return errMsg;
+    }
+
+    public void setErrMsg(String errMsg) {
+        this.errMsg = errMsg;
+    }
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public String getSubjectCode() {
+        return subjectCode;
+    }
+
+    public void setSubjectCode(String subjectCode) {
+        this.subjectCode = subjectCode;
+    }
+
+    public String getStudentCode() {
+        return studentCode;
+    }
+
+    public void setStudentCode(String studentCode) {
+        this.studentCode = studentCode;
+    }
+
+    public DataStatus getAnswerStatus() {
+        return answerStatus;
+    }
+
+    public void setAnswerStatus(DataStatus answerStatus) {
+        this.answerStatus = answerStatus;
+    }
+
+    public DataStatus getScoreStatus() {
+        return scoreStatus;
+    }
+
+    public void setScoreStatus(DataStatus scoreStatus) {
+        this.scoreStatus = scoreStatus;
+    }
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public void setAnswer(String answer) {
+        this.answer = answer;
+    }
+
+    public Long getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Long questionId) {
+        this.questionId = questionId;
+    }
+
+    public Boolean getScoreNone() {
+        return scoreNone;
+    }
+
+    public void setScoreNone(Boolean scoreNone) {
+        this.scoreNone = scoreNone;
+    }
+
+    public List<Double> getStepScore() {
+        return stepScore;
+    }
+
+    public void setStepScore(List<Double> stepScore) {
+        this.stepScore = stepScore;
+    }
+
+}

+ 45 - 0
src/main/java/cn/com/qmth/am/entity/base/BaseEntity.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.am.entity.base;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+
+/**
+ * 实体类基类
+ */
+public abstract class BaseEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+	 * 更新时间
+	 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+	private Date updateTime;
+
+	/**
+	 * 创建时间
+	 */
+    @TableField(fill = FieldFill.INSERT)
+	private Date createTime;
+
+	public Date getUpdateTime() {
+		return updateTime;
+	}
+
+	public void setUpdateTime(Date updateTime) {
+		this.updateTime = updateTime;
+	}
+
+	public Date getCreateTime() {
+		return createTime;
+	}
+
+	public void setCreateTime(Date createTime) {
+		this.createTime = createTime;
+	}
+
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/am/entity/base/IdEntity.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.am.entity.base;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+/**
+ * 实体类基类
+ */
+public abstract class IdEntity extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+
+    @TableId(type = IdType.AUTO)
+	private Long id;
+
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+
+}

+ 34 - 0
src/main/java/cn/com/qmth/am/enums/DataStatus.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.am.enums;
+
+/**数据处理状态
+ *
+ */
+public enum DataStatus {
+
+	WAITING("待处理"),
+	PROCESSING("处理中"),
+	SUCCESS("成功"),
+	FAILED("失败"),
+	;
+
+	private DataStatus(String name){
+		this.name = name;
+	}
+
+	private String name;
+
+	public String getName() {
+		return name;
+	}
+
+	
+	public static DataStatus getByName(String name) {
+    	for(DataStatus r:DataStatus.values()) {
+    		if(r.getName().equals(name)) {
+    			return r;
+    		}
+    	}
+    	return null;
+    }
+
+}

+ 32 - 0
src/main/java/cn/com/qmth/am/enums/DataType.java

@@ -0,0 +1,32 @@
+package cn.com.qmth.am.enums;
+
+/**数据源类型
+ *
+ */
+public enum DataType {
+
+	MARKING_CLOUD("云阅卷"),
+	TEACH_CLOUD("知学知考"),
+	;
+
+	private DataType(String name){
+		this.name = name;
+	}
+
+	private String name;
+
+	public String getName() {
+		return name;
+	}
+
+	
+	public static DataType getByName(String name) {
+    	for(DataType r:DataType.values()) {
+    		if(r.getName().equals(name)) {
+    			return r;
+    		}
+    	}
+    	return null;
+    }
+
+}

+ 30 - 0
src/main/java/cn/com/qmth/am/enums/ImportFileName.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.am.enums;
+
+public enum ImportFileName {
+
+	QUESTION_IMPORT("question-import.xlsx"),
+	STUDENT_IMPORT("student-import.xlsx"),
+	SCORE_IMPORT("score-import.xlsx"),
+	;
+
+	private ImportFileName(String name){
+		this.name = name;
+	}
+
+	private String name;
+
+	public String getName() {
+		return name;
+	}
+
+	
+	public static ImportFileName getByName(String name) {
+    	for(ImportFileName r:ImportFileName.values()) {
+    		if(r.getName().equals(name)) {
+    			return r;
+    		}
+    	}
+    	return null;
+    }
+
+}

+ 21 - 0
src/main/java/cn/com/qmth/am/enums/LockType.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.am.enums;
+
+public enum LockType {
+	STUDENT_IMPORT("student_import"),
+	QUESTION_IMPORT("question_import"),
+	MARKING_SCORE_IMPORT("marking_score_import"),
+	OCR("ocr"),
+    AI_MARKING("ai_marking"),
+    ;
+
+    private String name;
+
+    private LockType(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+}

+ 19 - 0
src/main/java/cn/com/qmth/am/enums/ModelTypeBak.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.am.enums;
+
+public enum ModelTypeBak {
+
+    solar("solar"), qwen2_5("Qwen2.5-32B-Instruct-GPTQ-Int4"), ds_r1("deepseek-r1-distill-qwen-32b-awq"), glm4_9(
+            "glm-4-9b-chat"), ds_v3(
+                    "deepseek-v3"), qwen72_ocr("qwen2.5-vl-72b-instruct"), qwen7_ocr("qwen2.5-vl-7b-instruct");
+
+    private ModelTypeBak(String code) {
+        this.code = code;
+    }
+
+    private String code;
+
+    public String getCode() {
+        return code;
+    }
+
+}

+ 16 - 0
src/main/java/cn/com/qmth/am/handle/ImageSliceListTypeHandler.java

@@ -0,0 +1,16 @@
+package cn.com.qmth.am.handle;
+
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JavaType;
+
+import cn.com.qmth.am.bean.ImageSlice;
+
+public class ImageSliceListTypeHandler extends ListTypeHandler<ImageSlice> {
+
+    @Override
+    protected JavaType specificType() {
+        return super.mapper.getTypeFactory().constructParametricType(List.class, ImageSlice.class);
+    }
+
+}

+ 66 - 0
src/main/java/cn/com/qmth/am/handle/ListTypeHandler.java

@@ -0,0 +1,66 @@
+package cn.com.qmth.am.handle;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.xiaoymin.knife4j.core.util.StrUtil;
+import com.qmth.boot.core.exception.StatusException;
+
+@MappedJdbcTypes(JdbcType.VARCHAR)
+@MappedTypes({List.class})
+public abstract class ListTypeHandler<T> extends BaseTypeHandler<List<T>> {
+	protected ObjectMapper mapper = new ObjectMapper();
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, List<T> parameter, JdbcType jdbcType) throws SQLException {
+		try {
+			String content = CollectionUtils.isEmpty(parameter) ? null : mapper.writeValueAsString(parameter);
+			ps.setString(i, content);
+		} catch (Exception e) {
+			throw new StatusException("出错",e);
+		}
+    }
+ 
+    @Override
+    public List<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        return this.getListByJsonArrayString(rs.getString(columnName));
+    }
+ 
+    @Override
+    public List<T> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        return this.getListByJsonArrayString(rs.getString(columnIndex));
+    }
+ 
+    @Override
+    public List<T> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        return this.getListByJsonArrayString(cs.getString(columnIndex));
+    }
+ 
+ 
+    private List<T> getListByJsonArrayString(String content) {
+        try {
+			return StrUtil.isBlank(content) ? null : mapper.readValue(content,specificType());
+		} catch (Exception e) {
+			throw new StatusException("出错",e);
+		}
+    }
+ 
+    /**
+     * 具体类型,由子类提供
+     *
+     * @return 具体类型
+     */
+    protected abstract JavaType specificType();
+ 
+ 
+}

+ 15 - 0
src/main/java/cn/com/qmth/am/handle/StandardAnswerListTypeHandler.java

@@ -0,0 +1,15 @@
+package cn.com.qmth.am.handle;
+
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.qmth.boot.core.ai.model.llm.StandardAnswer;
+
+public class StandardAnswerListTypeHandler extends ListTypeHandler<StandardAnswer> {
+
+    @Override
+    protected JavaType specificType() {
+        return super.mapper.getTypeFactory().constructParametricType(List.class, StandardAnswer.class);
+    }
+
+}

+ 54 - 0
src/main/java/cn/com/qmth/am/multithread/AopTargetUtils.java

@@ -0,0 +1,54 @@
+package cn.com.qmth.am.multithread;
+
+import java.lang.reflect.Field;
+
+import org.springframework.aop.framework.AdvisedSupport;
+import org.springframework.aop.framework.AopProxy;
+import org.springframework.aop.support.AopUtils;
+
+public class AopTargetUtils {
+
+    public static Object getTarget(Object obj) {
+        if (!AopUtils.isAopProxy(obj)) {
+            return obj;
+        }
+        try {
+
+            // 判断是jdk还是cglib代理
+            if (AopUtils.isJdkDynamicProxy(obj)) {
+                obj = getJdkDynamicProxyTargetObject(obj);
+            } else {
+                obj = getCglibDynamicProxyTargetObject(obj);
+            }
+
+        } catch (Exception e) {
+
+        }
+        return obj;
+
+    }
+
+    private static Object getCglibDynamicProxyTargetObject(Object obj) throws Exception {
+        Field h = obj.getClass().getDeclaredField("CGLIB$CALLBACK_0");
+        h.setAccessible(true);
+
+        Object dynamicAdvisedInterceptor = h.get(obj);
+        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
+        advised.setAccessible(true);
+        Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
+        return target;
+    }
+
+    private static Object getJdkDynamicProxyTargetObject(Object obj) throws Exception {
+
+        Field h = obj.getClass().getSuperclass().getDeclaredField("h");
+        h.setAccessible(true);
+
+        AopProxy aopProxy = (AopProxy) h.get(obj);
+        Field advised = aopProxy.getClass().getDeclaredField("advised");
+        advised.setAccessible(true);
+        Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
+        return target;
+
+    }
+}

+ 162 - 0
src/main/java/cn/com/qmth/am/multithread/Basket.java

@@ -0,0 +1,162 @@
+package cn.com.qmth.am.multithread;
+
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.qmth.boot.core.exception.StatusException;
+
+import cn.com.qmth.am.utils.Calculator;
+
+public class Basket<T> {
+
+    private String taskName;
+
+    private Integer total = 0;
+
+    private AtomicInteger process = new AtomicInteger(0);
+
+    private List<String> result = new CopyOnWriteArrayList<>();
+
+    private List<T> failed = new CopyOnWriteArrayList<>();
+
+    /**
+     * 数据阻塞队列
+     */
+    private BlockingQueue<Object> queue;
+
+    /**
+     * 多线程计数器,子线程都结束后主线程才继续执行
+     */
+    private CountDownLatch endGate;
+
+    /**
+     * 消费者数量
+     */
+    private int consumerCount;
+
+    /**
+     * 判断线程执行是否有出错,生产者、消费者出错都需要修改此值为true
+     */
+    private boolean isExcuteError = false;
+
+    public Basket(int consumerCount, String taskName) {
+        this.consumerCount = consumerCount;
+        this.taskName = taskName;
+        queue = new ArrayBlockingQueue<Object>(consumerCount * 2);
+        endGate = new CountDownLatch(consumerCount);
+    }
+
+    /**
+     * 生产数据,不采用put方法防止消费线程全部异常后生产线程阻塞
+     * 
+     * @param value
+     * @throws InterruptedException
+     */
+    protected void offer(final Object value) throws InterruptedException {
+        if (isExcuteError) {
+            throw new StatusException("线程异常");
+        } else {
+            boolean ret = queue.offer(value, 5, TimeUnit.SECONDS);
+            if (!ret) {
+                this.offer(value);
+            }
+        }
+    }
+
+    /**
+     * 消费数据,不采用take方法防止生产线程全部异常后消费线程阻塞
+     * 
+     * @return
+     * @throws InterruptedException
+     */
+    protected Object consume() throws InterruptedException {
+        if (isExcuteError) {
+            return new EndObject();
+        } else {
+            Object ob = queue.poll(5, TimeUnit.SECONDS);
+            if (ob == null) {
+                return this.consume();
+            } else {
+                return ob;
+            }
+        }
+    }
+
+    protected void endGateReset() {
+        endGate = new CountDownLatch(consumerCount);
+    }
+
+    protected void await() throws InterruptedException {
+        endGate.await();
+    }
+
+    protected void countDown() {
+        endGate.countDown();
+    }
+
+    protected boolean isExcuteError() {
+        return isExcuteError;
+    }
+
+    protected void setExcuteError(boolean isExcuteError) {
+        this.isExcuteError = isExcuteError;
+    }
+
+    protected int getConsumerCount() {
+        return consumerCount;
+    }
+
+    protected void setConsumerCount(int consumerCount) {
+        this.consumerCount = consumerCount;
+    }
+
+    public Integer getTotal() {
+        return total;
+    }
+
+    protected void setTotal(Integer total) {
+        this.total = total;
+    }
+
+    public AtomicInteger getProcess() {
+        return process;
+    }
+
+    protected void updateProcess(int add) {
+        process.addAndGet(add);
+    }
+
+    public String getProgress() {
+        if (total == 0) {
+            return "0%";
+        }
+        Double d = Calculator.divide(process.doubleValue(), total.doubleValue(), 4);
+        Double f = Calculator.multiply(d, 100);
+        return f + "%";
+    }
+
+    public List<String> getMsgs() {
+        return result;
+    }
+
+    public void addMsg(String msg) {
+        result.add(msg);
+    }
+
+    public String getTaskName() {
+        return taskName;
+    }
+
+    public void addFailDto(T t) {
+        failed.add(t);
+    }
+
+    public List<T> getFaildDto() {
+        return failed;
+    }
+}

+ 83 - 0
src/main/java/cn/com/qmth/am/multithread/Consumer.java

@@ -0,0 +1,83 @@
+package cn.com.qmth.am.multithread;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.qmth.boot.core.exception.StatusException;
+
+public abstract class Consumer<T> extends Thread {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Consumer.class);
+
+    private Basket<T> basket;
+
+    private Consumer<T> consumer;
+
+    public Consumer() {
+    }
+
+    public Consumer<T> getConsumer() {
+        return consumer;
+    }
+
+    public void setConsumer(Consumer<T> consumer) {
+        this.consumer = consumer;
+    }
+
+    @Override
+    public void run() {
+        try {
+            while (true) {
+                // 先判断是否有异常结束
+                if (basket.isExcuteError()) {
+                    break;
+                }
+                // 取消费数据
+                Object o = basket.consume();
+                // 判断消费数据是否是结束
+                if (o instanceof EndObject) {
+                    break;
+                }
+                @SuppressWarnings("unchecked")
+                T t = (T) o;
+                // 消费数据实现
+                int disposeCount = consumer.consume(t);
+                if (basket.getTotal() > 0) {
+                    basket.updateProcess(disposeCount);
+                    processInfo();
+                }
+            }
+        } catch (StatusException e) {
+            LOG.error(e.getMessage(), e);
+            addMsg(e.getMessage());
+            basket.setExcuteError(true);
+        } catch (Exception e) {
+            LOG.error(e.getMessage(), e);
+            basket.setExcuteError(true);
+        } finally {
+            basket.countDown();
+        }
+    }
+
+    protected abstract int consume(T t);
+
+    protected void setBasket(Basket<T> basket) {
+        this.basket = basket;
+    }
+
+    protected Basket<T> getBasket() {
+        return this.basket;
+    }
+
+    protected void addMsg(String msg) {
+        this.basket.addMsg(msg);
+    }
+
+    protected void addFailDto(T t) {
+        this.basket.addFailDto(t);
+    }
+
+    protected void processInfo() {
+        LOG.info(basket.getTaskName() + " 处理进度" + basket.getProgress());
+    }
+}

+ 11 - 0
src/main/java/cn/com/qmth/am/multithread/EndObject.java

@@ -0,0 +1,11 @@
+package cn.com.qmth.am.multithread;
+
+/**
+ * 消费结束标识对象
+ * 
+ * @author xiatian
+ *
+ */
+public class EndObject {
+
+}

+ 167 - 0
src/main/java/cn/com/qmth/am/multithread/Producer.java

@@ -0,0 +1,167 @@
+package cn.com.qmth.am.multithread;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.qmth.boot.core.exception.StatusException;
+
+import cn.com.qmth.am.utils.SpringContextHolder;
+
+public abstract class Producer<T, C extends Consumer<T>> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Producer.class);
+
+    private Basket<T> basket;
+
+    /**
+     * 消费线程class
+     */
+    private List<Consumer<T>> consumers;
+
+    public void startDispose(int consumerCount) {
+        startDispose(consumerCount, null, 0);
+    }
+
+    /**
+     * 处理开始方法
+     */
+    public void startDispose(int consumerCount, Map<String, Object> param) {
+        startDispose(consumerCount, param, 0);
+    }
+
+    public void startDispose(int consumerCount, int total) {
+        startDispose(consumerCount, null, total);
+    }
+
+    public void startDispose(int consumerCount, Map<String, Object> param, int total) {
+        // 启动消费者
+        startConsumer(consumerCount, total);
+        // 开始处理
+        dispose(param);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void startConsumer(int consumerCount, int total) {
+        if (consumerCount <= 0) {
+            consumerCount = 1;
+        }
+        ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
+        Class<C> clazz = (Class<C>) pt.getActualTypeArguments()[1];
+        consumers = new ArrayList<>();
+        this.basket = new Basket<T>(consumerCount, getTaskName());
+        basket.setTotal(total);
+        // 启动消费者
+        int count = basket.getConsumerCount();
+        for (int i = 0; i < count; i++) {
+            Consumer<T> co = SpringContextHolder.getBean(clazz);
+            co.setBasket(basket);
+            co.setConsumer(co);
+            co.start();
+            consumers.add((Consumer<T>) AopTargetUtils.getTarget(co));
+        }
+    }
+
+    private void dispose(Map<String, Object> param) {
+        try {
+            // 生产数据
+            int index = 0;
+            for (;;) {
+                T dto = findData(param, index);
+                if (dto == null) {
+                    // 拿不到数据,结束消费
+                    break;
+                }
+                offer(dto);
+                index++;
+            }
+            // 发送生产结束信息
+            endConsumer();
+            // 等待子线程结束
+            await();
+            // 判断子线程是否正常结束
+            if (basket.isExcuteError()) {
+                throw new StatusException("处理失败,线程异常");
+            }
+        } catch (StatusException e) {
+            LOG.error(e.getMessage(), e);
+            // 获取异常时发送异常结束信息
+            endConsumerAsError();
+            throw e;
+        } catch (Exception e) {
+            LOG.error(e.getMessage(), e);
+            // 获取异常时发送异常结束信息
+            endConsumerAsError();
+            throw new StatusException("处理失败", e);
+        }
+    }
+
+    /**
+     * 出异常后修改标识
+     * 
+     */
+    private void endConsumerAsError() {
+        basket.setExcuteError(true);
+    }
+
+    /**
+     * 正常结束消费者
+     * 
+     * @throws InterruptedException
+     */
+    private void endConsumer() throws InterruptedException {
+        int count = basket.getConsumerCount();
+        EndObject eo = new EndObject();
+        for (int i = 0; i < count; i++) {
+            basket.offer(eo);
+        }
+
+    }
+
+    /**
+     * 生产数据
+     * 
+     * @param ob
+     * @throws InterruptedException
+     */
+    private void offer(Object ob) throws InterruptedException {
+        synchronized (basket) {
+            basket.offer(ob);
+        }
+    }
+
+    /**
+     * 等待所有消费者结束
+     * 
+     * @throws InterruptedException
+     */
+    private void await() throws InterruptedException {
+        basket.await();
+    }
+
+    protected abstract T findData(Map<String, Object> param, int index);
+
+    protected abstract String getTaskName();
+
+    public List<String> getMsgs() {
+        return this.basket.getMsgs();
+    }
+
+    public Integer getTotal() {
+        return this.basket.getTotal();
+    }
+
+    public AtomicInteger getProcess() {
+        return this.basket.getProcess();
+    }
+
+    public List<T> getFaildDto() {
+        return this.basket.getFaildDto();
+    }
+
+}

+ 102 - 0
src/main/java/cn/com/qmth/am/multithread/consumer/LocalOcrConsumer.java

@@ -0,0 +1,102 @@
+package cn.com.qmth.am.multithread.consumer;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.ai.client.OcrApiClient;
+import com.qmth.boot.core.ai.model.ocr.OcrType;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
+import com.qmth.boot.core.retrofit.utils.UploadFile;
+import com.qmth.boot.core.solar.model.OrgInfo;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.bean.StudentScoreImageDto;
+import cn.com.qmth.am.multithread.Consumer;
+
+@Scope("prototype")
+@Service
+public class LocalOcrConsumer extends Consumer<OcrDto> {
+
+    private static final Logger log = LoggerFactory.getLogger(LocalOcrConsumer.class);
+
+    @Autowired
+    private OcrApiClient ocrApiClient;
+
+    @Override
+    public int consume(OcrDto ocrDto) {
+        File file = ocrDto.getFile();
+        String name = file.getName().substring(0, file.getName().lastIndexOf("."));
+        File txt = new File(file.getParentFile().getAbsolutePath() + "/" + name + ".txt");
+        if (!txt.exists()) {
+            try {
+                StudentScoreImageDto dto = new StudentScoreImageDto();
+                dto.setImage(fileToByte(file));
+                String content = ocrDispose(dto, ocrDto.getOrg());
+                FileUtils.write(txt, content, "utf-8");
+            } catch (Exception e) {
+                log.error("ocr异常", e);
+                addFailDto(ocrDto);
+            }
+        }
+        return 1;
+    }
+
+    private byte[] fileToByte(File img) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            BufferedImage bi;
+            bi = ImageIO.read(img);
+            ImageIO.write(bi, "jpg", baos);
+            byte[] bytes = baos.toByteArray();
+            return bytes;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            try {
+                baos.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+    private String ocrDispose(StudentScoreImageDto dto, OrgInfo org) {
+        SignatureInfo signature = SignatureInfo.secret(org.getAccessKey(), org.getAccessSecret());
+        try {
+            return ocrApiClient.forImage(signature, OcrType.HANDWRITING, UploadFile.build("image", "", dto.getImage()));
+        } catch (Exception e) {
+            log.error("ocr异常", e);
+            if (e instanceof RetrofitResponseError) {
+                RetrofitResponseError tem = (RetrofitResponseError) e;
+                if (tem.getCode() == 503) {
+                    if (dto.getRetry() <= 3) {
+                        try {
+                            Thread.sleep(3000);
+                        } catch (InterruptedException e1) {
+                        }
+                        dto.setRetry(dto.getRetry() + 1);
+                        return ocrDispose(dto, org);
+                    } else {
+                        throw new StatusException("重试次数过多");
+                    }
+                } else {
+                    throw e;
+                }
+            } else {
+                throw e;
+            }
+        }
+    }
+}

+ 31 - 0
src/main/java/cn/com/qmth/am/multithread/producer/LocalOcrProducer.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.am.multithread.producer;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.multithread.Producer;
+import cn.com.qmth.am.multithread.consumer.LocalOcrConsumer;
+
+@Service
+public class LocalOcrProducer extends Producer<OcrDto, LocalOcrConsumer> {
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected OcrDto findData(Map<String, Object> param, int index) {
+        List<OcrDto> ret = (List<OcrDto>) param.get("files");
+
+        if (index >= ret.size()) {
+            return null;
+        }
+        return ret.get(index);
+    }
+
+    @Override
+    protected String getTaskName() {
+        return "本地OCR处理";
+    }
+
+}

+ 15 - 0
src/main/java/cn/com/qmth/am/service/DsMarkingService.java

@@ -0,0 +1,15 @@
+package cn.com.qmth.am.service;
+
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+import com.qmth.boot.core.ai.model.llm.AutoScoreResult;
+
+/**
+ * 类注释
+ */
+public interface DsMarkingService {
+
+    AutoScoreResult autoScore(AutoScoreRequest req);
+
+    String ocr(String base64);
+
+}

+ 10 - 0
src/main/java/cn/com/qmth/am/service/OcrService.java

@@ -0,0 +1,10 @@
+package cn.com.qmth.am.service;
+
+/**
+ * 类注释
+ */
+public interface OcrService {
+
+    void ocr();
+
+}

+ 23 - 0
src/main/java/cn/com/qmth/am/service/QuestionService.java

@@ -0,0 +1,23 @@
+package cn.com.qmth.am.service;
+
+import java.io.InputStream;
+import java.util.List;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import cn.com.qmth.am.bean.ImportResult;
+import cn.com.qmth.am.entity.QuestionEntity;
+
+/**
+ * 类注释
+ */
+public interface QuestionService  extends IService<QuestionEntity> {
+
+	void importQuestion();
+
+	public ImportResult disposeFile(InputStream inputStream);
+
+	List<QuestionEntity> findByExamId(Long examId);
+
+	void removeBy(Long examId, String subjectCode);
+}

+ 51 - 0
src/main/java/cn/com/qmth/am/service/StudentScoreService.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.am.service;
+
+import java.util.List;
+import java.util.Map;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import cn.com.qmth.am.bean.AnswerImageDto;
+import cn.com.qmth.am.bean.StudentInfo;
+import cn.com.qmth.am.bean.StudentScoreImageDto;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.DataStatus;
+
+/**
+ * 类注释
+ */
+public interface StudentScoreService extends IService<StudentScoreEntity> {
+
+    void importScore();
+
+    void updateAnswerErr(Long id, String string);
+
+    void createSlice(StudentScoreEntity score, QuestionEntity q, Map<Integer, AnswerImageDto> answerImages);
+
+    // StudentScoreImageDto pollStudentScoreImage();
+
+    void ocr(StudentScoreImageDto dto);
+
+    void aiMarking(StudentScoreEntity score);
+
+    void resetStatus();
+
+    List<StudentScoreEntity> findAllToAiMarking();
+
+    int countBy(Long examId, DataStatus success);
+
+    int countOcrBy(Long examId, DataStatus success);
+
+    void removeBy(Long examId, String subjectCode);
+
+    List<StudentScoreEntity> findBy(Long examId, String subjectCode, Integer mainNumber, String subNumber,
+            Boolean exZero, Integer count, Integer score);
+
+    // List<StudentScoreEntity> findToAiMarking(Long studentId);
+
+    void addStudentScore(List<StudentInfo> students);
+
+    List<StudentScoreEntity> findAllToOcr();
+
+}

+ 32 - 0
src/main/java/cn/com/qmth/am/service/StudentService.java

@@ -0,0 +1,32 @@
+package cn.com.qmth.am.service;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import cn.com.qmth.am.bean.AnswerImageDto;
+import cn.com.qmth.am.bean.ImportResult;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+/**
+ * 类注释
+ */
+public interface StudentService {
+
+    void importStudent();
+
+    ImportResult disposeFile(InputStream inputStream);
+
+    void buildImage(StudentScoreEntity score, Map<Long, QuestionEntity> quetions);
+
+    // List<StudentScoreEntity> getOrCreateScores(StudentEntity student,
+    // Map<Long, QuestionEntity> quetions);
+
+    void createSlice(StudentScoreEntity score, Map<Long, QuestionEntity> quetions,
+            Map<Integer, AnswerImageDto> answerImages);
+
+    void reset(Long examId, String subjectCode);
+
+    void clear(Long examId, String subjectCode);
+
+}

+ 169 - 0
src/main/java/cn/com/qmth/am/service/impl/DsMarkingServiceImpl.java

@@ -0,0 +1,169 @@
+package cn.com.qmth.am.service.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+import com.qmth.boot.core.ai.model.llm.AutoScoreResult;
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+
+import cn.com.qmth.am.bean.ds.ChatReq;
+import cn.com.qmth.am.bean.ds.ChatResult;
+import cn.com.qmth.am.bean.ds.MarkingReq;
+import cn.com.qmth.am.bean.ds.OcrMessage;
+import cn.com.qmth.am.bean.ds.OcrReq;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.service.DsMarkingService;
+import cn.com.qmth.am.utils.FreeMarkerUtil;
+import cn.com.qmth.am.utils.HttpMethod;
+import cn.com.qmth.am.utils.OKHttpUtil;
+import okhttp3.Response;
+
+@Service
+public class DsMarkingServiceImpl implements DsMarkingService {
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    private static final Logger log = LoggerFactory.getLogger(DsMarkingService.class);
+
+    // @SuppressWarnings("deprecation")
+    // public static void main(String[] args) {
+    //
+    // Map<String, String> headers = new HashMap<>();
+    // headers.put("Authorization", "Bearer 7dac2f2166994b8f9c6de0a8eff2814c");
+    // Response resp = null;
+    // try {
+    // resp = OKHttpUtil.call(HttpMethod.POST,
+    // "http://39.174.90.3:31091/spiritx-api/v1/chat/completions", headers,
+    // "{\"model\":\"deepseek-r1-distill-qwen-32b-awq\",\"messages\":[{\"role\":\"user\",\"content\":\"你是谁?\"}]}");
+    // if (resp.code() != 200) {
+    // throw new RuntimeException("err :" + resp.body().string());
+    // } else {
+    // System.out.println("成功处理:" + resp.body().string());
+    // }
+    // } catch (Exception e) {
+    // throw new RuntimeException(e);
+    // } finally {
+    // IOUtils.closeQuietly(resp);
+    // }
+    // }
+    @Override
+    public String ocr(String base64) {
+        OcrReq dreq = new OcrReq(sysProperty.getOcrModel());
+        dreq.addMsg(new OcrMessage(base64));
+        String res = ocr(dreq);
+        ChatResult result = JSONObject.parseObject(res, ChatResult.class);
+        String text = result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).findFirst().orElse("");
+        return text;
+    }
+
+    @Override
+    public AutoScoreResult autoScore(AutoScoreRequest request) {
+        String question = FreeMarkerUtil.getDsMarkingReq(request);
+        MarkingReq dreq = new MarkingReq(sysProperty.getMarkingModel());
+        dreq.addMsg(ChatRole.user, question);
+        String res = marking(dreq);
+        ChatResult result = JSONObject.parseObject(res, ChatResult.class);
+        String text = result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).findFirst().orElse("");
+        try {
+            AutoScoreResult scoreResult = new AutoScoreResult();
+            String scoreStr = fomatStr(text);
+            scoreResult.setTotalScore(Double.valueOf(scoreStr));
+            return scoreResult;
+        } catch (Exception e) {
+            log.error(e.getMessage() + " | " + res);
+            return null;
+        }
+    }
+
+    private String fomatStr(String scoreStr) {
+        scoreStr = scoreStr.substring(scoreStr.lastIndexOf("\n") + 1).trim();
+        String ret = scoreStr.replaceAll(",", ",").replaceAll("。", "").replaceAll(":", ":");
+        ret = ret.substring(ret.lastIndexOf(":") + 1).trim();
+        return ret;
+    }
+
+    // private String fomatStrByRex(String scoreStr) {
+    // int tag = scoreStr.lastIndexOf("</think>");
+    // if (tag != -1) {
+    // scoreStr = scoreStr.substring(tag).trim();
+    // }
+    // String ret = scoreStr.replaceAll(",", ",").replaceAll("。",
+    // "").replaceAll("[0-9]\\.", "");
+    // Pattern pattern = Pattern.compile("(\\d{1,3}\\s*,\\s*)+\\d{1,3}");
+    // Matcher matcher = pattern.matcher(ret);
+    // if (matcher.find()) {
+    // return matcher.group();
+    // } else {
+    // throw new RuntimeException("返回格式错误");
+    // }
+    // }
+
+    public static void main(String[] args) {
+        String scoreStr = "</think>。\\n\\n\\n70,70,60\\n\\n评分结果2个3,29,110 \\n\\n考生的回答完全覆盖了所有的关键内容,逻辑清晰,术语使用准确";
+        scoreStr = scoreStr.substring(scoreStr.lastIndexOf("</think>") + 1).trim();
+        System.out.println(Runtime.getRuntime().availableProcessors());
+        String ret = scoreStr.replaceAll(",", ",").replaceAll("。", "").replaceAll(":", ":").replaceAll("[0-9]\\.", "");
+        Pattern pattern = Pattern.compile("(\\d{1,3}\\s*,\\s*)+\\d{1,3}");
+        Matcher matcher = pattern.matcher(ret);
+        if (matcher.find()) {
+            System.out.println(matcher.group());
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private String marking(ChatReq dreq) {
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + sysProperty.getMarkingKey());
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, sysProperty.getMarkingServer(), headers,
+                    JSONObject.toJSONString(dreq));
+            if (resp.code() != 200) {
+                throw new RetrofitResponseError(resp.code(), resp.body().string());
+            } else {
+                return resp.body().string();
+            }
+        } catch (Exception e) {
+            throw new RetrofitResponseError(500, e.getMessage(), e);
+        } finally {
+            IOUtils.closeQuietly(resp);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private String ocr(ChatReq dreq) {
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + sysProperty.getOcrKey());
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, sysProperty.getOcrServer(), headers, JSONObject.toJSONString(dreq));
+            if (resp.code() != 200) {
+                throw new RetrofitResponseError(resp.code(), resp.body().string());
+            } else {
+                return resp.body().string();
+            }
+        } catch (RetrofitResponseError e) {
+            throw e;
+        } catch (Exception e) {
+            throw new RetrofitResponseError(500, e.getMessage(), e);
+        } finally {
+            IOUtils.closeQuietly(resp);
+        }
+    }
+}

+ 66 - 0
src/main/java/cn/com/qmth/am/service/impl/OcrServiceImpl.java

@@ -0,0 +1,66 @@
+package cn.com.qmth.am.service.impl;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.solar.model.OrgInfo;
+import com.qmth.boot.core.solar.service.SolarService;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.multithread.producer.LocalOcrProducer;
+import cn.com.qmth.am.service.OcrService;
+
+@Service
+public class OcrServiceImpl implements OcrService {
+
+    private static final Logger log = LoggerFactory.getLogger(OcrService.class);
+
+    @Autowired
+    private SolarService solarService;
+
+    @Override
+    public void ocr() {
+        log.warn("OcrService ocr start*************");
+        File dir = new File("d:/ocr/data");
+        OrgInfo org = solarService.getOrgList().get(0);
+        List<OcrDto> files = new ArrayList<>();
+        disposeFile(files, dir, org);
+        LocalOcrProducer producer = new LocalOcrProducer();
+        Map<String, Object> param = new HashMap<>();
+        param.put("files", files);
+        producer.startDispose(4, param, files.size());
+        while (true) {
+            List<OcrDto> failed = producer.getFaildDto();
+            if (CollectionUtils.isEmpty(failed)) {
+                break;
+            }
+            param = new HashMap<>();
+            param.put("files", failed);
+            producer = new LocalOcrProducer();
+            producer.startDispose(4, param, failed.size());
+        }
+        log.warn("OcrService ocr finish*************");
+    }
+
+    private void disposeFile(List<OcrDto> files, File file, OrgInfo org) {
+        if (file.isFile() && file.getName().toLowerCase().endsWith(".jpg")) {
+            files.add(new OcrDto(file, org));
+        } else {
+            if (file.isDirectory()) {
+                for (File subFile : file.listFiles()) {
+                    disposeFile(files, subFile, org);
+                }
+            }
+        }
+    }
+
+}

+ 481 - 0
src/main/java/cn/com/qmth/am/service/impl/QuestionServiceImpl.java

@@ -0,0 +1,481 @@
+package cn.com.qmth.am.service.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.boot.core.ai.model.llm.StandardAnswer;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.tools.excel.ExcelReader;
+import com.qmth.boot.tools.excel.enums.ExcelType;
+import com.qmth.boot.tools.excel.model.DataMap;
+
+import cn.com.qmth.am.bean.ImageSlice;
+import cn.com.qmth.am.bean.ImportResult;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.dao.QuestionDao;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.service.QuestionService;
+
+@Service
+public class QuestionServiceImpl extends ServiceImpl<QuestionDao, QuestionEntity> implements QuestionService {
+
+    private static final Logger log = LoggerFactory.getLogger(QuestionService.class);
+
+    private Pattern scoreRex = Pattern.compile("\\[\\[([0-9][0-9]*(.[0-9]+){0,1})分\\]\\]");
+
+    private static final String[] EXCEL_HEADER = new String[] { "考试ID", "科目代码", "科目名称", "题目名称", "大题号", "小题号", "满分",
+            "试题内容", "试题答案", "作答坐标" };
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Override
+    public void importQuestion() {
+        File dir = new File(sysProperty.getDataDir());
+        File[] fs = dir.listFiles();
+        if (fs == null || fs.length == 0) {
+            return;
+        }
+        for (File file : fs) {
+            if (!file.isFile() || !file.getName().equals(ImportFileName.QUESTION_IMPORT.getName())) {
+                continue;
+            }
+            InputStream inputStream = null;
+            ImportResult ret = null;
+            try {
+                inputStream = new FileInputStream(file);
+                ret = questionService.disposeFile(inputStream);
+            } catch (Exception e) {
+                String errMsg;
+                if (e instanceof FileNotFoundException) {
+                    errMsg = "未找到文件:" + file.getAbsolutePath();
+                } else {
+                    errMsg = "系统错误:" + e.getMessage();
+                    log.error("系统错误", e);
+                }
+                ret = new ImportResult(errMsg);
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+            moveFile(dir, file, ret);
+        }
+    }
+
+    private void moveFile(File dir, File file, ImportResult ret) {
+        try {
+            boolean succss = CollectionUtils.isEmpty(ret.getErrMsg());
+            if (succss) {
+                File sucDir = new File(dir.getAbsoluteFile() + "/success/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.write(msgFile, ret.getCountInfo(), "utf-8");
+            } else {
+                File sucDir = new File(dir.getAbsoluteFile() + "/failed/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.writeLines(msgFile, StandardCharsets.UTF_8.name(), ret.getErrMsg());
+            }
+        } catch (IOException e) {
+            throw new StatusException("文件处理出错", e);
+        }
+
+    }
+
+    private String errorMsg(int lineNum, String msg) {
+        return "第" + lineNum + "行 " + msg;
+    }
+
+    private String trimAndNullIfBlank(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        return s.trim();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Transactional
+    @Override
+    public ImportResult disposeFile(InputStream inputStream) {
+        List<DataMap> lineList = null;
+        ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
+        try {
+            lineList = reader.getDataMapList();
+        } catch (Exception e) {
+            throw new StatusException("Excel 解析失败");
+        }
+        if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
+            throw new StatusException("Excel表头错误");
+        }
+        if (CollectionUtils.isEmpty(lineList)) {
+            throw new StatusException("Excel无内容");
+        }
+        if (100001 < lineList.size()) {
+            throw new StatusException("数据行数不能超过100000");
+        }
+        List<QuestionEntity> ss = new ArrayList<>();
+        ImportResult ret = new ImportResult();
+        List<String> failRecords = new ArrayList<>();
+        ret.setErrMsg(failRecords);
+        for (int i = 0; i < lineList.size(); i++) {
+            DataMap line = lineList.get(i);
+
+            StringBuilder msg = new StringBuilder();
+
+            QuestionEntity imp = new QuestionEntity();
+            String examId = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
+            if (StringUtils.isBlank(examId)) {
+                msg.append("  考试ID不能为空");
+            } else if (examId.length() > 20) {
+                msg.append("  考试ID不能超过20个字符");
+            } else {
+                try {
+                    Long examIdVal = Long.parseLong(examId);
+                    imp.setExamId(examIdVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  考试ID只能是数字");
+                }
+            }
+
+            String subjectCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
+            if (StringUtils.isBlank(subjectCode)) {
+                msg.append("  科目代码不能为空");
+            } else if (subjectCode.length() > 100) {
+                msg.append("  科目代码不能超过100个字符");
+            }
+            imp.setSubjectCode(subjectCode);
+
+            String subjectName = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
+            if (StringUtils.isBlank(subjectName)) {
+                msg.append("  科目名称不能为空");
+            } else if (subjectName.length() > 100) {
+                msg.append("  科目名称不能超过100个字符");
+            }
+            imp.setSubjectName(subjectName);
+
+            String title = trimAndNullIfBlank(line.get(EXCEL_HEADER[3]));
+            if (StringUtils.isBlank(title)) {
+                msg.append("  题目名称不能为空");
+            } else if (title.length() > 100) {
+                msg.append("  题目名称不能超过100个字符");
+            }
+            imp.setTitle(title);
+
+            String mainNum = trimAndNullIfBlank(line.get(EXCEL_HEADER[4]));
+            if (StringUtils.isBlank(mainNum)) {
+                msg.append("  大题号不能为空");
+            } else if (mainNum.length() > 10) {
+                msg.append("  大题号不能超过10个字符");
+            } else {
+                try {
+                    Integer mainNumVal = Integer.parseInt(mainNum);
+                    if (mainNumVal <= 0) {
+                        msg.append("  大题号必须大于0");
+                    }
+                    imp.setMainNumber(mainNumVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  大题号格式错误");
+                }
+            }
+
+            String subNum = trimAndNullIfBlank(line.get(EXCEL_HEADER[5]));
+            if (StringUtils.isBlank(subNum)) {
+                msg.append("  小题号不能为空");
+            } else if (subNum.length() > 10) {
+                msg.append("  小题号不能超过10个字符");
+            }
+            imp.setSubNumber(subNum);
+
+            String fullScore = trimAndNullIfBlank(line.get(EXCEL_HEADER[6]));
+            if (StringUtils.isBlank(fullScore)) {
+                msg.append("  满分不能为空");
+            } else if (fullScore.length() > 10) {
+                msg.append("  满分不能超过10个字符");
+            } else {
+                try {
+                    Double fullScoreVal = Double.parseDouble(fullScore);
+                    if (fullScoreVal <= 0) {
+                        msg.append("  满分必须大于0");
+                    }
+                    imp.setFullScore(fullScoreVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  满分格式错误");
+                }
+            }
+
+            String content = trimAndNullIfBlank(line.get(EXCEL_HEADER[7]));
+            if (StringUtils.isBlank(content)) {
+                msg.append("  试题内容不能为空");
+            }
+            imp.setContent(content);
+
+            String answer = trimAndNullIfBlank(line.get(EXCEL_HEADER[8]));
+            // if (StringUtils.isBlank(answer)) {
+            // msg.append(" 试题答案不能为空");
+            // }
+            imp.setAnswer(getStandardAnswer(answer));
+
+            String imageSlice = trimAndNullIfBlank(line.get(EXCEL_HEADER[9]));
+            if (StringUtils.isBlank(imageSlice)) {
+                msg.append("  作答坐标不能为空");
+            } else if (imageSlice.length() > 1000) {
+                msg.append("  作答坐标不能超过1000个字符");
+            } else {
+                List<ImageSlice> val = getImageSlice(imageSlice);
+                if (val == null) {
+                    msg.append("  作答坐标格式有误");
+                } else {
+                    imp.setImageSlice(val);
+                }
+            }
+
+            if (msg.length() > 0) {
+                failRecords.add(errorMsg(i + 2, msg.toString()));
+            } else {
+                ss.add(imp);
+            }
+
+        }
+
+        if (CollectionUtils.isNotEmpty(failRecords)) {
+            return ret;
+        }
+        try {
+            saveQuestionBatch(ret, ss);
+        } catch (Exception e) {
+            failRecords.add("系统错误:" + e.getMessage());
+        }
+        return ret;
+    }
+
+    private List<ImageSlice> getImageSlice(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        s = s.trim();
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        try {
+            List<ImageSlice> list = new ArrayList<>();
+            String[] items = s.split(",");
+            for (int i = 0; i < items.length; i++) {
+                String item = items[i];
+                item = item.trim();
+                String[] config = item.split(":");
+                if (config.length != 5) {
+                    return null;
+                }
+                int iVal = Integer.valueOf(config[0]);
+                int x = Integer.valueOf(config[1]);
+                int y = Integer.valueOf(config[2]);
+                int w = Integer.valueOf(config[3]);
+                int h = Integer.valueOf(config[4]);
+                ImageSlice ret = new ImageSlice();
+                ret.setH(h);
+                ret.setI(iVal);
+                ret.setW(w);
+                ret.setX(x);
+                ret.setY(y);
+                if (ret.getH() == null || ret.getI() == null || ret.getW() == null || ret.getX() == null
+                        || ret.getY() == null || ret.getI() < 0) {
+                    return null;
+                }
+                list.add(ret);
+            }
+            if (list.size() == 0) {
+                return null;
+            }
+            list.sort(new Comparator<ImageSlice>() {
+
+                @Override
+                public int compare(ImageSlice o1, ImageSlice o2) {
+                    long c1 = o1.getI();
+                    long c2 = o2.getI();
+                    if (c1 < c2) {
+                        return -1;
+                    } else if (c1 > c2) {
+                        return 1;
+                    } else {
+                        return 0;
+                    }
+                }
+            });
+            return list;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private List<StandardAnswer> getStandardAnswer(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        s = s.trim();
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        List<StandardAnswer> list = new ArrayList<>();
+        Matcher matcher = scoreRex.matcher(s);
+        int start = 0;
+        Double score = null;
+        while (matcher.find()) {
+            if (start != 0) {
+                StandardAnswer a = new StandardAnswer();
+                list.add(a);
+                a.setScore(score);
+                a.setContent(s.substring(start, matcher.start()));
+            }
+            try {
+                score = Double.valueOf(matcher.group(1));
+            } catch (NumberFormatException e) {
+                throw new StatusException("分数格式有误");
+            }
+            checkScore(score);
+            start = matcher.end();
+        }
+        if (start < s.length()) {
+            StandardAnswer a = new StandardAnswer();
+            list.add(a);
+            a.setScore(score);
+            checkScore(score);
+            a.setContent(s.substring(start, s.length()));
+        }
+        return list;
+    }
+
+    private void checkScore(Double score) {
+        if (score == null) {
+            throw new StatusException("分数不能为空");
+        }
+    }
+
+    private void saveQuestionBatch(ImportResult ret, List<QuestionEntity> ss) {
+        if (CollectionUtils.isEmpty(ss)) {
+            ret.setCountInfo("新增数量:0,更新数量:0");
+            return;
+        }
+        List<QuestionEntity> all = this.list();
+        Map<String, QuestionEntity> old = new HashMap<>();
+        Map<String, QuestionEntity> addMap = new HashMap<>();
+        if (CollectionUtils.isNotEmpty(all)) {
+            for (QuestionEntity s : all) {
+                String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getMainNumber() + "-"
+                        + s.getSubNumber();
+                old.put(key, s);
+            }
+        }
+        List<QuestionEntity> adds = new ArrayList<>();
+        List<QuestionEntity> updates = new ArrayList<>();
+        for (QuestionEntity s : ss) {
+            String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getMainNumber() + "-" + s.getSubNumber();
+            if (old.get(key) == null) {
+                QuestionEntity add = addMap.get(key);
+                if (add != null) {
+                    add.setSubjectName(s.getSubjectName());
+                    add.setFullScore(s.getFullScore());
+                    add.setImageSlice(s.getImageSlice());
+                    add.setContent(s.getContent());
+                    add.setAnswer(s.getAnswer());
+                } else {
+                    addMap.put(key, s);
+                    adds.add(s);
+                }
+            } else {
+                QuestionEntity up = old.get(key);
+                up.setSubjectName(s.getSubjectName());
+                up.setFullScore(s.getFullScore());
+                up.setImageSlice(s.getImageSlice());
+                up.setContent(s.getContent());
+                up.setAnswer(s.getAnswer());
+                updates.add(up);
+            }
+        }
+        if (CollectionUtils.isNotEmpty(adds)) {
+            saveBatch(adds);
+        }
+        if (CollectionUtils.isNotEmpty(updates)) {
+            updateBatchById(updates);
+        }
+        ret.setCountInfo("新增数量:" + adds.size() + ",更新数量:" + updates.size());
+    }
+
+    @Override
+    public List<QuestionEntity> findByExamId(Long examId) {
+        QueryWrapper<QuestionEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<QuestionEntity> lw = wrapper.lambda();
+        lw.eq(QuestionEntity::getExamId, examId);
+        lw.orderByAsc(QuestionEntity::getSubjectCode).orderByAsc(QuestionEntity::getMainNumber)
+                .orderByAsc(QuestionEntity::getSubNumber);
+        return this.list(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void removeBy(Long examId, String subjectCode) {
+        QueryWrapper<QuestionEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<QuestionEntity> lw = wrapper.lambda();
+        if (subjectCode != null) {
+            lw.eq(QuestionEntity::getSubjectCode, subjectCode);
+        }
+        lw.eq(QuestionEntity::getExamId, examId);
+        this.remove(wrapper);
+    }
+
+}

+ 912 - 0
src/main/java/cn/com/qmth/am/service/impl/StudentScoreServiceImpl.java

@@ -0,0 +1,912 @@
+package cn.com.qmth.am.service.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.boot.core.ai.client.OcrApiClient;
+import com.qmth.boot.core.ai.model.llm.AutoScoreResult;
+import com.qmth.boot.core.ai.model.ocr.OcrType;
+import com.qmth.boot.core.ai.service.AiService;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+import com.qmth.boot.core.retrofit.utils.SignatureInfo;
+import com.qmth.boot.core.retrofit.utils.UploadFile;
+import com.qmth.boot.core.solar.model.OrgInfo;
+import com.qmth.boot.core.solar.service.SolarService;
+import com.qmth.boot.tools.excel.ExcelReader;
+import com.qmth.boot.tools.excel.enums.ExcelType;
+import com.qmth.boot.tools.excel.model.DataMap;
+import com.qmth.boot.tools.models.ByteArray;
+
+import cn.com.qmth.am.bean.AiMarkingDto;
+import cn.com.qmth.am.bean.AnswerImageDto;
+import cn.com.qmth.am.bean.AutoScoreEnRequest;
+import cn.com.qmth.am.bean.ImageSlice;
+import cn.com.qmth.am.bean.ImportResult;
+import cn.com.qmth.am.bean.StudentInfo;
+import cn.com.qmth.am.bean.StudentScoreDto;
+import cn.com.qmth.am.bean.StudentScoreImageDto;
+import cn.com.qmth.am.bean.StudentScoreInfo;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.dao.StudentScoreDao;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.DataType;
+import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.enums.ModelTypeBak;
+import cn.com.qmth.am.service.DsMarkingService;
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StudentScoreService;
+import cn.com.qmth.am.utils.BatchSetDataUtil;
+import cn.com.qmth.am.utils.FileUtil;
+import cn.com.qmth.am.utils.ImageUtil;
+
+@Service
+public class StudentScoreServiceImpl extends ServiceImpl<StudentScoreDao, StudentScoreEntity>
+        implements StudentScoreService {
+
+    private static final Logger log = LoggerFactory.getLogger(StudentScoreService.class);
+
+    // private static BlockingQueue<StudentScoreImageDto> queue;
+
+    private static final String[] EXCEL_HEADER = new String[] { "考试ID", "科目代码", "考生编号", "大题号", "小题号", "评分" };
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private SolarService solarService;
+
+    @Autowired
+    private OcrApiClient ocrApiClient;
+
+    @Autowired
+    private AiService aiService;
+
+    @Autowired
+    private DsMarkingService dsMarkingService;
+
+    @Autowired
+    private QuestionService questionService;
+    // static {
+    // int threadCount = Runtime.getRuntime().availableProcessors();
+    // queue = new ArrayBlockingQueue<>(threadCount * 2);
+    // }
+
+    @Override
+    public void importScore() {
+        File dir = new File(sysProperty.getDataDir());
+        File[] fs = dir.listFiles();
+        if (fs == null || fs.length == 0) {
+            return;
+        }
+        for (File file : fs) {
+            if (!file.isFile() || !file.getName().equals(ImportFileName.SCORE_IMPORT.getName())) {
+                continue;
+            }
+            InputStream inputStream = null;
+            ImportResult ret = null;
+            try {
+                inputStream = new FileInputStream(file);
+                ret = disposeFile(inputStream);
+            } catch (Exception e) {
+                String errMsg;
+                if (e instanceof FileNotFoundException) {
+                    errMsg = "未找到文件:" + file.getAbsolutePath();
+                } else {
+                    errMsg = "系统错误:" + e.getMessage();
+                    log.error("系统错误", e);
+                }
+                ret = new ImportResult(errMsg);
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+            moveFile(dir, file, ret);
+        }
+    }
+
+    private void moveFile(File dir, File file, ImportResult ret) {
+        try {
+            boolean succss = CollectionUtils.isEmpty(ret.getErrMsg());
+            if (succss) {
+                File sucDir = new File(dir.getAbsoluteFile() + "/success/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.write(msgFile, ret.getCountInfo(), "utf-8");
+            } else {
+                File sucDir = new File(dir.getAbsoluteFile() + "/failed/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.writeLines(msgFile, StandardCharsets.UTF_8.name(), ret.getErrMsg());
+            }
+        } catch (IOException e) {
+            throw new StatusException("文件处理出错", e);
+        }
+
+    }
+
+    private String errorMsg(int lineNum, String msg) {
+        return "第" + lineNum + "行 " + msg;
+    }
+
+    private String trimAndNullIfBlank(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        return s.trim();
+    }
+
+    @SuppressWarnings("deprecation")
+    private ImportResult disposeFile(InputStream inputStream) {
+        List<DataMap> lineList = null;
+        ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
+        try {
+            lineList = reader.getDataMapList();
+        } catch (Exception e) {
+            throw new StatusException("Excel 解析失败");
+        }
+        if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
+            throw new StatusException("Excel表头错误");
+        }
+        if (CollectionUtils.isEmpty(lineList)) {
+            throw new StatusException("Excel无内容");
+        }
+        if (100001 < lineList.size()) {
+            throw new StatusException("数据行数不能超过100000");
+        }
+        List<StudentScoreDto> ss = new ArrayList<>();
+        ImportResult ret = new ImportResult();
+        List<String> failRecords = new ArrayList<>();
+        ret.setErrMsg(failRecords);
+        for (int i = 0; i < lineList.size(); i++) {
+            DataMap line = lineList.get(i);
+
+            StringBuilder msg = new StringBuilder();
+
+            StudentScoreDto imp = new StudentScoreDto();
+            String examId = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
+            if (StringUtils.isBlank(examId)) {
+                msg.append("  考试ID不能为空");
+            } else if (examId.length() > 20) {
+                msg.append("  考试ID不能超过20个字符");
+            } else {
+                try {
+                    Long examIdVal = Long.parseLong(examId);
+                    imp.setExamId(examIdVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  考试ID只能是数字");
+                }
+            }
+
+            String subjectCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
+            if (StringUtils.isBlank(subjectCode)) {
+                msg.append("  科目代码不能为空");
+            } else if (subjectCode.length() > 100) {
+                msg.append("  科目代码不能超过100个字符");
+            }
+            imp.setSubjectCode(subjectCode);
+
+            String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
+            if (StringUtils.isBlank(studentCode)) {
+                msg.append("  考生编号不能为空");
+            } else if (studentCode.length() > 100) {
+                msg.append("  考生编号不能超过100个字符");
+            }
+            imp.setStudentCode(studentCode);
+
+            String mainNum = trimAndNullIfBlank(line.get(EXCEL_HEADER[3]));
+            if (StringUtils.isBlank(mainNum)) {
+                msg.append("  大题号不能为空");
+            } else if (mainNum.length() > 10) {
+                msg.append("  大题号不能超过10个字符");
+            } else {
+                try {
+                    Integer mainNumVal = Integer.parseInt(mainNum);
+                    if (mainNumVal <= 0) {
+                        msg.append("  大题号必须大于0");
+                    }
+                    imp.setMainNumber(mainNumVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  大题号格式错误");
+                }
+            }
+
+            String subNum = trimAndNullIfBlank(line.get(EXCEL_HEADER[4]));
+            if (StringUtils.isBlank(subNum)) {
+                msg.append("  小题号不能为空");
+            } else if (subNum.length() > 10) {
+                msg.append("  小题号不能超过10个字符");
+            }
+            imp.setSubNumber(subNum);
+
+            String score = trimAndNullIfBlank(line.get(EXCEL_HEADER[5]));
+            if (StringUtils.isBlank(score)) {
+                msg.append("  评分不能为空");
+            } else if (score.length() > 10) {
+                msg.append("  评分不能超过10个字符");
+            } else {
+                try {
+                    Double scoreVal = Double.parseDouble(score);
+                    imp.setMarkingScore(scoreVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  评分格式错误");
+                }
+            }
+
+            if (msg.length() > 0) {
+                failRecords.add(errorMsg(i + 2, msg.toString()));
+            } else {
+                ss.add(imp);
+            }
+
+        }
+
+        if (CollectionUtils.isNotEmpty(failRecords)) {
+            return ret;
+        }
+        try {
+            updateScoreBatch(ret, ss);
+        } catch (Exception e) {
+            failRecords.add("系统错误:" + e.getMessage());
+        }
+        return ret;
+    }
+
+    private void updateScoreBatch(ImportResult ret, List<StudentScoreDto> ss) {
+        if (CollectionUtils.isEmpty(ss)) {
+            ret.setCountInfo("更新数量:0");
+            return;
+        }
+        int count = 0;
+        for (StudentScoreDto s : ss) {
+            count = count + updateScore(s);
+        }
+        ret.setCountInfo("更新数量:" + count);
+    }
+
+    private int updateScore(StudentScoreDto dto) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getMarkingScore, dto.getMarkingScore());
+        lw.eq(StudentScoreEntity::getExamId, dto.getExamId());
+        lw.eq(StudentScoreEntity::getSubjectCode, dto.getSubjectCode());
+        lw.eq(StudentScoreEntity::getStudentCode, dto.getStudentCode());
+        lw.eq(StudentScoreEntity::getMainNumber, dto.getMainNumber());
+        lw.eq(StudentScoreEntity::getSubNumber, dto.getSubNumber());
+        return this.update(wrapper) ? 1 : 0;
+    }
+
+    @Transactional
+    @Override
+    public void addStudentScore(List<StudentInfo> ss) {
+        List<QuestionEntity> qs = questionService.list();
+        if (CollectionUtils.isEmpty(qs)) {
+            throw new StatusException("试题信息为空");
+        }
+        Map<String, List<QuestionEntity>> qmap = new HashMap<>();
+        for (QuestionEntity q : qs) {
+            String key = q.getExamId() + "-" + q.getSubjectCode();
+            List<QuestionEntity> tem = qmap.get(key);
+            if (tem == null) {
+                tem = new ArrayList<>();
+                qmap.put(key, tem);
+            }
+            tem.add(q);
+        }
+        Set<String> allStudent = getAllStudent();
+
+        BatchSetDataUtil<StudentInfo> bs = new BatchSetDataUtil<StudentInfo>() {
+
+            @Override
+            protected void setData(List<StudentInfo> dataList) {
+                List<StudentScoreEntity> adds = new ArrayList<>();
+                for (StudentInfo stu : dataList) {
+                    String key = stu.getExamId() + "-" + stu.getSubjectCode();
+                    if (qmap.get(key) == null) {
+                        throw new StatusException("试题信息为空:" + key);
+                    }
+                    for (QuestionEntity q : qmap.get(key)) {
+                        String scorekey = q.getId() + "-" + stu.getStudentCode();
+                        if (!allStudent.contains(scorekey)) {
+                            StudentScoreEntity s = new StudentScoreEntity();
+                            adds.add(s);
+                            allStudent.add(scorekey);
+                            s.setQuestionId(q.getId());
+                            s.setExamId(stu.getExamId());
+                            s.setAnswerStatus(DataStatus.WAITING);
+                            s.setMainNumber(q.getMainNumber());
+                            s.setScoreStatus(DataStatus.WAITING);
+                            s.setStudentCode(stu.getStudentCode());
+                            s.setSubjectCode(stu.getSubjectCode());
+                            s.setSubNumber(q.getSubNumber());
+                        }
+                    }
+                }
+                if (CollectionUtils.isNotEmpty(adds)) {
+                    saveBatch(adds);
+                }
+            }
+        };
+        bs.setDataForBatch(ss, 1000);
+
+    }
+
+    private Set<String> getAllStudent() {
+        Set<String> ret = new HashSet<>();
+        List<StudentScoreInfo> list = this.baseMapper.getAllList();
+        if (CollectionUtils.isEmpty(list)) {
+            return ret;
+        }
+        for (StudentScoreInfo s : list) {
+            ret.add(s.getQuestionId() + "-" + s.getStudentCode());
+        }
+        return ret;
+    }
+
+    @Transactional
+    @Override
+    public void updateAnswerErr(Long id, String err) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getAnswerStatus, DataStatus.FAILED);
+        lw.set(StudentScoreEntity::getErrMsg, err);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void createSlice(StudentScoreEntity score, QuestionEntity q, Map<Integer, AnswerImageDto> answerImages) {
+        StudentScoreImageDto dto = new StudentScoreImageDto();
+        dto.setStudentScoreId(score.getId());
+        getSlice(score, q, answerImages, dto);
+        // saveSliceImage(score, dto.getImage());
+        ocr(dto);
+    }
+
+    private void getSlice(StudentScoreEntity score, QuestionEntity q, Map<Integer, AnswerImageDto> answerImages,
+            StudentScoreImageDto dto) {
+        List<byte[]> ret = new ArrayList<>();
+        String suff = null;
+        for (ImageSlice s : q.getImageSlice()) {
+            AnswerImageDto sheet = getSheet(score, q, s.getI(), answerImages);
+            suff = sheet.getSuff();
+            ret.add(ImageUtil.cutImage(sheet.getImage(), sheet.getSuff(), s.getX().intValue(), s.getY().intValue(),
+                    s.getW().intValue(), s.getH().intValue()));
+        }
+        dto.setSuff(suff);
+        if (ret.size() > 1) {
+            dto.setImage(ImageUtil.joinImages(ret, suff));
+        } else {
+            dto.setImage(ret.get(0));
+        }
+    }
+
+    private AnswerImageDto getSheet(StudentScoreEntity score, QuestionEntity q, Integer pageIndex,
+            Map<Integer, AnswerImageDto> answerImages) {
+        AnswerImageDto ret = answerImages.get(pageIndex);
+        if (ret != null) {
+            return ret;
+        }
+        ret = new AnswerImageDto();
+        String url = getImageUrl(score, q, pageIndex);
+        url = url.replace("https", "http");
+        try {
+            String tem = url.split("\\?")[0];
+            String suff = tem.substring(tem.lastIndexOf(".") + 1).toLowerCase();
+            ret.setImage(ByteArray.fromUrl(url).value());
+            // saveSheetImage(score,pageIndex, ret.getImage());
+            ret.setPageIndex(pageIndex);
+            ret.setSuff(suff);
+            answerImages.put(pageIndex, ret);
+            return ret;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+    // private void saveSheetImage(StudentScoreEntity s,Integer page,byte[] bs)
+    // {
+    // File dir=new File(sysProperty.getDataDir()+"/"+"sheet");
+    // if(!dir.exists()) {
+    // dir.mkdir();
+    // }
+    // File image=new
+    // File(dir.getAbsolutePath()+"/"+s.getStudentCode()+"-"+page+".jpg");
+    // if(image.exists()) {
+    // image.delete();
+    // }
+    // FileOutputStream out=null;
+    // try {
+    // out = new FileOutputStream(image);
+    // out.write(bs, 0, bs.length);
+    // out.flush();
+    // } catch (Exception e) {
+    // }finally {
+    // if(out!=null) {
+    // try {
+    // out.close();
+    // } catch (IOException e) {
+    // }
+    // }
+    // }
+    // }
+    // private void saveSliceImage(StudentScoreEntity s,byte[] bs) {
+    // File dir=new File(sysProperty.getDataDir()+"/"+"slice");
+    // if(!dir.exists()) {
+    // dir.mkdir();
+    // }
+    // File image=new
+    // File(dir.getAbsolutePath()+"/"+s.getStudentCode()+"-"+s.getMainNumber()+"-"+s.getSubNumber()+".jpg");
+    // if(image.exists()) {
+    // image.delete();
+    // }
+    // FileOutputStream out=null;
+    // try {
+    // out = new FileOutputStream(image);
+    // out.write(bs, 0, bs.length);
+    // out.flush();
+    // } catch (Exception e) {
+    // }finally {
+    // if(out!=null) {
+    // try {
+    // out.close();
+    // } catch (IOException e) {
+    // }
+    // }
+    // }
+    // }
+
+    private String getImageUrl(StudentScoreEntity score, QuestionEntity q, Integer pageIndex) {
+        if (DataType.MARKING_CLOUD.equals(sysProperty.getDataType())) {
+            return getImageUrlFromMarkingCloud(score, pageIndex);
+        } else if (DataType.TEACH_CLOUD.equals(sysProperty.getDataType())) {
+            return getImageUrlFromTeachCloud(score, q, pageIndex);
+        } else {
+            throw new StatusException("数据类型错误");
+        }
+    }
+
+    private String getImageUrlFromMarkingCloud(StudentScoreEntity score, Integer pageIndex) {
+        return sysProperty.getImageServer() + "/" + getMarkingCloudPath(score.getExamId(),
+                getSuffix(score.getStudentCode()), score.getStudentCode(), pageIndex, "jpg");
+    }
+
+    private static String getSuffix(String input) {
+        return StringUtils.trimToEmpty(input).substring(Math.max(0, input.length() - 3));
+    }
+
+    private String getImageUrlFromTeachCloud(StudentScoreEntity score, QuestionEntity q, Integer pageIndex) {
+        int paperNum = (pageIndex + 1) / 2;
+        int page = 1;
+        if (pageIndex % 2 == 0) {
+            page = 2;
+        }
+        return sysProperty.getImageServer() + "/" + getTeachCloudPath(score.getExamId(), score.getSubjectCode(),
+                score.getStudentCode(), paperNum, page, "jpg");
+    }
+
+    private String getTeachCloudPath(Object... param) {
+        return String.format("sheet/%d/%s/%s/%d-%d.%s", param);
+    }
+
+    private static String getMarkingCloudPath(Object... param) {
+        return String.format("sheet/%d/%s/%s-%d.%s", param);
+    }
+
+    public static void main(String[] args) {
+        // Pattern pattern =
+        // Pattern.compile("\\[\\[([0-9](.[0-9]+){0,1})分\\]\\]");
+        // String d="[[1.1分]]中国共产党是中国特色社会主义事业的坚强领导核心。\n"
+        // + "[[1.2分]]中国共产党的领导地位是在历史奋斗中形成的。\n"
+        // + "[[1.3分]]中国共产党领导是人民当家作主的可靠保障。\n"
+        // + "[[1.4分]]中国共产党领导关系中国特色社会主义的性质、方向和命运。\n"
+        // + "[[2分]]中国共产党领导是实现中华民族伟大复兴的根本保证。";
+        // Matcher matcher = pattern.matcher(d);
+        // int start=0;
+        // double score=0.0;
+        // while (matcher.find()) {
+        // if(start!=0) {
+        // System.out.println(score+d.substring(start,matcher.start()));
+        // }
+        // score=Double.valueOf(matcher.group(1));
+        // start=matcher.end();
+        // }
+        // if(start<d.length()) {
+        // System.out.println(score+d.substring(start,d.length()));
+        // }
+        // String[] items = d.split("\\[\\[[0-9](.[0-9]+){0,1}分\\]\\]");
+        String code = "23635173";
+        String s = "https://file.markingcloud.com/" + getMarkingCloudPath(725, getSuffix(code), code, 2, "jpg");
+        System.out.println(s);
+    }
+
+    // @Override
+    // public StudentScoreImageDto pollStudentScoreImage() {
+    // try {
+    // return queue.take();
+    // } catch (InterruptedException e) {
+    // throw new RuntimeException(e);
+    // }
+    // }
+
+    @Transactional
+    @Override
+    public void ocr(StudentScoreImageDto dto) {
+        try {
+            OrgInfo org = solarService.getOrgList().get(0);
+            String ret = ocrDispose(dto, org);
+            if (ret != null) {
+                updateAnswer(dto.getStudentScoreId(), ret);
+            } else {
+                ocrErr(dto, "ocr失败,返回null");
+            }
+        } catch (Exception e) {
+            ocrErr(dto, e.getMessage());
+        }
+    }
+
+    private void updateAnswer(Long id, String answer) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getAnswerStatus, DataStatus.SUCCESS);
+        lw.set(StudentScoreEntity::getErrMsg, null);
+        lw.set(StudentScoreEntity::getAnswer, answer);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    private String ocrDispose(StudentScoreImageDto dto, OrgInfo org) {
+        SignatureInfo signature = null;
+        if (ModelTypeBak.solar.getCode().equals(sysProperty.getOcrModel())) {
+            signature = SignatureInfo.secret(org.getAccessKey(), org.getAccessSecret());
+        }
+        try {
+            if (ModelTypeBak.solar.getCode().equals(sysProperty.getOcrModel())) {
+                String ret = ocrApiClient.forImage(signature, OcrType.HANDWRITING,
+                        UploadFile.build("image", "", dto.getImage()));
+                return ret;
+            } else {
+                String base64 = FileUtil.byteToBase64(dto.getImage(), dto.getSuff());
+                String ret = dsMarkingService.ocr(base64);
+                return ret;
+
+            }
+        } catch (Exception e) {
+            log.error("ocr异常", e);
+            if (e instanceof RetrofitResponseError) {
+                RetrofitResponseError tem = (RetrofitResponseError) e;
+                if (tem.getCode() == 503) {
+                    if (dto.getRetry() <= 3) {
+                        try {
+                            Thread.sleep(3000);
+                        } catch (InterruptedException e1) {
+                        }
+                        dto.setRetry(dto.getRetry() + 1);
+                        return ocrDispose(dto, org);
+                    } else {
+                        throw new StatusException("重试次数过多");
+                    }
+                } else {
+                    throw e;
+                }
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    private void ocrErr(StudentScoreImageDto dto, String err) {
+        updateAnswerErr(dto.getStudentScoreId(), err);
+    }
+
+    @Override
+    public List<StudentScoreEntity> findAllToOcr() {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.in(StudentScoreEntity::getAnswerStatus, DataStatus.WAITING, DataStatus.FAILED);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public List<StudentScoreEntity> findAllToAiMarking() {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.eq(StudentScoreEntity::getAnswerStatus, DataStatus.SUCCESS);
+        lw.in(StudentScoreEntity::getScoreStatus, DataStatus.WAITING, DataStatus.FAILED);
+        return this.list(wrapper);
+    }
+
+    // @Override
+    // public List<StudentScoreEntity> findToAiMarking(Long studentId) {
+    // QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+    // LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+    // lw.eq(StudentScoreEntity::getStudentId, studentId);
+    // lw.eq(StudentScoreEntity::getAnswerStatus, DataStatus.SUCCESS);
+    // lw.in(StudentScoreEntity::getScoreStatus, DataStatus.WAITING,
+    // DataStatus.FAILED);
+    // return this.list(wrapper);
+    // }
+
+    @Transactional
+    @Override
+    public void aiMarking(StudentScoreEntity score) {
+        AiMarkingDto dto = new AiMarkingDto();
+        dto.setScoreInfo(score);
+        try {
+            OrgInfo org = solarService.getOrgList().get(0);
+            QuestionEntity q = questionService.getById(score.getQuestionId());
+            if (q == null) {
+                throw new StatusException("未找到试题信息");
+            }
+            if (CollectionUtils.isEmpty(q.getAnswer())) {
+                return;
+            }
+            AutoScoreEnRequest req = new AutoScoreEnRequest();
+            req.setQuestionBody(q.getContent());
+            req.setStandardAnswer(q.getAnswer());
+            req.setStudentAnswer(score.getAnswer());
+            req.setSubjectName(q.getSubjectName());
+            req.setTotalScore(q.getFullScore());
+            req.setIntervalScore(0.1);
+            req.setQuestionTitle(q.getTitle());
+            AutoScoreResult ret = aiMarkingDispose(dto, org, req);
+            if (ret != null) {
+                updateScore(score.getId(), ret.getTotalScore());
+            } else {
+                updateScoreNone(score.getId(), 0.0);
+            }
+        } catch (Exception e) {
+            aiScoreErr(dto, e.getMessage());
+        }
+    }
+
+    private AutoScoreResult aiMarkingDispose(AiMarkingDto dto, OrgInfo org, AutoScoreEnRequest req) {
+        SignatureInfo signature = null;
+        if (ModelTypeBak.solar.getCode().equals(sysProperty.getMarkingModel())) {
+            signature = SignatureInfo.secret(org.getAccessKey(), org.getAccessSecret());
+        }
+        try {
+            if (ModelTypeBak.solar.getCode().equals(sysProperty.getMarkingModel())) {
+                return aiService.autoScore(req, signature);
+            } else {
+                return dsMarkingService.autoScore(req);
+            }
+
+        } catch (Exception e) {
+            log.error("aiScore异常", e);
+            if (e instanceof RetrofitResponseError) {
+                RetrofitResponseError tem = (RetrofitResponseError) e;
+                if (tem.getCode() == 503) {
+                    if (dto.getRetry() <= 3) {
+                        try {
+                            Thread.sleep(3000);
+                        } catch (InterruptedException e1) {
+                        }
+                        dto.setRetry(dto.getRetry() + 1);
+                        return aiMarkingDispose(dto, org, req);
+                    } else {
+                        throw new StatusException("重试次数过多");
+                    }
+                } else {
+                    throw e;
+                }
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    private void updateScore(Long id, Double aiScore) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getScoreStatus, DataStatus.SUCCESS);
+        lw.set(StudentScoreEntity::getAiScore, aiScore);
+        lw.set(StudentScoreEntity::getErrMsg, null);
+        lw.set(StudentScoreEntity::getScoreNone, false);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    private void updateScoreNone(Long id, Double aiScore) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getScoreStatus, DataStatus.SUCCESS);
+        lw.set(StudentScoreEntity::getAiScore, aiScore);
+        lw.set(StudentScoreEntity::getStepScore, null);
+        lw.set(StudentScoreEntity::getErrMsg, null);
+        lw.set(StudentScoreEntity::getScoreNone, true);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    private void aiScoreErr(AiMarkingDto dto, String err) {
+        updateScoreErr(dto.getScoreInfo().getId(), err);
+    }
+
+    private void updateScoreErr(Long id, String err) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getScoreStatus, DataStatus.FAILED);
+        lw.set(StudentScoreEntity::getErrMsg, err);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    private void resetAnswerStatus() {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getAnswerStatus, DataStatus.WAITING);
+        lw.eq(StudentScoreEntity::getAnswerStatus, DataStatus.PROCESSING);
+        this.update(wrapper);
+    }
+
+    private void resetScoreStatus() {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getScoreStatus, DataStatus.WAITING);
+        lw.eq(StudentScoreEntity::getScoreStatus, DataStatus.PROCESSING);
+        this.update(wrapper);
+    }
+
+    @Override
+    public void resetStatus() {
+        resetAnswerStatus();
+        resetScoreStatus();
+    }
+
+    @Override
+    public int countBy(Long examId, DataStatus status) {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        if (status != null) {
+            lw.eq(StudentScoreEntity::getScoreStatus, status);
+        }
+        lw.eq(StudentScoreEntity::getExamId, examId);
+        return this.count(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void removeBy(Long examId, String subjectCode) {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        if (subjectCode != null) {
+            lw.eq(StudentScoreEntity::getSubjectCode, subjectCode);
+        }
+        lw.eq(StudentScoreEntity::getExamId, examId);
+        this.remove(wrapper);
+    }
+
+    @Override
+    public int countOcrBy(Long examId, DataStatus status) {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        if (status != null) {
+            lw.eq(StudentScoreEntity::getAnswerStatus, status);
+        }
+        lw.eq(StudentScoreEntity::getExamId, examId);
+        return this.count(wrapper);
+    }
+
+    @Override
+    public List<StudentScoreEntity> findBy(Long examId, String subjectCode, Integer mainNumber, String subNumber,
+            Boolean exZero, Integer count, Integer score) {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.eq(StudentScoreEntity::getExamId, examId);
+        lw.eq(StudentScoreEntity::getSubjectCode, subjectCode);
+        lw.eq(StudentScoreEntity::getMainNumber, mainNumber);
+        lw.eq(StudentScoreEntity::getSubNumber, subNumber);
+        lw.isNotNull(StudentScoreEntity::getAiScore);
+        lw.isNotNull(StudentScoreEntity::getMarkingScore);
+        lw.and(wq -> {
+            wq.or(wq1 -> wq1.isNull(StudentScoreEntity::getScoreNone));
+            wq.or(wq2 -> wq2.eq(StudentScoreEntity::getScoreNone, false));
+        });
+        if (exZero != null && exZero) {
+            lw.and(wq -> {
+                wq.or(wq1 -> wq1.ne(StudentScoreEntity::getAiScore, 0));
+                wq.or(wq2 -> wq2.eq(StudentScoreEntity::getMarkingScore, 0));
+            });
+        }
+        List<StudentScoreEntity> ret = this.list(wrapper);
+        if (CollectionUtils.isEmpty(ret)) {
+            return ret;
+        }
+        if (score != null) {
+            List<StudentScoreEntity> tem = new ArrayList<>();
+            for (StudentScoreEntity s : ret) {
+                if (getSubtract(s.getAiScore(), s.getMarkingScore()) <= score) {
+                    tem.add(s);
+                }
+            }
+            ret = tem;
+        }
+        if (CollectionUtils.isEmpty(ret)) {
+            return ret;
+        }
+        if (count != null) {
+            ret.sort(new Comparator<StudentScoreEntity>() {
+
+                @Override
+                public int compare(StudentScoreEntity o1, StudentScoreEntity o2) {
+                    String c1 = o1.getStudentCode();
+                    String c2 = o2.getStudentCode();
+                    return c1.compareTo(c2);
+                }
+            });
+            if (ret.size() <= count) {
+                return ret;
+            }
+            return ret.subList(0, count);
+        }
+        return ret;
+    }
+
+    private Double getSubtract(Double d1, Double d2) {
+        if (d1 == null || d2 == null) {
+            return null;
+        }
+        Double r = d1 - d2;
+        if (r < 0) {
+            r = 0 - r;
+        }
+        return r;
+    }
+}

+ 303 - 0
src/main/java/cn/com/qmth/am/service/impl/StudentServiceImpl.java

@@ -0,0 +1,303 @@
+package cn.com.qmth.am.service.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.tools.excel.ExcelReader;
+import com.qmth.boot.tools.excel.enums.ExcelType;
+import com.qmth.boot.tools.excel.model.DataMap;
+
+import cn.com.qmth.am.bean.AnswerImageDto;
+import cn.com.qmth.am.bean.ImportResult;
+import cn.com.qmth.am.bean.StudentInfo;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StudentScoreService;
+import cn.com.qmth.am.service.StudentService;
+
+@Service
+public class StudentServiceImpl implements StudentService {
+
+    private static final Logger log = LoggerFactory.getLogger(StudentService.class);
+
+    private static final String[] EXCEL_HEADER = new String[] { "考试ID", "科目代码", "考生编号" };
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Override
+    public void importStudent() {
+        File dir = new File(sysProperty.getDataDir());
+        File[] fs = dir.listFiles();
+        if (fs == null || fs.length == 0) {
+            return;
+        }
+        for (File file : fs) {
+            if (!file.isFile() || !file.getName().equals(ImportFileName.STUDENT_IMPORT.getName())) {
+                continue;
+            }
+            InputStream inputStream = null;
+            ImportResult ret = null;
+            try {
+                inputStream = new FileInputStream(file);
+                ret = studentService.disposeFile(inputStream);
+            } catch (Exception e) {
+                String errMsg;
+                if (e instanceof FileNotFoundException) {
+                    errMsg = "未找到文件:" + file.getAbsolutePath();
+                } else {
+                    errMsg = "错误:" + e.getMessage();
+                    log.error("系统错误", e);
+                }
+                ret = new ImportResult(errMsg);
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+            moveFile(dir, file, ret);
+        }
+    }
+
+    private void moveFile(File dir, File file, ImportResult ret) {
+        try {
+            boolean succss = CollectionUtils.isEmpty(ret.getErrMsg());
+            if (succss) {
+                File sucDir = new File(dir.getAbsoluteFile() + "/success/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.write(msgFile, ret.getCountInfo(), "utf-8");
+            } else {
+                File sucDir = new File(dir.getAbsoluteFile() + "/failed/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.writeLines(msgFile, StandardCharsets.UTF_8.name(), ret.getErrMsg());
+            }
+        } catch (IOException e) {
+            throw new StatusException("文件处理出错", e);
+        }
+
+    }
+
+    private String errorMsg(int lineNum, String msg) {
+        return "第" + lineNum + "行 " + msg;
+    }
+
+    private String trimAndNullIfBlank(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        return s.trim();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Transactional
+    @Override
+    public ImportResult disposeFile(InputStream inputStream) {
+        List<DataMap> lineList = null;
+        ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
+        try {
+            lineList = reader.getDataMapList();
+        } catch (Exception e) {
+            throw new StatusException("Excel 解析失败");
+        }
+        if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
+            throw new StatusException("Excel表头错误");
+        }
+        if (CollectionUtils.isEmpty(lineList)) {
+            throw new StatusException("Excel无内容");
+        }
+        if (100001 < lineList.size()) {
+            throw new StatusException("数据行数不能超过100000");
+        }
+        List<StudentInfo> ss = new ArrayList<>();
+        ImportResult ret = new ImportResult();
+        List<String> failRecords = new ArrayList<>();
+        ret.setErrMsg(failRecords);
+        for (int i = 0; i < lineList.size(); i++) {
+            DataMap line = lineList.get(i);
+
+            StringBuilder msg = new StringBuilder();
+
+            StudentInfo imp = new StudentInfo();
+            String examId = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
+            if (StringUtils.isBlank(examId)) {
+                msg.append("  考试ID不能为空");
+            } else if (examId.length() > 20) {
+                msg.append("  考试ID不能超过20个字符");
+            } else {
+                try {
+                    Long examIdVal = Long.parseLong(examId);
+                    imp.setExamId(examIdVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  考试ID只能是数字");
+                }
+            }
+
+            String subjectCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
+            if (StringUtils.isBlank(subjectCode)) {
+                msg.append("  科目代码不能为空");
+            } else if (subjectCode.length() > 100) {
+                msg.append("  科目代码不能超过100个字符");
+            }
+            imp.setSubjectCode(subjectCode);
+
+            String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
+            if (StringUtils.isBlank(studentCode)) {
+                msg.append("  考生编号不能为空");
+            } else if (studentCode.length() > 100) {
+                msg.append("  考生编号不能超过100个字符");
+            }
+            imp.setStudentCode(studentCode);
+
+            if (msg.length() > 0) {
+                failRecords.add(errorMsg(i + 2, msg.toString()));
+            } else {
+                ss.add(imp);
+            }
+
+        }
+
+        if (CollectionUtils.isNotEmpty(failRecords)) {
+            return ret;
+        }
+        try {
+            saveStudentBatch(ret, ss);
+        } catch (StatusException e) {
+            failRecords.add("错误:" + e.getMessage());
+        }
+        return ret;
+    }
+
+    private void saveStudentBatch(ImportResult ret, List<StudentInfo> ss) {
+        if (CollectionUtils.isEmpty(ss)) {
+            ret.setCountInfo("新增数量:0");
+            return;
+        }
+        // List<StudentEntity> all = this.list();
+        Set<String> set = new HashSet<>();
+        // if (CollectionUtils.isNotEmpty(all)) {
+        // for (StudentEntity s : all) {
+        // String key = s.getExamId() + "-" + s.getSubjectCode() + "-" +
+        // s.getStudentCode();
+        // set.add(key);
+        // }
+        // }
+        List<StudentInfo> adds = new ArrayList<>();
+        for (StudentInfo s : ss) {
+            String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getStudentCode();
+            if (!set.contains(key)) {
+                adds.add(s);
+            } else {
+                set.add(key);
+            }
+        }
+        if (CollectionUtils.isNotEmpty(adds)) {
+            // saveBatch(adds);
+            studentScoreService.addStudentScore(adds);
+        }
+        // ret.setCountInfo("新增数量:" + adds.size());
+    }
+
+    @Override
+    public void buildImage(StudentScoreEntity score, Map<Long, QuestionEntity> quetions) {
+        Map<Integer, AnswerImageDto> answerImages = new HashMap<>();
+        if (DataStatus.WAITING.equals(score.getAnswerStatus()) || DataStatus.FAILED.equals(score.getAnswerStatus())) {
+            studentService.createSlice(score, quetions, answerImages);
+        }
+    }
+
+    @Override
+    public void createSlice(StudentScoreEntity score, Map<Long, QuestionEntity> quetions,
+            Map<Integer, AnswerImageDto> answerImages) {
+        QuestionEntity q = quetions.get(score.getQuestionId());
+        if (q == null) {
+            studentScoreService.updateAnswerErr(score.getId(), "未找到试题信息");
+            return;
+        }
+        try {
+            studentScoreService.createSlice(score, q, answerImages);
+        } catch (Exception e) {
+            if (e instanceof StatusException) {
+                studentScoreService.updateAnswerErr(score.getId(), e.getMessage());
+            } else {
+                log.error("系统异常", e);
+                studentScoreService.updateAnswerErr(score.getId(), "系统异常");
+            }
+        }
+    }
+
+    @Transactional
+    @Override
+    public void reset(Long examId, String subjectCode) {
+        studentScoreService.removeBy(examId, subjectCode);
+    }
+
+    @Transactional
+    @Override
+    public void clear(Long examId, String subjectCode) {
+        studentScoreService.removeBy(examId, subjectCode);
+        questionService.removeBy(examId, subjectCode);
+    }
+}

+ 97 - 0
src/main/java/cn/com/qmth/am/task/AiMarkingJob.java

@@ -0,0 +1,97 @@
+package cn.com.qmth.am.task;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.consumer.MarkingConsumer;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.StudentScoreService;
+import cn.com.qmth.am.utils.SpringContextHolder;
+
+@Service
+public class AiMarkingJob {
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        int threadCount = sysProperty.getThreadCount();
+        executor = new ThreadPoolExecutor(threadCount, threadCount, 0L, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(threadCount * 2), r -> {
+                    Thread t = new Thread(r);
+                    return t;
+                }, (r, executor) -> {
+                    if (!executor.isShutdown()) {
+                        try {
+                            executor.getQueue().put(r);
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }
+                });
+    }
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        if (!sysProperty.getMarkingTaskEnable()) {
+            return;
+        }
+        boolean lock = concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            this.dispose();
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    private void dispose() {
+        List<StudentScoreEntity> scores = studentScoreService.findAllToAiMarking();
+        if (CollectionUtils.isNotEmpty(scores)) {
+            CountDownLatch endGate = new CountDownLatch(scores.size());
+            for (StudentScoreEntity score : scores) {
+                if (!sysProperty.getMarkingTaskEnable()) {
+                    return;
+                }
+                MarkingConsumer com = SpringContextHolder.getBean(MarkingConsumer.class);
+                com.setScore(score);
+                com.setEndGate(endGate);
+                executor.execute(com);
+            }
+            try {
+                endGate.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+}

+ 125 - 0
src/main/java/cn/com/qmth/am/task/OcrJob.java

@@ -0,0 +1,125 @@
+package cn.com.qmth.am.task;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.consumer.OcrConsumer;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StudentScoreService;
+import cn.com.qmth.am.utils.SpringContextHolder;
+
+@Service
+public class OcrJob {
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        int threadCount = sysProperty.getThreadCount();
+        executor = new ThreadPoolExecutor(threadCount, threadCount, 0L, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(threadCount * 2), r -> {
+                    Thread t = new Thread(r);
+                    return t;
+                }, (r, executor) -> {
+                    if (!executor.isShutdown()) {
+                        try {
+                            executor.getQueue().put(r);
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }
+                });
+    }
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        if (!sysProperty.getOcrTaskEnable()) {
+            return;
+        }
+
+        List<QuestionEntity> qs = questionService.list();
+        if (CollectionUtils.isEmpty(qs)) {
+            return;
+        }
+        List<StudentScoreEntity> scores = studentScoreService.findAllToOcr();
+        if (CollectionUtils.isEmpty(scores)) {
+            return;
+        }
+        boolean lock = concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            this.dispose(scores, qs);
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    private void dispose(List<StudentScoreEntity> scores, List<QuestionEntity> qs) {
+        Map<String, Map<Long, QuestionEntity>> qmap = new HashMap<>();
+        for (QuestionEntity q : qs) {
+            String key = q.getExamId() + "-" + q.getSubjectCode();
+            Map<Long, QuestionEntity> tem = qmap.get(key);
+            if (tem == null) {
+                tem = new HashMap<>();
+                qmap.put(key, tem);
+            }
+            tem.put(q.getId(), q);
+        }
+        CountDownLatch endGate = new CountDownLatch(scores.size());
+        for (StudentScoreEntity score : scores) {
+            if (!sysProperty.getOcrTaskEnable()) {
+                return;
+            }
+            String key = score.getExamId() + "-" + score.getSubjectCode();
+            Map<Long, QuestionEntity> tem = qmap.get(key);
+            if (tem != null) {
+                OcrConsumer com = SpringContextHolder.getBean(OcrConsumer.class);
+                com.setScore(score);
+                com.setQuetions(tem);
+                com.setEndGate(endGate);
+                executor.execute(com);
+            }
+        }
+        try {
+            endGate.await();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 36 - 0
src/main/java/cn/com/qmth/am/task/QuestionImportJob.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.am.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.QuestionService;
+
+@Service
+public class QuestionImportJob {
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        boolean lock = concurrentService.getReadWriteLock(LockType.QUESTION_IMPORT.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            questionService.importQuestion();
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.QUESTION_IMPORT.name()).writeLock().unlock();
+            }
+        }
+    }
+
+}

+ 36 - 0
src/main/java/cn/com/qmth/am/task/StudentImportJob.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.am.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.StudentService;
+
+@Service
+public class StudentImportJob {
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        boolean lock = concurrentService.getReadWriteLock(LockType.STUDENT_IMPORT.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            studentService.importStudent();
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.STUDENT_IMPORT.name()).writeLock().unlock();
+            }
+        }
+    }
+
+}

+ 36 - 0
src/main/java/cn/com/qmth/am/task/StudentScoreImportJob.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.am.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.qmth.boot.core.concurrent.service.ConcurrentService;
+
+import cn.com.qmth.am.enums.LockType;
+import cn.com.qmth.am.service.StudentScoreService;
+
+@Service
+public class StudentScoreImportJob {
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        boolean lock = concurrentService.getReadWriteLock(LockType.MARKING_SCORE_IMPORT.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            studentScoreService.importScore();
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.MARKING_SCORE_IMPORT.name()).writeLock().unlock();
+            }
+        }
+    }
+
+}

+ 40 - 0
src/main/java/cn/com/qmth/am/utils/BatchSetDataUtil.java

@@ -0,0 +1,40 @@
+package cn.com.qmth.am.utils;
+
+import java.util.List;
+
+public abstract class BatchSetDataUtil<P> {
+
+    /**
+     * @param dataList
+     *            待填充的对象集合
+     * @param batchSize
+     *            每批数量
+     */
+    public final void setDataForBatch(List<P> dataList, int batchSize) {
+        if (dataList == null || dataList.size() == 0) {
+            return;
+        }
+        int batchIndex = 1;
+        if (dataList.size() <= batchSize) {
+            setData(dataList);
+            afterBatch(dataList, batchIndex, batchSize);
+        } else {
+            int size = dataList.size();
+            int len = batchSize;
+            int count = (size + len - 1) / len;
+
+            for (int i = 0; i < count; i++) {
+                List<P> subList = dataList.subList(i * len, ((i + 1) * len > size ? size : len * (i + 1)));
+                setData(subList);
+                afterBatch(subList, batchIndex, batchSize);
+                batchIndex++;
+            }
+        }
+    }
+
+    protected void afterBatch(List<P> dataList, int batchIndex, int batchSize) {
+
+    }
+
+    protected abstract void setData(List<P> dataList);
+}

+ 200 - 0
src/main/java/cn/com/qmth/am/utils/Calculator.java

@@ -0,0 +1,200 @@
+package cn.com.qmth.am.utils;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.math3.stat.StatUtils;
+import org.apache.commons.math3.stat.correlation.PearsonsCorrelation;
+
+import com.qmth.boot.core.exception.StatusException;
+
+/**
+ * 计算器
+ *
+ * @author
+ * @date 2019年7月30日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class Calculator {
+    public static double mean(double[] values) {
+        if (values.length == 0) {
+            return 0;
+        }
+        return StatUtils.mean(values);
+    }
+    public static double correlation(double[] x, double[] y) {
+        if (x.length != y.length) {
+            throw new StatusException("两组样本数据数量大小不一致!");
+        }
+
+        if (x.length < 2) {
+            // throw new StatusException("样本数据数量过少!");
+            return 0d;
+        }
+
+        double r = new PearsonsCorrelation().correlation(x, y);
+        if (Double.isNaN(r)) {
+            return 0;
+        }
+        return r;
+    }
+    /**
+     * 加法 保留两位小数
+     * 
+     * @param v1
+     * @param v2
+     * @return
+     */
+    public static double add(double v1, double v2) {
+        return add(v1, v2, 2);
+
+    }
+
+    /**
+     * 加法 保留指定位小数
+     * 
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double add(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        return b1.add(b2).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+
+    }
+    
+    /** 加法 保留指定位小数
+     * @param ds
+     * @param len
+     * @return
+     */
+    public static double add(List<Double> ds, int len) {
+        if(CollectionUtils.isEmpty(ds)) {
+            throw new StatusException("数组为空");
+        }
+        BigDecimal ret = new BigDecimal(0.0);
+        for(Double d:ds) {
+            if(d==null) {
+                throw new StatusException("数组元素为空");
+            }
+            ret=ret.add(new BigDecimal(d));
+        }
+        return ret.setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+
+    }
+
+    /**
+     * 减法 保留两位小数
+     * 
+     * @param v1
+     * @param v2
+     * @return
+     */
+    public static double subtract(double v1, double v2) {
+        return subtract(v1, v2, 2);
+
+    }
+
+    /**
+     * 减法 保留指定位小数
+     * 
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double subtract(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        return b1.subtract(b2).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+
+    }
+    /**差值绝对值
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double absoluteDiff(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        if(v1>v2) {
+            return b1.subtract(b2).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+        }else {
+            return b2.subtract(b1).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+        }
+
+    }
+    /**
+     * 乘法 保留两位小数
+     * 
+     * @param v1
+     * @param v2
+     * @return
+     */
+    public static double multiply(double v1, double v2) {
+        return multiply(v1, v2, 2);
+
+    }
+
+    /**
+     * 乘法 保留指定位小数
+     * 
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double multiply(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        return b1.multiply(b2).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
+
+    }
+    
+    
+    /**
+     * 除法 保留两位小数
+     * 
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double divide(double v1, double v2) {
+        return divide(v1, v2, 2);
+    }
+
+    /**
+     * 除法 保留指定位小数
+     * 
+     * @param v1
+     * @param v2
+     * @param len
+     * @return
+     */
+    public static double divide(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        return b1.divide(b2, len, BigDecimal.ROUND_HALF_UP).doubleValue();
+    }
+
+    public static String divide2String(Double v1, Double v2, int len) {
+        if(v1==null||v2==null||v2==0) {
+            return "-";
+        }
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        return String.valueOf(b1.divide(b2, len, BigDecimal.ROUND_HALF_UP).doubleValue());
+    }
+    
+    public static String percentage(double v1, double v2, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        BigDecimal b2 = new BigDecimal(v2);
+        BigDecimal b3 = new BigDecimal(100);
+        return b1.divide(b2, len+2, BigDecimal.ROUND_HALF_UP).multiply(b3).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue()+"%";
+    }
+}

+ 899 - 0
src/main/java/cn/com/qmth/am/utils/FileUtil.java

@@ -0,0 +1,899 @@
+package cn.com.qmth.am.utils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import sun.misc.BASE64Decoder;
+
+@SuppressWarnings("restriction")
+public class FileUtil {
+
+    public static void deleteDirectory(File dirFile) {
+        if (!dirFile.exists()) {
+            return;
+        }
+        if (dirFile.isFile()) {
+            dirFile.delete();
+        } else {
+            File[] files = dirFile.listFiles();
+            if (files != null) {
+                for (int i = 0; i < files.length; i++) {
+                    deleteDirectory(files[i]);
+                }
+            }
+            dirFile.delete();
+        }
+    }
+
+    public static boolean saveFileByUrl(String fileUrl, File file) {
+        URL url;
+        try {
+            url = new URL(fileUrl);
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("文件链接错误:" + fileUrl, e);
+        }
+
+        HttpURLConnection connection;
+        try {
+            connection = (HttpURLConnection) url.openConnection();
+        } catch (IOException e) {
+            throw new RuntimeException("下载出错", e);
+        }
+
+        try (DataInputStream dataInputStream = new DataInputStream(connection.getInputStream());
+                FileOutputStream fileOutputStream = new FileOutputStream(file);
+                DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);) {
+
+            byte[] buffer = new byte[4096];
+            int count;
+            while ((count = dataInputStream.read(buffer)) > 0) {
+                dataOutputStream.write(buffer, 0, count);
+            }
+            fileOutputStream.flush();
+            dataOutputStream.flush();
+            return true;
+        } catch (Exception e) {
+            throw new RuntimeException("下载出错", e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    public static byte[] base64ToByte(String base64Str) {
+        if (base64Str.contains("data:image")) {// base64图片
+            base64Str = base64Str.substring(base64Str.indexOf(",") + 1).replaceAll("%0A", "");
+            BASE64Decoder decoder = new BASE64Decoder();
+            try {
+                return decoder.decodeBuffer(base64Str);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            throw new RuntimeException("not base64 string");
+        }
+    }
+
+    public static void base64ToFile(File file, String base64Str) {
+        if (base64Str.contains("data:image")) {// base64图片
+            base64Str = base64Str.substring(base64Str.indexOf(",") + 1).replaceAll("%0A", "");
+            BASE64Decoder decoder = new BASE64Decoder();
+            try {
+                byte[] bytes = decoder.decodeBuffer(base64Str);
+                FileUtils.writeByteArrayToFile(file, bytes);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            throw new RuntimeException("not base64 string");
+        }
+    }
+
+    public static String fileToBase64(InputStream is, String suff) {
+        byte[] base64Byte;
+        try {
+            base64Byte = new byte[0];
+            byte[] imgByte;
+            imgByte = IOUtils.toByteArray(is);
+            base64Byte = Base64.encodeBase64(imgByte);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return "data:image/" + suff + ";base64," + new String(base64Byte);
+    }
+
+    public static String byteToBase64(byte[] imgByte, String suff) {
+        InputStream is = null;
+        byte[] base64Byte = new byte[0];
+        try {
+            base64Byte = Base64.encodeBase64(imgByte);
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return "data:image/" + suff + ";base64," + new String(base64Byte);
+    }
+
+    public static String fileToBase64(File imgFile) {
+        String fileName = imgFile.getName();
+        String suff = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+        InputStream is = null;
+        byte[] base64Byte;
+        try {
+            base64Byte = new byte[0];
+            byte[] imgByte;
+            is = new FileInputStream(imgFile);
+            imgByte = IOUtils.toByteArray(is);
+            base64Byte = Base64.encodeBase64(imgByte);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return "data:image/" + suff + ";base64," + new String(base64Byte);
+    }
+
+    /**
+     * 下载服务器上的文件
+     *
+     * @param filename
+     *            文件名称
+     * @param fullFilePath
+     *            文件全路径
+     * @param response
+     */
+    public static void downloadFile(String filename, String fullFilePath, HttpServletResponse response) {
+        try (InputStream in = new FileInputStream(fullFilePath); OutputStream out = response.getOutputStream();) {
+
+            // 设置编码
+            response.setCharacterEncoding("UTF-8");
+
+            // 设置Content-Disposition,名称强制为UTF-8
+            response.setHeader("Content-Disposition", "attachment;filename="
+                    + URLEncoder.encode(filename, "UTF-8").replace("%28", "(").replace("%29", ")"));
+            response.setHeader("Accept-Length", String.valueOf(in.available()));
+
+            // 设置强制下载不打开
+            response.setContentType("application/octet-stream;charset=utf-8");
+
+            // 读取目标文件,通过response将目标文件写到客户端
+            byte[] buffer = new byte[4096];
+            int count;
+            while ((count = in.read(buffer)) > 0) {
+                out.write(buffer, 0, count);
+            }
+
+            response.flushBuffer();
+        } catch (IOException e) {
+            throw new RuntimeException("下载出错:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 获得文件MIME类型
+     *
+     * @param filename
+     * @return
+     */
+    public static String getContentType(String filename) {
+        String type = null;
+        Path path = Paths.get(filename);
+        try {
+            type = Files.probeContentType(path);
+        } catch (IOException e) {
+            throw new RuntimeException("出错:" + e.getMessage(), e);
+        }
+        return type;
+    }
+
+    /**
+     * 将存放在sourceFilePath目录下的源文件,打包成fileName名称的zip文件,并存放到zipFilePath路径下
+     *
+     * @param sourceFilePath
+     *            :待压缩的文件夹路径
+     * @param zipFilePath
+     *            :压缩后zip文件的存放路径
+     * @param fileName
+     *            :zip文件的名称
+     * @return
+     */
+    public static void fileToZip(String sourceFilePath, String zipFilePath, String fileName) {
+
+        File sourceFile = new File(sourceFilePath);
+        if (!sourceFile.exists()) {
+            throw new RuntimeException("待压缩的文件目录:" + sourceFilePath + "不存在.");
+        }
+
+        File zipFile = new File(zipFilePath + File.separator + fileName + ".zip");
+        if (zipFile.exists()) {
+            throw new RuntimeException(zipFilePath + "目录下存在名字为:" + fileName + ".zip" + "打包文件.");
+        }
+
+        File[] sourceFiles = sourceFile.listFiles();
+        if (null == sourceFiles || sourceFiles.length < 1) {
+            throw new RuntimeException("待压缩的文件目录:" + sourceFilePath + "里面不存在文件,无需压缩.");
+        }
+
+        try (FileOutputStream fos = new FileOutputStream(zipFile);
+                BufferedOutputStream bos = new BufferedOutputStream(fos);
+                ZipOutputStream zos = new ZipOutputStream(bos);) {
+
+            byte[] bytes = new byte[1024 * 10];
+            for (int i = 0; i < sourceFiles.length; i++) {
+                File file = sourceFiles[i];
+                if (!file.isFile()) {
+                    continue;
+                }
+
+                try (FileInputStream fis = new FileInputStream(file);
+                        BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 10);) {
+                    // 创建ZIP实体,并添加进压缩包
+                    String fileEncode = System.getProperty("file.encoding");
+                    String name = new String(file.getName().getBytes(), fileEncode);
+
+                    ZipEntry zipEntry = new ZipEntry(name);
+                    zos.putNextEntry(zipEntry);
+
+                    // 读取待压缩的文件并写进压缩包里
+                    int read;
+                    while ((read = bis.read(bytes, 0, 1024 * 10)) != -1) {
+                        zos.write(bytes, 0, read);
+                    }
+
+                    zos.flush();
+                } catch (Exception e) {
+                    throw new RuntimeException("出错:" + e.getMessage(), e);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("出错:" + e.getMessage(), e);
+        }
+    }
+
+    public static void createDirectory(String downloadDirectory) {
+        File directory = new File(downloadDirectory);
+        if (!directory.exists()) {
+            directory.mkdirs();
+        } else {
+            FileUtils.deleteQuietly(directory);
+            directory.mkdirs();
+        }
+    }
+
+    public static File createZip(String sourceFilePath, String targetFilePath) throws IOException {
+        OutputStream fos = null;
+        ZipOutputStream zos = null;
+        try {
+            File zipfile = new File(targetFilePath);
+            zipfile.deleteOnExit();
+            fos = new FileOutputStream(zipfile);
+            zos = new ZipOutputStream(fos);
+            // zos.setEncoding("utf-8"); // Solve linxu's mess
+            writeZip(new File(sourceFilePath), null, zos);
+            return zipfile;
+        } finally {
+            if (zos != null) {
+                zos.close();
+            }
+            if (fos != null) {
+                fos.close();
+            }
+        }
+    }
+
+    private static void writeZip(File file, String parentPath, ZipOutputStream zos) throws IOException {
+        if (file.exists()) {
+            ZipEntry ze = null;
+            if (file.isDirectory()) {// Processing folder
+                if (parentPath == null) {
+                    parentPath = "";
+                } else {
+                    parentPath += file.getName() + "/";
+                }
+                File[] files = file.listFiles();
+                if (files != null) {
+                    for (File f : files) {
+                        writeZip(f, parentPath, zos);
+                    }
+                } else { // An empty directory creates the current directory
+                    try {
+                        ze = new ZipEntry(parentPath);
+                        // ze.setUnixMode(755);// Solve Linux mess file Settings
+                        // 644 directory Settings 755
+                        zos.putNextEntry(ze);
+
+                        zos.flush();
+                    } finally {
+                        if (zos != null) {
+                            zos.closeEntry();
+                        }
+                    }
+                }
+            } else {
+                FileInputStream fis = null;
+                try {
+                    fis = new FileInputStream(file);
+                    ze = new ZipEntry(parentPath + file.getName());
+                    // ze.setUnixMode(644);// Solve Linux mess file Settings 644
+                    // directory Settings 755
+                    zos.putNextEntry(ze);
+                    byte[] content = new byte[1024];
+                    int len;
+                    while ((len = fis.read(content)) != -1) {
+                        zos.write(content, 0, len);
+                        zos.flush();
+                    }
+
+                } finally {
+                    if (fis != null) {
+                        fis.close();
+                    }
+                    if (zos != null) {
+                        zos.closeEntry();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 文件压缩
+     *
+     * @param target
+     *            目录或文件
+     * @param zipFile
+     *            压缩后的ZIP文件
+     */
+    public static boolean doZip(File target, File zipFile) {
+        if (target == null || !target.exists()) {
+            throw new RuntimeException("目录或文件不能为空!");
+        }
+
+        if (zipFile == null) {
+            throw new RuntimeException("待压缩的文件不能为空!");
+        }
+
+        try (OutputStream outStream = new FileOutputStream(zipFile);
+                ZipOutputStream zipOutStream = new ZipOutputStream(outStream, Charset.forName("UTF-8"));) {
+            if (!zipFile.exists()) {
+                boolean ok = zipFile.createNewFile();
+                if (!ok) {
+                    throw new RuntimeException("压缩的文件创建失败!");
+                }
+            }
+
+            if (target.isDirectory()) {
+                File[] files = target.listFiles();
+                if (files.length == 0) {
+                    throw new RuntimeException("文件夹内未找到任何文件!");
+                }
+
+                for (File file : files) {
+                    doZip(zipOutStream, file, null);
+                }
+            } else {
+                doZip(zipOutStream, target, null);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        return true;
+    }
+
+    private static void doZip(ZipOutputStream zipOutStream, File target, String parentDir) throws IOException {
+        // log.info("Zip:" + parentDir);
+        if (parentDir == null) {
+            parentDir = "";
+        }
+
+        if (!"".equals(parentDir) && !parentDir.endsWith(File.separator)) {
+            parentDir += File.separator;
+        }
+
+        if (target.isDirectory()) {
+            File[] files = target.listFiles();
+            if (files.length > 0) {
+                for (File file : files) {
+                    doZip(zipOutStream, file, parentDir + target.getName());
+                }
+            } else {
+                zipOutStream.putNextEntry(new ZipEntry(parentDir + target.getName()));
+                zipOutStream.closeEntry();
+            }
+        } else {
+            try (InputStream is = new FileInputStream(target);) {
+                zipOutStream.putNextEntry(new ZipEntry(parentDir + target.getName()));
+                int len;
+                byte[] bytes = new byte[1024];
+                while ((len = is.read(bytes)) > 0) {
+                    zipOutStream.write(bytes, 0, len);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            zipOutStream.closeEntry();
+        }
+    }
+
+    /**
+     * 解压文件
+     *
+     * @param targetDir
+     *            解压目录
+     * @param zipFile
+     *            待解压的ZIP文件
+     */
+    public static List<File> unZip(File targetDir, File zipFile) {
+        if (targetDir == null) {
+            throw new RuntimeException("解压目录不能为空!");
+        }
+
+        if (zipFile == null) {
+            throw new RuntimeException("待解压的文件不能为空!");
+        }
+
+        if (!zipFile.exists()) {
+            throw new RuntimeException("待解压的文件不存在!" + zipFile.getAbsolutePath());
+        }
+
+        String zipName = zipFile.getName().toLowerCase();
+        if (zipFile.isDirectory() || zipName.indexOf(".zip") < 0) {
+            throw new RuntimeException("待解压的文件格式错误!");
+        }
+
+        if (!targetDir.exists()) {
+            targetDir.mkdir();
+        }
+
+        List<File> result = new LinkedList<>();
+
+        try (ZipFile zip = new ZipFile(zipFile, Charset.forName("GBK"));) {
+
+            @SuppressWarnings("rawtypes")
+            Enumeration entries = zip.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = (ZipEntry) entries.nextElement();
+
+                // Linux中需要替换掉路径的反斜杠
+                String entryName = (File.separator + entry.getName()).replaceAll("\\\\", "/");
+
+                String filePath = targetDir.getAbsolutePath() + entryName;
+                File target = new File(filePath);
+                if (entry.isDirectory()) {
+                    target.mkdirs();
+                } else {
+                    File dir = target.getParentFile();
+                    if (!dir.exists()) {
+                        dir.mkdirs();
+                    }
+
+                    try (OutputStream os = new FileOutputStream(target); InputStream is = zip.getInputStream(entry);) {
+                        IOUtils.copy(is, os);
+                        os.flush();
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                    result.add(target);
+                }
+            }
+
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        return result;
+    }
+
+    public static void unZipFiles(String zipFileName, String targetDirName) throws IOException {
+        if (!targetDirName.endsWith(File.separator)) {
+            targetDirName = targetDirName + File.separator;
+        }
+        ZipFile zipFile = null;
+        try {
+            // Create the ZipFile object from the ZIP file
+            zipFile = new ZipFile(zipFileName);
+            ZipEntry entry = null;
+            String entryName = null;
+            String descFileDir = null;
+            byte[] buf = new byte[4096];
+            int readByte = 0;
+            // Gets all entry in the ZIP file
+            @SuppressWarnings("rawtypes")
+            Enumeration enums = zipFile.entries();
+            // Go through all entry
+            while (enums.hasMoreElements()) {
+                entry = (ZipEntry) enums.nextElement();
+                // Get the name entry
+                entryName = entry.getName();
+                descFileDir = targetDirName + entryName;
+                if (entry.isDirectory()) {
+                    // If entry is a directory, create the directory
+                    // entry.setUnixMode(755);// Solve Linux mess file Settings
+                    // 644 directory Settings 755
+                    new File(descFileDir).mkdirs();
+                    continue;
+                } else {
+                    // If entry is a file, the parent directory is created
+                    // entry.setUnixMode(644);//Solve Linux mess file Settings
+                    // 644 directory Settings 755
+                    new File(descFileDir).getParentFile().mkdirs();
+                }
+                File file = new File(descFileDir);
+                // Open the file output stream
+                OutputStream os = null;
+                // Open the entry input stream from the ZipFile object
+                InputStream is = null;
+                try {
+                    os = new FileOutputStream(file);
+                    is = zipFile.getInputStream(entry);
+                    while ((readByte = is.read(buf)) != -1) {
+                        os.write(buf, 0, readByte);
+                    }
+                } finally {
+                    if (os != null)
+                        os.close();
+                    if (is != null)
+                        is.close();
+                }
+            }
+        } finally {
+            if (zipFile != null)
+                zipFile.close();
+        }
+    }
+
+    public static void deleteFolder(String path) {
+
+        File file = new File(path);
+        if (file.exists()) {
+            if (file.isFile()) {
+                deleteFile(path);
+            } else {
+                deleteDirectory(path);
+            }
+        }
+    }
+
+    public static void deleteFile(String path) {
+        File file = new File(path);
+        if (file.isFile() && file.exists()) {
+            file.delete();
+        }
+    }
+
+    public static void deleteDirectory(String path) {
+        if (!path.endsWith(File.separator)) {
+            path = path + File.separator;
+        }
+        File dirFile = new File(path);
+        if (!dirFile.exists() || !dirFile.isDirectory()) {
+            return;
+        }
+        File[] files = dirFile.listFiles();
+        if (files != null) {
+            for (int i = 0; i < files.length; i++) {
+                if (files[i].isFile()) {
+                    deleteFile(files[i].getAbsolutePath());
+                } else {
+                    deleteDirectory(files[i].getAbsolutePath());
+                }
+            }
+        }
+
+        dirFile.delete();
+    }
+
+    public static File cutFile(String sourcePath, String targetPath, int n) {
+        File file = new File(sourcePath);
+        File newFile = new File(targetPath);
+
+        try (FileInputStream fis = new FileInputStream(file);
+                InputStream is = new BufferedInputStream(fis);
+                OutputStream os = new FileOutputStream(newFile);) {
+
+            // 从n个字节开始读,注意中文是两个字节
+            fis.skip(n);
+
+            // 指定文件位置读取的文件流,存入新文件
+            byte buffer[] = new byte[4 * 1024];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                os.write(buffer, 0, len);
+            }
+
+            os.flush();
+            return newFile;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 读取文件前面部分N个字节
+     *
+     * @param path
+     *            文件路径
+     * @param headerSize
+     *            头信息字节数(必须2的倍数)
+     * @param signSize
+     *            签名信息字节数
+     * @return
+     */
+    public static String[] readFileHeader(String path, int headerSize, int signSize) {
+        int n = headerSize / 2;
+        String[] codes = new String[n + 1];
+
+        File file = new File(path);
+        try (FileInputStream fis = new FileInputStream(file); DataInputStream ois = new DataInputStream(fis);) {
+            // 分n次读取文件(n * 2)个字节
+            for (int i = 0; i < n; i++) {
+                codes[i] = String.valueOf(ois.readShort());
+            }
+
+            if (signSize > 0) {
+                StringBuilder ss = new StringBuilder();
+                for (int i = 0; i < signSize; i++) {
+                    ss.append((char) ois.readByte());
+                }
+                codes[2] = ss.toString();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        return codes;
+    }
+
+    /**
+     * 读取文件内容
+     *
+     * @param file
+     * @return
+     */
+    @SuppressWarnings("deprecation")
+    public static String readFileContent(File file) {
+        StringBuilder content = new StringBuilder();
+        InputStreamReader streamReader = null;
+        BufferedReader bufferedReader = null;
+        try {
+            String encoding = "UTF-8";
+            if (file.exists() && file.isFile()) {
+                streamReader = new InputStreamReader(new FileInputStream(file), encoding);
+                bufferedReader = new BufferedReader(streamReader);
+                String line;
+                while ((line = bufferedReader.readLine()) != null) {
+                    content.append(line);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            IOUtils.closeQuietly(streamReader);
+            IOUtils.closeQuietly(bufferedReader);
+        }
+        return content.toString();
+    }
+
+    @SuppressWarnings("deprecation")
+    public static String readFileContent(InputStream in) {
+        StringBuilder content = new StringBuilder();
+        InputStreamReader streamReader = null;
+        BufferedReader bufferedReader = null;
+        try {
+            String encoding = "UTF-8";
+            streamReader = new InputStreamReader(in, encoding);
+            bufferedReader = new BufferedReader(streamReader);
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                content.append(line);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            IOUtils.closeQuietly(in);
+            IOUtils.closeQuietly(streamReader);
+            IOUtils.closeQuietly(bufferedReader);
+        }
+        return content.toString();
+    }
+
+    /**
+     * 生成日期目录路径
+     */
+    public static String generateDateDir() {
+        return "/" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + "/";
+    }
+
+    public static String generateFileName() {
+        return UUID.randomUUID().toString().replaceAll("-", "");
+    }
+
+    public static String generateDateName() {
+        return new SimpleDateFormat("yyMMddHHmmss").format(new Date());
+    }
+
+    /**
+     * 获取文件后缀名(包含".")
+     */
+    public static String getFileSuffix(String fileName) {
+        if (fileName == null) {
+            return "";
+        }
+        int index = fileName.lastIndexOf(".");
+        if (index > -1) {
+            return fileName.substring(index).toLowerCase();
+        }
+        return "";
+    }
+
+    /**
+     * 获取无后缀的文件名
+     *
+     * @param fileName
+     *            示例:../xxx/abc.xx
+     * @return 示例:../xxx/abc
+     */
+    public static String getFilePathName(String fileName) {
+        if (fileName != null && fileName.length() > 0) {
+            int index = fileName.lastIndexOf(".");
+            if (index != -1) {
+                return fileName.substring(0, index);
+            }
+        }
+        return "";
+    }
+
+    /**
+     * 创建文件目录
+     */
+    public static boolean makeDirs(String path) {
+        if (path == null || "".equals(path)) {
+            return false;
+        }
+        File folder = new File(path);
+        if (!folder.exists()) {
+            return folder.mkdirs();
+        }
+        return true;
+    }
+
+    public static void dozip(File target, File[] files) {
+        // 压缩后的 zip 文件名
+        if (target == null || !target.exists()) {
+            throw new RuntimeException("目录或文件不能为空!");
+        }
+        try {
+            // 创建 ZipOutputStream 对象
+            FileOutputStream fos = new FileOutputStream(target);
+            ZipOutputStream zipOut = new ZipOutputStream(fos, Charset.forName("UTF-8"));
+
+            // 循环每个文件并将其添加到压缩包
+            for (File vo : files) {
+                FileInputStream in = new FileInputStream(vo);
+                try {
+                    ZipEntry zipEntry = new ZipEntry(vo.getName());
+                    zipOut.putNextEntry(zipEntry);
+                    // 将文件内容写入 ZipOutputStream
+                    byte[] bytes = new byte[1024];
+                    int length;
+                    while ((length = in.read(bytes)) >= 0) {
+                        zipOut.write(bytes, 0, length);
+                    }
+                } finally {
+                    // 关闭当前文件的输入流
+                    in.close();
+                }
+            }
+
+            // 关闭 ZipOutputStream
+            zipOut.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void dozip(File target, List<File> files) {
+        // 压缩后的 zip 文件名
+        if (target == null || !target.exists()) {
+            throw new RuntimeException("目录或文件不能为空!");
+        }
+        try {
+            // 创建 ZipOutputStream 对象
+            FileOutputStream fos = new FileOutputStream(target);
+            ZipOutputStream zipOut = new ZipOutputStream(fos, Charset.forName("UTF-8"));
+
+            // 循环每个文件并将其添加到压缩包
+            for (File vo : files) {
+                FileInputStream in = new FileInputStream(vo);
+                try {
+                    ZipEntry zipEntry = new ZipEntry(vo.getName());
+                    zipOut.putNextEntry(zipEntry);
+                    // 将文件内容写入 ZipOutputStream
+                    byte[] bytes = new byte[1024];
+                    int length;
+                    while ((length = in.read(bytes)) >= 0) {
+                        zipOut.write(bytes, 0, length);
+                    }
+                } finally {
+                    // 关闭当前文件的输入流
+                    in.close();
+                }
+            }
+
+            // 关闭 ZipOutputStream
+            zipOut.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    public static void donwLoadFile(HttpServletResponse response, String fileName, InputStream in) {
+        OutputStream out = null;
+        try {
+            fileName = URLEncoder.encode(fileName, "UTF-8");
+            response.reset();
+            response.setHeader("Content-Disposition", "inline; filename=" + fileName);
+            response.setContentType("application/octet-stream;charset=UTF-8");
+            out = new BufferedOutputStream(response.getOutputStream());
+            IOUtils.copy(in, out);
+            out.flush();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            IOUtils.closeQuietly(out);
+            IOUtils.closeQuietly(in);
+        }
+    }
+}

+ 51 - 0
src/main/java/cn/com/qmth/am/utils/FormFilePart.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.am.utils;
+
+import java.io.File;
+
+public class FormFilePart {
+
+    private String paramName;
+
+    private String filename;
+
+    private File file;
+
+    /**
+     * 构造函数
+     *
+     * @param paramName
+     * @param filename
+     * @param file
+     */
+    public FormFilePart(String paramName, String filename, File file) {
+        super();
+        this.paramName = paramName;
+        this.filename = filename;
+        this.file = file;
+    }
+
+    public String getParamName() {
+        return paramName;
+    }
+
+    public void setParamName(String paramName) {
+        this.paramName = paramName;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    public File getFile() {
+        return file;
+    }
+
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/am/utils/FreeMarkerUtil.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.am.utils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+public class FreeMarkerUtil {
+
+    private static final String ENCODING = "UTF-8";
+
+    private static Configuration config;
+
+    private static Template dsMarkingReq;
+
+    static {
+
+        config = new Configuration(Configuration.VERSION_2_3_25);
+        // 设置编码
+        config.setDefaultEncoding(ENCODING);
+        // 设置ftl模板路径
+        config.setClassForTemplateLoading(FreeMarkerUtil.class, "/templates/");
+
+        try {
+            dsMarkingReq = config.getTemplate("ds_marking_translation.ftl", ENCODING);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String getDsMarkingReq(AutoScoreRequest req) {
+        StringWriter result = null;
+        try {
+            result = new StringWriter();
+            dsMarkingReq.process(req, result);
+            return result.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 108 - 0
src/main/java/cn/com/qmth/am/utils/HttpMethod.java

@@ -0,0 +1,108 @@
+package cn.com.qmth.am.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpMethod {
+
+    public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");
+
+    public static final HttpMethod GET = new HttpMethod("GET");
+
+    public static final HttpMethod HEAD = new HttpMethod("HEAD");
+
+    public static final HttpMethod POST = new HttpMethod("POST");
+
+    public static final HttpMethod PUT = new HttpMethod("PUT");
+
+    public static final HttpMethod PATCH = new HttpMethod("PATCH");
+
+    public static final HttpMethod DELETE = new HttpMethod("DELETE");
+
+    public static final HttpMethod TRACE = new HttpMethod("TRACE");
+
+    public static final HttpMethod CONNECT = new HttpMethod("CONNECT");
+
+    private static final Map<String, HttpMethod> METHOD_MAP = new HashMap<String, HttpMethod>();
+
+    static {
+        METHOD_MAP.put(OPTIONS.toString(), OPTIONS);
+        METHOD_MAP.put(GET.toString(), GET);
+        METHOD_MAP.put(HEAD.toString(), HEAD);
+        METHOD_MAP.put(POST.toString(), POST);
+        METHOD_MAP.put(PUT.toString(), PUT);
+        METHOD_MAP.put(PATCH.toString(), PATCH);
+        METHOD_MAP.put(DELETE.toString(), DELETE);
+        METHOD_MAP.put(TRACE.toString(), TRACE);
+        METHOD_MAP.put(CONNECT.toString(), CONNECT);
+    }
+
+    private final String name;
+
+    /**
+     * 构造函数
+     *
+     * @param name
+     */
+    private HttpMethod(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+
+        name = name.trim();
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("empty name");
+        }
+
+        for (int i = 0; i < name.length(); i++) {
+            if (Character.isISOControl(name.charAt(i)) || Character.isWhitespace(name.charAt(i))) {
+                throw new IllegalArgumentException("invalid character in name");
+            }
+        }
+
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public static HttpMethod valueOf(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+
+        name = name.trim();
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("empty name");
+        }
+
+        HttpMethod result = METHOD_MAP.get(name);
+        if (result != null) {
+            return result;
+        } else {
+            throw new IllegalArgumentException("undefined name");
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return getName().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof HttpMethod)) {
+            return false;
+        }
+
+        HttpMethod that = (HttpMethod) o;
+        return getName().equals(that.getName());
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+}

+ 376 - 0
src/main/java/cn/com/qmth/am/utils/ImageUtil.java

@@ -0,0 +1,376 @@
+package cn.com.qmth.am.utils;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
+
+public final class ImageUtil {
+
+	/**
+	 * 图片水印
+	 * 
+	 * @param pressImg  水印图片
+	 * @param targetImg 目标图片
+	 * @param x         修正值 默认在中间
+	 * @param y         修正值 默认在中间
+	 * @param alpha     透明度
+	 */
+	public final static void pressImage(String pressImg, String targetImg, int x, int y, float alpha) {
+		try {
+			File img = new File(targetImg);
+			Image src = ImageIO.read(img);
+			int wideth = src.getWidth(null);
+			int height = src.getHeight(null);
+			BufferedImage image = new BufferedImage(wideth, height, BufferedImage.TYPE_INT_RGB);
+			Graphics2D g = image.createGraphics();
+			g.drawImage(src, 0, 0, wideth, height, null);
+			// 水印文件
+			Image src_biao = ImageIO.read(new File(pressImg));
+			int wideth_biao = src_biao.getWidth(null);
+			int height_biao = src_biao.getHeight(null);
+			g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+			g.drawImage(src_biao, (wideth - wideth_biao) / 2, (height - height_biao) / 2, wideth_biao, height_biao,
+					null);
+			// 水印文件结束
+			g.dispose();
+			ImageIO.write((BufferedImage) image, "jpg", img);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * 文字水印
+	 * 
+	 * @param pressText 水印文字
+	 * @param targetImg 目标图片
+	 * @param fontName  字体名称
+	 * @param fontStyle 字体样式
+	 * @param color     字体颜色
+	 * @param fontSize  字体大小
+	 * @param x         修正值
+	 * @param y         修正值
+	 * @param alpha     透明度
+	 */
+	public static void pressText(String pressText, String targetImg, String fontName, int fontStyle, Color color,
+			int fontSize, int x, int y, float alpha) {
+		try {
+			File img = new File(targetImg);
+			Image src = ImageIO.read(img);
+			int width = src.getWidth(null);
+			int height = src.getHeight(null);
+			BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+			Graphics2D g = image.createGraphics();
+			g.drawImage(src, 0, 0, width, height, null);
+			g.setColor(color);
+			g.setFont(new Font(fontName, fontStyle, fontSize));
+			g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+			g.drawString(pressText, (width - (getLength(pressText) * fontSize)) / 2 + x, (height - fontSize) / 2 + y);
+			g.dispose();
+			ImageIO.write((BufferedImage) image, "jpg", img);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * 缩放
+	 * 
+	 * @param filePath    图片路径
+	 * @param newFileName 缩放完比图片路径
+	 * @param height      高度
+	 * @param width       宽度
+	 * @param bb          比例不对时是否需要补白
+	 */
+	@SuppressWarnings("static-access")
+	public static void resize(String filePath, String newFileName, int height, int width, String formatName,
+			boolean bb) {
+		try {
+			double ratio = 0.0; // 缩放比例
+			File f = new File(filePath);
+			BufferedImage bi = ImageIO.read(f);
+			Image itemp = bi.getScaledInstance(width, height, bi.SCALE_SMOOTH);
+			// 计算比例
+			if ((bi.getHeight() > height) || (bi.getWidth() > width)) {
+				if (bi.getHeight() > bi.getWidth()) {
+					ratio = (new Integer(height)).doubleValue() / bi.getHeight();
+				} else {
+					ratio = (new Integer(width)).doubleValue() / bi.getWidth();
+				}
+				AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio), null);
+				itemp = op.filter(bi, null);
+			}
+			if (bb) {
+				BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+				Graphics2D g = image.createGraphics();
+				g.setColor(Color.white);
+				g.fillRect(0, 0, width, height);
+				if (width == itemp.getWidth(null))
+					g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2, itemp.getWidth(null),
+							itemp.getHeight(null), Color.white, null);
+				else
+					g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0, itemp.getWidth(null),
+							itemp.getHeight(null), Color.white, null);
+				g.dispose();
+				itemp = image;
+			}
+			BufferedImage outbi = null;
+			if (itemp instanceof BufferedImage)
+				outbi = (BufferedImage) itemp;
+			else
+				outbi = convertImageToBuffer(itemp);
+
+			File newFile = null;
+			if (newFileName == null || newFileName.length() == 0) {
+				newFile = f;
+			} else {
+				newFile = new File(newFileName);
+				if (!newFile.getParentFile().exists())
+					newFile.getParentFile().mkdirs();
+			}
+			ImageIO.write(outbi, formatName, newFile);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * 对流缩放
+	 * 
+	 * @param input      图片流
+	 * @param formatName 图片格式
+	 * @param height     高度
+	 * @param width      宽度
+	 * @param bb         比例不对时是否需要补白
+	 */
+	@SuppressWarnings("static-access")
+	public static InputStream MemoryResize(InputStream input, String formatName, int height, int width, boolean bb) {
+		try {
+			double ratio = 0.0; // 缩放比例
+			BufferedImage bi = ImageIO.read(input);
+			Image itemp = bi.getScaledInstance(width, height, bi.SCALE_SMOOTH);
+			// 计算比例
+			if ((bi.getHeight() > height) || (bi.getWidth() > width)) {
+				if (bi.getHeight() > bi.getWidth()) {
+					ratio = (new Integer(height)).doubleValue() / bi.getHeight();
+				} else {
+					ratio = (new Integer(width)).doubleValue() / bi.getWidth();
+				}
+				AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio), null);
+				itemp = op.filter(bi, null);
+			}
+			if (bb) {
+				BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+				Graphics2D g = image.createGraphics();
+				g.setColor(Color.white);
+				g.fillRect(0, 0, width, height);
+				if (width == itemp.getWidth(null))
+					g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2, itemp.getWidth(null),
+							itemp.getHeight(null), Color.white, null);
+				else
+					g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0, itemp.getWidth(null),
+							itemp.getHeight(null), Color.white, null);
+				g.dispose();
+				itemp = image;
+			}
+			ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+			BufferedImage outbi = null;
+			if (itemp instanceof BufferedImage)
+				outbi = (BufferedImage) itemp;
+			else
+				outbi = convertImageToBuffer(itemp);
+
+			ImageIO.write(outbi, formatName, output);
+			byte[] buff = output.toByteArray();
+			return new ByteArrayInputStream(buff);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+
+	public static byte[] cutImage(byte[] img, String suff, int x,int y, int width, int height) {
+		InputStream in = null;
+		BufferedImage bi = null;
+		ByteArrayOutputStream output = null;
+		try {
+			in = new ByteArrayInputStream(img);
+			bi = ImageIO.read(in);
+			height = Math.min(height, bi.getHeight());
+			width = Math.min(width, bi.getWidth());
+			if (height <= 0)
+				height = bi.getHeight();
+			if (width <= 0)
+				width = bi.getWidth();
+			x = Math.min(Math.max(0, x), bi.getWidth() - width);
+			y = Math.min(Math.max(0, y), bi.getHeight() - height);
+
+			BufferedImage bi_cropper = bi.getSubimage(x, y, width, height);
+			output = new ByteArrayOutputStream();
+			ImageIO.write(bi_cropper, suff, output);
+			byte[] ret = output.toByteArray();
+			return ret;
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		} finally {
+			try {
+				if (in != null) {
+					in.close();
+				}
+			} catch (IOException e) {
+			}
+			try {
+				if (output != null) {
+					output.close();
+				}
+			} catch (IOException e) {
+			}
+		}
+	}
+
+	public static int getLength(String text) {
+		int length = 0;
+		for (int i = 0; i < text.length(); i++) {
+			if (new String(text.charAt(i) + "").getBytes().length > 1) {
+				length += 2;
+			} else {
+				length += 1;
+			}
+		}
+		return length / 2;
+	}
+
+	public static String getExtension(File f) {
+		return (f != null) ? getExtension(f.getName()) : "";
+	}
+
+	public static String getExtension(String filename) {
+		return getExtension(filename, "");
+	}
+
+	public static String getExtension(String filename, String defExt) {
+		if ((filename != null) && (filename.length() > 0)) {
+			int i = filename.lastIndexOf('.');
+
+			if ((i > -1) && (i < (filename.length() - 1))) {
+				return filename.substring(i + 1);
+			}
+		}
+		return defExt;
+	}
+
+	public static String trimExtension(String filename) {
+		if ((filename != null) && (filename.length() > 0)) {
+			int i = filename.lastIndexOf('.');
+			if ((i > -1) && (i < (filename.length()))) {
+				return filename.substring(0, i);
+			}
+		}
+		return filename;
+	}
+
+	/**
+	 * 转化Image 为 BufferedImage
+	 * 
+	 * @param img
+	 * @return
+	 */
+	private static BufferedImage convertImageToBuffer(Image img) {
+		BufferedImage bufferedImage = new BufferedImage(img.getWidth(null), img.getHeight(null),
+				BufferedImage.TYPE_INT_RGB);
+		Graphics g = bufferedImage.createGraphics();
+		g.drawImage(img, 0, 0, null);
+		g.dispose();
+		return bufferedImage;
+	}
+
+	// 拼接图片
+	public static byte[] joinImages(List<byte[]> slices, String suff) {
+		boolean horizontal = false;
+		List<BufferedImage> imageList = new ArrayList<>();
+		for (byte[] slice : slices) {
+			InputStream in = null;
+			try {
+				in = new ByteArrayInputStream(slice);
+				BufferedImage image = ImageIO.read(in);
+				imageList.add(image);
+			} catch (Exception e) {
+				throw new RuntimeException(e);
+			} finally {
+				try {
+					if (in != null) {
+						in.close();
+					}
+				} catch (IOException e) {
+				}
+			}
+		}
+		int height = imageList.get(0).getHeight();
+		int width = imageList.get(0).getWidth();
+		if (horizontal) {
+			height = imageList.stream().mapToInt(BufferedImage::getHeight).max().getAsInt();
+			width = imageList.stream().mapToInt(BufferedImage::getWidth).sum();
+		} else {
+			width = imageList.stream().mapToInt(BufferedImage::getWidth).max().getAsInt();
+			height = imageList.stream().mapToInt(BufferedImage::getHeight).sum();
+		}
+		// 创建拼接后的图片画布,参数分别为宽,高,类型,这里我们使用RGB3元色类型
+		BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+		Graphics graphics = resultImage.getGraphics();
+		int previousWidth = 0;
+		int previousHeight = 0;
+		for (BufferedImage image : imageList) {
+			// 向画布上画图片
+			graphics.drawImage(image, previousWidth, previousHeight, null);
+			if (horizontal) {
+				previousWidth += image.getWidth();
+			} else {
+				previousHeight += image.getHeight();
+			}
+		}
+		ByteArrayOutputStream output = new ByteArrayOutputStream();
+		try {
+			Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(suff);
+			ImageWriter imageWriter = iter.next();
+			JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(Locale.getDefault());
+			jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+			jpegParams.setCompressionQuality(1.0F);
+			imageWriter.setOutput(ImageIO.createImageOutputStream(output));
+			imageWriter.write(null, new IIOImage(resultImage, null, null), jpegParams);
+			byte[] ret = output.toByteArray();
+			return ret;
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		} finally {
+			try {
+				if (output != null) {
+					output.close();
+				}
+			} catch (IOException e) {
+			}
+		}
+	}
+}

+ 96 - 0
src/main/java/cn/com/qmth/am/utils/JsonHelper.java

@@ -0,0 +1,96 @@
+package cn.com.qmth.am.utils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.exception.StatusException;
+
+public class JsonHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(JsonHelper.class);
+
+    public static String toJson(Object object) {
+        if (object == null) {
+            return null;
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+            return mapper.writeValueAsString(object);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("JSON转换失败!");
+        }
+    }
+
+    public static <T> T toObj(String jsonStr, Class<T> clazz) {
+        if (StringUtils.isEmpty(jsonStr)) {
+            return null;
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+            return mapper.readValue(jsonStr, clazz);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("JSON转换失败!");
+        }
+    }
+
+    public static <T> List<T> toList(String jsonStr, Class<T> clazz) {
+        if (StringUtils.isEmpty(jsonStr)) {
+            return null;
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+            JavaType javaType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
+            return mapper.readValue(jsonStr, javaType);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("JSON转换失败!");
+        }
+    }
+
+    public static <T> Map<String, T> toMap(String jsonStr, Class<T> clazz) {
+        if (StringUtils.isEmpty(jsonStr)) {
+            return null;
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            JavaType javaType = mapper.getTypeFactory().constructMapType(HashMap.class, String.class, clazz);
+            return mapper.readValue(jsonStr, javaType);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("JSON转换失败!");
+        }
+    }
+
+    public static JsonNode getNode(String jsonStr) {
+        if (StringUtils.isEmpty(jsonStr)) {
+            return null;
+        }
+
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            return mapper.readTree(jsonStr);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("JSON转换失败!");
+        }
+    }
+
+}

+ 88 - 0
src/main/java/cn/com/qmth/am/utils/MD5Util.java

@@ -0,0 +1,88 @@
+package cn.com.qmth.am.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+/**
+ * @Description: MD5加密工具类
+ */
+public class MD5Util {
+
+    /**
+     * MD5加密
+     *
+     * @param text
+     * @return
+     * @throws Exception
+     */
+    public static String encoder(String text) throws NoSuchAlgorithmException {
+        text = Optional.of(text).get();
+        MessageDigest digest = MessageDigest.getInstance("MD5");
+        digest.update(text.getBytes(Charset.forName("UTF-8")));
+        byte s[] = digest.digest();
+        StringJoiner result = new StringJoiner("");
+        for (int i = 0; i < s.length; i++) {
+            result.add(Integer.toHexString((0x000000FF & s[i]) | 0xFFFFFF00).substring(6));
+        }
+        return result.toString();
+    }
+
+    /**
+     * MD5校验
+     *
+     * @param text
+     * @param md5
+     * @return
+     * @throws Exception
+     */
+    public static boolean verify(String text, String md5) throws NoSuchAlgorithmException {
+        text = Optional.of(text).get();
+        md5 = Optional.of(md5).get();
+        //根据传入的密钥进行验证
+        String md5Text = encoder(text);
+        if (md5Text.equalsIgnoreCase(md5)) {
+            return true;
+        }
+        return false;
+    }
+    
+	public static String md5Hex(File file) {
+		FileInputStream in = null;
+		try {
+			in = new FileInputStream(file);
+			return DigestUtils.md5Hex(in);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		} finally {
+			if(in!=null)
+				try {
+					in.close();
+				} catch (IOException e) {
+				}
+		}
+	}
+	
+	public static String md5Hex(InputStream in) {
+		try {
+			return DigestUtils.md5Hex(in);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		} finally {
+			if(in!=null)
+				try {
+					in.close();
+				} catch (IOException e) {
+				}
+		}
+	}
+}
+

+ 277 - 0
src/main/java/cn/com/qmth/am/utils/OKHttpUtil.java

@@ -0,0 +1,277 @@
+package cn.com.qmth.am.utils;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.collections4.CollectionUtils;
+
+import okhttp3.FormBody;
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Request.Builder;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class OKHttpUtil {
+
+    public static final class MediaTypes {
+
+        public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+    }
+
+    public static interface RequestBodyBuilder {
+
+        RequestBody build();
+    }
+
+    /**
+     * json请求体构建器
+     *
+     * @author
+     * @date 2019年4月10日
+     * @Copyright (c) 2018-2020 [QQ:522080330] All Rights Reserved.
+     */
+    public static final class JsonBodyBuilder implements RequestBodyBuilder {
+
+        private String json;
+
+        public JsonBodyBuilder(String json) {
+            super();
+            this.json = json;
+        }
+
+        @Override
+        public RequestBody build() {
+            return RequestBody.create(MediaTypes.JSON, json);
+        }
+
+        @Override
+        public String toString() {
+            return json;
+        }
+    }
+
+    private static OkHttpClient okHttpClient;
+
+    // public static void initOkHttpClient() {
+    static {
+        okHttpClient = new OkHttpClient.Builder()
+                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
+                .hostnameVerifier(SSLSocketClient.getHostnameVerifier()).readTimeout(2000, TimeUnit.SECONDS)
+                .writeTimeout(2000, TimeUnit.SECONDS).build();
+    }
+
+    public static OkHttpClient getOkHttpClient() {
+        return okHttpClient;
+    }
+
+    public static void close() {
+        okHttpClient.connectionPool().evictAll();
+    }
+
+    /**
+     * 发送请求 (带json请求体)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param jsonBody
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers, String jsonBody) {
+        return call(httpMethod, url, headers, new JsonBodyBuilder(jsonBody));
+    }
+
+    /**
+     * 发送请求 (带请求体)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param requestBodyBuilder
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            RequestBodyBuilder requestBodyBuilder) {
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(requestBodyBuilder.build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url) {
+        return call(httpMethod, url, null);
+    }
+
+    /**
+     * 发送请求
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers) {
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.GET)) {
+            builder = new Request.Builder().url(url);
+        } else if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(new FormBody.Builder().build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (表单)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            Map<String, String> params) {
+
+        okhttp3.FormBody.Builder formBody = new FormBody.Builder();
+
+        if (null != params && 0 != params.size()) {
+            for (Entry<String, String> entry : params.entrySet()) {
+                formBody.add(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(formBody.build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(formBody.build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(formBody.build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (包含文件表单)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @param formFilePartList
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            Map<String, String> params, List<FormFilePart> formFilePartList) {
+
+        okhttp3.MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
+                .setType(MultipartBody.ALTERNATIVE);
+
+        if (null != params) {
+            for (Entry<String, String> entry : params.entrySet()) {
+                multipartBodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(formFilePartList)) {
+            MediaType type = MediaType.parse("application/octet-stream");
+            for (FormFilePart part : formFilePartList) {
+                RequestBody fileBody = RequestBody.create(type, part.getFile());
+                multipartBodyBuilder.addFormDataPart(part.getParamName(), part.getFilename(), fileBody);
+            }
+        }
+
+        Builder builder = new Request.Builder().url(url).post(multipartBodyBuilder.build());
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 64 - 0
src/main/java/cn/com/qmth/am/utils/ResouceUtil.java

@@ -0,0 +1,64 @@
+package cn.com.qmth.am.utils;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+import org.apache.commons.io.IOUtils;
+
+public class ResouceUtil {
+	public static URL getUrl(String path) {
+		try {
+			ClassLoader classLoader = ResouceUtil.class.getClassLoader();
+
+			URL url = classLoader.getResource(path);
+			return url;
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+	}
+	public static InputStream getStream(String path) {
+		try {
+			ClassLoader classLoader = ResouceUtil.class.getClassLoader();
+
+			URL url = classLoader.getResource(path);
+			return url.openStream();
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+	}
+	public static String getContent(String path) {
+		try {
+			ClassLoader classLoader = ResouceUtil.class.getClassLoader();
+
+			URL url = classLoader.getResource(path);
+			return readFileContent(url.openStream());
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+	}
+	
+	@SuppressWarnings("deprecation")
+	private static String readFileContent(InputStream in) {
+		StringBuilder content = new StringBuilder();
+		InputStreamReader streamReader = null;
+		BufferedReader bufferedReader = null;
+		try {
+			String encoding = "UTF-8";
+			streamReader = new InputStreamReader(in, encoding);
+			bufferedReader = new BufferedReader(streamReader);
+			String line;
+			while ((line = bufferedReader.readLine()) != null) {
+				content.append(line);
+			}
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(in);
+			IOUtils.closeQuietly(streamReader);
+			IOUtils.closeQuietly(bufferedReader);
+		}
+		return content.toString();
+	}
+}

+ 63 - 0
src/main/java/cn/com/qmth/am/utils/SSLSocketClient.java

@@ -0,0 +1,63 @@
+package cn.com.qmth.am.utils;
+
+import javax.net.ssl.*;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+public class SSLSocketClient {
+
+    // 获取这个SSLSocketFactory
+    public static SSLSocketFactory getSSLSocketFactory() {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("SSL");
+            sslContext.init(null, getTrustManager(), new SecureRandom());
+            return sslContext.getSocketFactory();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 获取TrustManager
+    private static TrustManager[] getTrustManager() {
+        return new TrustManager[] { new X509TrustManager() {
+
+            @Override
+            public void checkClientTrusted(X509Certificate[] chain, String authType) {
+            }
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] chain, String authType) {
+            }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[] {};
+            }
+        } };
+    }
+
+    // 获取HostnameVerifier
+    public static HostnameVerifier getHostnameVerifier() {
+        return (s, sslSession) -> true;
+    }
+
+    public static X509TrustManager getX509TrustManager() {
+        X509TrustManager trustManager = null;
+        try {
+            TrustManagerFactory trustManagerFactory = TrustManagerFactory
+                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            trustManagerFactory.init((KeyStore) null);
+            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+                throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
+            }
+            trustManager = (X509TrustManager) trustManagers[0];
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return trustManager;
+    }
+}

+ 74 - 0
src/main/java/cn/com/qmth/am/utils/SpringContextHolder.java

@@ -0,0 +1,74 @@
+package cn.com.qmth.am.utils;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.ResolvableType;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring context holder.
+ *
+ */
+@Component
+public class SpringContextHolder implements ApplicationContextAware {
+
+	private static ApplicationContext ctx = null;
+
+	@Override
+	public void setApplicationContext(ApplicationContext ctx) {
+		SpringContextHolder.ctx = ctx;
+	}
+
+	public static ApplicationContext getApplicationContext() {
+		return ctx;
+	}
+
+	public static Object getBean(String name) {
+		return ctx.getBean(name);
+	}
+
+	public static <T> T getBean(String name, Class<T> requiredType) {
+		return ctx.getBean(name, requiredType);
+	}
+
+	public static <T> T getBean(Class<T> requiredType) {
+		return ctx.getBean(requiredType);
+	}
+
+	public static Object getBean(String name, Object... args) {
+		return ctx.getBean(name, args);
+	}
+
+	public static <T> T getBean(Class<T> requiredType, Object... args) {
+		return ctx.getBean(requiredType, args);
+	}
+
+	public static boolean containsBean(String name) {
+		return ctx.containsBean(name);
+	}
+
+	public static boolean isSingleton(String name) {
+		return ctx.isSingleton(name);
+	}
+
+	public static boolean isPrototype(String name) {
+		return ctx.isPrototype(name);
+	}
+
+	public static boolean isTypeMatch(String name, ResolvableType typeToMatch) {
+		return ctx.isTypeMatch(name, typeToMatch);
+	}
+
+	public static boolean isTypeMatch(String name, Class<?> typeToMatch) {
+		return ctx.isTypeMatch(name, typeToMatch);
+	}
+
+	public static Class<?> getType(String name) {
+		return ctx.getType(name);
+	}
+
+	public static String[] getAliases(String name) {
+		return ctx.getAliases(name);
+	}
+
+}

+ 53 - 0
src/main/resources/application.properties

@@ -0,0 +1,53 @@
+#
+# ********** server config **********
+#
+server.port=8093
+server.servlet.session.timeout=PT2H
+server.servlet.context-path=/
+spring.servlet.multipart.max-request-size=100MB
+spring.servlet.multipart.max-file-size=100MB
+com.qmth.mybatis.log-level=error
+com.qmth.mybatis.block-attack=false
+#
+# ********** db config **********
+#
+db.host=localhost
+db.port=3306
+db.database=ai_marking_en
+com.qmth.datasource.username=root
+com.qmth.datasource.password=123456
+com.qmth.datasource.url=jdbc:mysql://${db.host}:${db.port}/${db.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8&rewriteBatchedStatements=true
+com.qmth.datasource.max-pool-size=200
+com.qmth.datasource.min-idle=10
+#
+# ********** sys config **********
+#
+com.qmth.logging.root-level=info
+com.qmth.logging.file-path=./log/ai-marking.log
+
+
+spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
+spring.jackson.time-zone=GMT+8
+
+com.qmth.solar.app-version=@project.version@
+
+##################################setting##########################################
+com.qmth.llm.server=https://solar.qmth.com.cn
+com.qmth.ocr.server=https://solar.qmth.com.cn
+
+com.qmth.solar.access-key=7bbdc11570bc474dbf50e0d4a5dff328
+com.qmth.solar.access-secret=IOodRvbp2LspJTHOScgB7Yx8MRloMpyl
+
+am.marking-thread-count=8
+am.ocr-task.enable=false
+am.marking-task.enable=false
+am.data-type=MARKING_CLOUD
+am.image-server=https://file.markingcloud.com
+am.data-dir=./data
+am.marking-ocr-model=!!solar
+am.marking-ocr-server=
+am.marking-ocr-key=
+am.marking-marking-model=deepseek-r1-distill-qwen-32b-awq
+am.marking-marking-server=http://39.174.90.3:31091/spiritx-api/v1/chat/completions
+am.marking-marking-key=sk-loBBngbg1ymvUo6f647bF35d69684f1280E5D544F1F59f20
+##################################setting##########################################

+ 5 - 0
src/main/resources/mapper/QuestionMapper.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.com.qmth.am.dao.QuestionDao">
+
+</mapper>

+ 7 - 0
src/main/resources/mapper/StudentScoreMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.com.qmth.am.dao.StudentScoreDao">
+	<select id="getAllList" resultType="cn.com.qmth.am.bean.StudentScoreInfo">
+		select t.question_id,t.student_code from am_student_score t
+	</select>
+</mapper>

+ 23 - 0
src/main/resources/templates/ds_marking.ftl

@@ -0,0 +1,23 @@
+作为"${subjectName}"科目的评分教师,您的任务是依据详细的评分指南,对考生关于特定试题的回答进行全面评估。请按照以下结构化流程进行细致评判:
+
+#### 试题内容:
+${questionBody}
+
+#### 参考答案由${standardAnswer?size}条关键内容组成:
+<#list standardAnswer as item> 
+${item?counter}. ${item.content}
+</#list>
+
+#### 考生回答:
+${studentAnswer}
+
+#### 评判细则:
+- **完整性检查**:对照考生答案与关键内容,确认是否全面包含所有指定要点。
+- **语义通畅性**:分析考生回答是否条理清晰、逻辑连贯。
+- **术语准确性**:考察考生对相关概念与术语使用的恰当程度。
+
+#### 评分操作指引:
+针对每一条关键内容,依据完整性、语义通畅性及术语准确性对考生回答进行细致评判,得分为介于0至100之间的整数,准确反映考生回答质量。
+
+#### 最终输出要求:
+直接输出${standardAnswer?size}条关键内容的评分结果,评分结果无需其他文字说明,各分数间以英文逗号分隔,分数项不要加序号且无需其他文字说明。

+ 28 - 0
src/main/resources/templates/ds_marking_translation.ftl

@@ -0,0 +1,28 @@
+作为${subjectName}科目${questionTitle}试题评分员,请严格按照以下标准为考生作答进行打分:
+
+# 试题内容
+${questionBody}
+
+# 参考答案
+<#list standardAnswer as item> 
+${item.content}
+</#list>
+
+# 评分规则
+5个档次和对应的分数范围如下:
+- **优秀**(14~15): 原文的信息全部传达,语气和文体风格与原文一致;断句恰当,句式正确。选词妥帖。段落之间、句子之间互应自然,有一定文采
+- **良好**(12~13): 除个别次要信息有疏漏之外,原文的重要信息全部传达,语气和文体风格与原文一致;选词较正确、得体。句子组织与安排符合汉语规范
+- **中等**(10~11): 有少量理解错误或个别漏译,但主要精神与原文一致;拘泥于英文的句式,行文不够顺大,但没有重大的选词和句式错误
+- **及格**(9~10): 有个别重大错误或遗漏,部分信息含混,但总体上基本达意;语句不够连贯,行文灰色,有个别重大的选词和句式错误
+- **不及格**(0~8): 误译、漏译较多,不能传达原文主要精神;用词不当,行文不通顺,语言不符合汉语规范
+
+# 评分流程
+1. 理解试题内容与评分规则,分析考生作答内容,对比参考答案,准确判断考生作答属于哪个档次
+2. 从所属档次的分值范围中选择一个合适的分数作为最终评分结果,能准确反映考生作答的情况
+3. 若考生作答仅包含试题名称或试题内容,没有有效作答,直接判为0分
+
+# 考生作答
+${studentAnswer}
+
+# 输出要求:
+直接输出最终评分结果,用数字表示,无需其他文字说明。