xiatian 2 mesiacov pred
commit
9eff6e409e
100 zmenil súbory, kde vykonal 8446 pridanie a 0 odobranie
  1. 53 0
      .gitignore
  2. 49 0
      db/am_db.sql
  3. BIN
      file/question-import.xlsx
  4. 6 0
      file/启动服务.bat
  5. 9 0
      file/接口说明.txt
  6. 29 0
      file/说明.txt
  7. 148 0
      pom.xml
  8. 22 0
      src/main/java/cn/com/qmth/am/AmApplication.java
  9. 38 0
      src/main/java/cn/com/qmth/am/bean/AiMarkingDto.java
  10. 26 0
      src/main/java/cn/com/qmth/am/bean/AnswerImageDto.java
  11. 36 0
      src/main/java/cn/com/qmth/am/bean/DataKey.java
  12. 25 0
      src/main/java/cn/com/qmth/am/bean/ImagePosition.java
  13. 31 0
      src/main/java/cn/com/qmth/am/bean/ImageSize.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. 90 0
      src/main/java/cn/com/qmth/am/bean/ModelSpeed.java
  17. 45 0
      src/main/java/cn/com/qmth/am/bean/OcrDto.java
  18. 35 0
      src/main/java/cn/com/qmth/am/bean/OcrServer.java
  19. 35 0
      src/main/java/cn/com/qmth/am/bean/StudentInfo.java
  20. 64 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreDto.java
  21. 55 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreImageDto.java
  22. 25 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreInfo.java
  23. 134 0
      src/main/java/cn/com/qmth/am/bean/StudentScoreVo.java
  24. 35 0
      src/main/java/cn/com/qmth/am/bean/StudentVo.java
  25. 25 0
      src/main/java/cn/com/qmth/am/bean/TrackPosition.java
  26. 132 0
      src/main/java/cn/com/qmth/am/bean/ds/AutoScoreRequest.java
  27. 24 0
      src/main/java/cn/com/qmth/am/bean/ds/AutoScoreResult.java
  28. 25 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatContent.java
  29. 30 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatReq.java
  30. 17 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatResult.java
  31. 6 0
      src/main/java/cn/com/qmth/am/bean/ds/ChatRole.java
  32. 25 0
      src/main/java/cn/com/qmth/am/bean/ds/DsChoice.java
  33. 26 0
      src/main/java/cn/com/qmth/am/bean/ds/ImageContent.java
  34. 20 0
      src/main/java/cn/com/qmth/am/bean/ds/ImageUrl.java
  35. 31 0
      src/main/java/cn/com/qmth/am/bean/ds/MarkingReq.java
  36. 4 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrContent.java
  37. 28 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrMessage.java
  38. 28 0
      src/main/java/cn/com/qmth/am/bean/ds/OcrReq.java
  39. 33 0
      src/main/java/cn/com/qmth/am/bean/ds/StandardAnswer.java
  40. 17 0
      src/main/java/cn/com/qmth/am/bean/ds/TextContent.java
  41. 22 0
      src/main/java/cn/com/qmth/am/config/FillMetaObjectHandler.java
  42. 46 0
      src/main/java/cn/com/qmth/am/config/InitData.java
  43. 15 0
      src/main/java/cn/com/qmth/am/config/MyBatisPlusConfig.java
  44. 25 0
      src/main/java/cn/com/qmth/am/config/OcrServerCfg.java
  45. 174 0
      src/main/java/cn/com/qmth/am/config/SysProperty.java
  46. 48 0
      src/main/java/cn/com/qmth/am/consumer/MarkingConsumer.java
  47. 48 0
      src/main/java/cn/com/qmth/am/consumer/OcrConsumer.java
  48. 67 0
      src/main/java/cn/com/qmth/am/consumer/UpdateMarkingScoreConsumer.java
  49. 522 0
      src/main/java/cn/com/qmth/am/controller/AdminController.java
  50. 9 0
      src/main/java/cn/com/qmth/am/dao/local/QuestionDao.java
  51. 23 0
      src/main/java/cn/com/qmth/am/dao/local/StudentScoreDao.java
  52. 19 0
      src/main/java/cn/com/qmth/am/dao/stmms/StmmsDao.java
  53. 183 0
      src/main/java/cn/com/qmth/am/entity/QuestionEntity.java
  54. 182 0
      src/main/java/cn/com/qmth/am/entity/StudentScoreEntity.java
  55. 45 0
      src/main/java/cn/com/qmth/am/entity/base/BaseEntity.java
  56. 27 0
      src/main/java/cn/com/qmth/am/entity/base/IdEntity.java
  57. 30 0
      src/main/java/cn/com/qmth/am/enums/AnswerRangeType.java
  58. 34 0
      src/main/java/cn/com/qmth/am/enums/DataStatus.java
  59. 32 0
      src/main/java/cn/com/qmth/am/enums/DataType.java
  60. 46 0
      src/main/java/cn/com/qmth/am/enums/FileType.java
  61. 30 0
      src/main/java/cn/com/qmth/am/enums/ImportFileName.java
  62. 21 0
      src/main/java/cn/com/qmth/am/enums/LockType.java
  63. 19 0
      src/main/java/cn/com/qmth/am/enums/ModelTypeBak.java
  64. 25 0
      src/main/java/cn/com/qmth/am/enums/PromptTemplate.java
  65. 16 0
      src/main/java/cn/com/qmth/am/handle/ImagePositionListTypeHandler.java
  66. 16 0
      src/main/java/cn/com/qmth/am/handle/ImageSliceListTypeHandler.java
  67. 66 0
      src/main/java/cn/com/qmth/am/handle/ListTypeHandler.java
  68. 16 0
      src/main/java/cn/com/qmth/am/handle/StandardAnswerListTypeHandler.java
  69. 54 0
      src/main/java/cn/com/qmth/am/multithread/AopTargetUtils.java
  70. 162 0
      src/main/java/cn/com/qmth/am/multithread/Basket.java
  71. 83 0
      src/main/java/cn/com/qmth/am/multithread/Consumer.java
  72. 11 0
      src/main/java/cn/com/qmth/am/multithread/EndObject.java
  73. 167 0
      src/main/java/cn/com/qmth/am/multithread/Producer.java
  74. 17 0
      src/main/java/cn/com/qmth/am/service/DsMarkingService.java
  75. 32 0
      src/main/java/cn/com/qmth/am/service/QuestionService.java
  76. 16 0
      src/main/java/cn/com/qmth/am/service/StmmsService.java
  77. 54 0
      src/main/java/cn/com/qmth/am/service/StudentScoreService.java
  78. 24 0
      src/main/java/cn/com/qmth/am/service/StudentService.java
  79. 227 0
      src/main/java/cn/com/qmth/am/service/impl/DsMarkingServiceImpl.java
  80. 561 0
      src/main/java/cn/com/qmth/am/service/impl/QuestionServiceImpl.java
  81. 31 0
      src/main/java/cn/com/qmth/am/service/impl/StmmsServiceImpl.java
  82. 745 0
      src/main/java/cn/com/qmth/am/service/impl/StudentScoreServiceImpl.java
  83. 140 0
      src/main/java/cn/com/qmth/am/service/impl/StudentServiceImpl.java
  84. 103 0
      src/main/java/cn/com/qmth/am/task/AiMarkingJob.java
  85. 131 0
      src/main/java/cn/com/qmth/am/task/OcrJob.java
  86. 36 0
      src/main/java/cn/com/qmth/am/task/QuestionImportJob.java
  87. 36 0
      src/main/java/cn/com/qmth/am/task/StudentImportJob.java
  88. 100 0
      src/main/java/cn/com/qmth/am/task/StudentScoreImportJob.java
  89. 40 0
      src/main/java/cn/com/qmth/am/utils/BatchSetDataUtil.java
  90. 214 0
      src/main/java/cn/com/qmth/am/utils/Calculator.java
  91. 899 0
      src/main/java/cn/com/qmth/am/utils/FileUtil.java
  92. 51 0
      src/main/java/cn/com/qmth/am/utils/FormFilePart.java
  93. 47 0
      src/main/java/cn/com/qmth/am/utils/FreeMarkerUtil.java
  94. 108 0
      src/main/java/cn/com/qmth/am/utils/HttpMethod.java
  95. 423 0
      src/main/java/cn/com/qmth/am/utils/ImageUtil.java
  96. 96 0
      src/main/java/cn/com/qmth/am/utils/JsonHelper.java
  97. 88 0
      src/main/java/cn/com/qmth/am/utils/MD5Util.java
  98. 277 0
      src/main/java/cn/com/qmth/am/utils/OKHttpUtil.java
  99. 64 0
      src/main/java/cn/com/qmth/am/utils/ResouceUtil.java
  100. 63 0
      src/main/java/cn/com/qmth/am/utils/SSLSocketClient.java

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

+ 49 - 0
db/am_db.sql

@@ -0,0 +1,49 @@
+
+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` DEFAULT NOT NULL,
+  `full_score` double NOT NULL,
+  `answer` longtext COLLATE utf8mb4_bin DEFAULT NULL,
+  `content` longtext COLLATE utf8mb4_bin NOT NULL,
+  `score_grades` longtext COLLATE utf8mb4_bin DEFAULT NULL,
+  `ext` longtext COLLATE utf8mb4_bin DEFAULT NULL,
+  `answer_range_type` varchar(1000) COLLATE utf8mb4_bin NOT NULL,
+  `image_slice` varchar(1000) COLLATE utf8mb4_bin DEFAULT NULL,
+  `prompt_template` varchar(255) NOT NULL,
+  `status` varchar(255) 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,
+   `student_id` bigint NOT NULL,
+  `exam_number` varchar(255) COLLATE utf8mb4_bin 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,
+  `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,
+  `sheet` varchar(1000) DEFAULT NULL,
+  `slice` varchar(500) DEFAULT NULL,
+  `image_position` varchar(1000) COLLATE utf8mb4_bin DEFAULT NULL,
+  `sheet_count` int DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `IDX_STUDENT_SCORE_01` (`question_id`,`exam_number`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

BIN
file/question-import.xlsx


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

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

+ 9 - 0
file/接口说明.txt

@@ -0,0 +1,9 @@
+分系结果查询
+https://ai.ea100.com.cn/api/admin/fenxi?examId=1342
+参数 examId,考试id,必填;exZero,是否排除机评0分人评非0分,true/false,非必填;count 样本数量,非必填;score 分数差值小于等于,非必填;
+
+https://ai.ea100.com.cn/api/admin/marking/status?enable=true 启停机评
+https://ai.ea100.com.cn/api/admin/ocr/status?enable=true 启停ocr
+
+https://ai.ea100.com.cn/api/admin/info?examId=1379 查询数据信息
+

+ 29 - 0
file/说明.txt

@@ -0,0 +1,29 @@
+首次运行前先执行 初始化数据库.bat
+
+templates目录:为提示词模版文件,文件名不可修改,question导入时填写此目录下的模版文件名。
+data目录:需导入的Excel文件放在此目录下,系统定时读取,读取成功或失败后移动到data下的success或failed目录下,生成相应的信息文件。data目录可配置修改。
+
+
+执行  启动服务.bat 来启动服务,启动后不能关闭窗口。
+
+
+
+
+question-import.xlsx为小题导入文件,文件名不可修改。
+
+application.properties为配置文件
+
+配置文件可修改内容:
+	am.ocr-task.enable:是否开启ocr任务true/false
+	am.marking-task.enable:是否开启评分任务true/false
+	am.image-server:图片文件访问域名 示例:https://file.markingcloud.com
+	am.data-dir:excel导入文件读取目录。示例./data
+
+接口:
+	数据详情查询
+		参数examId 考试id 必填参数
+		示例:http://localhost:8093/api/admin/info?examId=1
+	数据重置(重置后需重新导入人评分数)
+		参数examId 考试id  必填参数
+		参数subjectCode 科目代码(知学知考为课程id) 非必填参数
+		示例:http://localhost:8093/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-cet</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>mysql</groupId>
+    <artifactId>mysql-connector-java</artifactId>
+</dependency>
+      <dependency>
+        <groupId>com.baomidou</groupId>
+        <artifactId>mybatis-plus-boot-starter</artifactId>
+        <version>3.4.2</version>
+      </dependency>
+     <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>3.4.1</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-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>

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

@@ -0,0 +1,22 @@
+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);
+    }
+
+}

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

@@ -0,0 +1,38 @@
+package cn.com.qmth.am.bean;
+
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+public class AiMarkingDto {
+
+    private StudentScoreEntity scoreInfo;
+
+    private QuestionEntity question;
+
+    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;
+    }
+
+    public QuestionEntity getQuestion() {
+        return question;
+    }
+
+    public void setQuestion(QuestionEntity question) {
+        this.question = question;
+    }
+
+}

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

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

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

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean;
+
+public class ImagePosition {
+
+    private Boolean left;
+
+    private Integer pageIndex;
+
+    public Boolean getLeft() {
+        return left;
+    }
+
+    public void setLeft(Boolean left) {
+        this.left = left;
+    }
+
+    public Integer getPageIndex() {
+        return pageIndex;
+    }
+
+    public void setPageIndex(Integer pageIndex) {
+        this.pageIndex = pageIndex;
+    }
+
+}

+ 31 - 0
src/main/java/cn/com/qmth/am/bean/ImageSize.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.am.bean;
+
+public class ImageSize {
+
+    private int width;
+
+    private int height;
+
+    public ImageSize(int width, int height) {
+        super();
+        this.width = width;
+        this.height = height;
+    }
+
+    public int getWidth() {
+        return width;
+    }
+
+    public void setWidth(int width) {
+        this.width = width;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+}

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

+ 90 - 0
src/main/java/cn/com/qmth/am/bean/ModelSpeed.java

@@ -0,0 +1,90 @@
+package cn.com.qmth.am.bean;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.am.utils.Calculator;
+
+@Component
+public class ModelSpeed {
+
+    private Long ocrStartTime;
+
+    private Long markingStartTime;
+
+    private AtomicInteger ocrCount = new AtomicInteger(0);
+
+    private AtomicInteger markingCount = new AtomicInteger(0);
+
+    private Double ocrSpeed;
+
+    private Double markingSpeed;
+
+    public void addOcrCOunt() {
+        ocrCount.addAndGet(1);
+    }
+
+    public void addMarkingCount() {
+        markingCount.addAndGet(1);
+    }
+
+    public void ocrStart() {
+        this.ocrStartTime = System.currentTimeMillis();
+        this.ocrSpeed = null;
+    }
+
+    public void ocrEnd() {
+        this.ocrStartTime = null;
+        this.ocrCount = new AtomicInteger(0);
+
+    }
+
+    public void markingStart() {
+        this.markingStartTime = System.currentTimeMillis();
+        this.markingSpeed = null;
+    }
+
+    public void markingEnd() {
+        this.markingStartTime = null;
+        this.markingCount = new AtomicInteger(0);
+
+    }
+
+    public double getOcrSpeed() {
+        Long start = this.ocrStartTime;
+        if (start == null) {
+            Double speed = this.ocrSpeed;
+            if (speed != null) {
+                return speed;
+            } else {
+                return 0;
+            }
+        } else {
+            return getSpeed(start, this.ocrCount);
+        }
+    }
+
+    public double getMarkingSpeed() {
+        Long start = this.markingStartTime;
+        if (start == null) {
+            Double speed = this.markingSpeed;
+            if (speed != null) {
+                return speed;
+            } else {
+                return 0;
+            }
+        } else {
+            return getSpeed(start, this.markingCount);
+        }
+    }
+
+    private Double getSpeed(long startTime, AtomicInteger count) {
+        long end = System.currentTimeMillis();
+        if (count.get() == 0) {
+            return 0.0;
+        } else {
+            return Calculator.divide(Calculator.multiply(count.get(), 1000), end - startTime, 5);
+        }
+    }
+}

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

@@ -0,0 +1,45 @@
+package cn.com.qmth.am.bean;
+
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+public class OcrDto {
+
+    private StudentScoreEntity score;
+
+    private QuestionEntity quetion;
+
+    private OcrServer ocrServer;
+
+    public OcrDto(StudentScoreEntity score, QuestionEntity quetion, OcrServer ocrServer) {
+        super();
+        this.score = score;
+        this.quetion = quetion;
+        this.ocrServer = ocrServer;
+    }
+
+    public StudentScoreEntity getScore() {
+        return score;
+    }
+
+    public void setScore(StudentScoreEntity score) {
+        this.score = score;
+    }
+
+    public QuestionEntity getQuetion() {
+        return quetion;
+    }
+
+    public void setQuetion(QuestionEntity quetion) {
+        this.quetion = quetion;
+    }
+
+    public OcrServer getOcrServer() {
+        return ocrServer;
+    }
+
+    public void setOcrServer(OcrServer ocrServer) {
+        this.ocrServer = ocrServer;
+    }
+
+}

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

@@ -0,0 +1,35 @@
+package cn.com.qmth.am.bean;
+
+public class OcrServer {
+
+    private String model;
+
+    private String server;
+
+    private String key;
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getServer() {
+        return server;
+    }
+
+    public void setServer(String server) {
+        this.server = server;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+}

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

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

@@ -0,0 +1,55 @@
+package cn.com.qmth.am.bean;
+
+public class StudentScoreImageDto {
+
+    private OcrServer ocrServer;
+
+    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;
+    }
+
+    public OcrServer getOcrServer() {
+        return ocrServer;
+    }
+
+    public void setOcrServer(OcrServer ocrServer) {
+        this.ocrServer = ocrServer;
+    }
+
+}

+ 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 examNumber;
+
+    public Long getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(Long questionId) {
+        this.questionId = questionId;
+    }
+
+    public String getExamNUmber() {
+        return examNumber;
+    }
+
+    public void setExamNumber(String examNumber) {
+        this.examNumber = examNumber;
+    }
+
+}

+ 134 - 0
src/main/java/cn/com/qmth/am/bean/StudentScoreVo.java

@@ -0,0 +1,134 @@
+package cn.com.qmth.am.bean;
+
+import java.util.List;
+
+import cn.com.qmth.am.enums.DataStatus;
+
+public class StudentScoreVo {
+
+    private Long id;
+
+    private Long studentId;
+
+    private Long examId;
+
+    private String subjectCode;
+
+    private Long questionId;
+
+    private String examNumber;
+
+    // 机评分
+    private Double aiScore;
+
+    // 人评分
+    private Double markingScore;
+
+    // ocr状态
+    private DataStatus answerStatus;
+
+    // 评分状态
+    private DataStatus scoreStatus;
+
+    // 机评返回null
+    private Boolean scoreNone;
+
+    private List<ImagePosition> ips;
+
+    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 String getExamNumber() {
+        return examNumber;
+    }
+
+    public void setExamNumber(String examNumber) {
+        this.examNumber = examNumber;
+    }
+
+    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 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 Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    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 List<ImagePosition> getIps() {
+        return ips;
+    }
+
+    public void setIps(List<ImagePosition> ips) {
+        this.ips = ips;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+}

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

@@ -0,0 +1,35 @@
+package cn.com.qmth.am.bean;
+
+public class StudentVo {
+
+    private Long studentId;
+
+    private String examNumber;
+
+    private Integer sheetCount;
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public String getExamNumber() {
+        return examNumber;
+    }
+
+    public void setExamNumber(String examNumber) {
+        this.examNumber = examNumber;
+    }
+
+    public Integer getSheetCount() {
+        return sheetCount;
+    }
+
+    public void setSheetCount(Integer sheetCount) {
+        this.sheetCount = sheetCount;
+    }
+
+}

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

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean;
+
+public class TrackPosition {
+
+    private Integer offsetX;
+
+    private Integer offsetIndex;
+
+    public Integer getOffsetX() {
+        return offsetX;
+    }
+
+    public void setOffsetX(Integer offsetX) {
+        this.offsetX = offsetX;
+    }
+
+    public Integer getOffsetIndex() {
+        return offsetIndex;
+    }
+
+    public void setOffsetIndex(Integer offsetIndex) {
+        this.offsetIndex = offsetIndex;
+    }
+
+}

+ 132 - 0
src/main/java/cn/com/qmth/am/bean/ds/AutoScoreRequest.java

@@ -0,0 +1,132 @@
+package cn.com.qmth.am.bean.ds;
+
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 自动判分请求参数
+ */
+@Validated
+public class AutoScoreRequest {
+
+    @NotBlank(message = "科目名称不能为空")
+    private String subjectName;
+
+    @NotBlank(message = "试题内容不能为空")
+    private String questionBody;
+
+    @NotNull(message = "标答不能为空")
+    @Size(min = 1, message = "标答不能为空")
+    @Valid
+    private List<StandardAnswer> standardAnswer = new ArrayList<>();
+
+    @NotNull(message = "考生回答不能为空")
+    private String studentAnswer;
+
+    private double totalScore;
+
+    private double intervalScore = 1;
+
+    // 分档
+    private String scoreGrades;
+
+    // 补充说明
+    private String ext;
+
+    // 试题名称
+    private String questionTitle;
+
+    public String getSubjectName() {
+        return subjectName;
+    }
+
+    public void setSubjectName(String subjectName) {
+        this.subjectName = subjectName;
+    }
+
+    public String getQuestionBody() {
+        return questionBody;
+    }
+
+    public void setQuestionBody(String questionBody) {
+        this.questionBody = questionBody;
+    }
+
+    public List<StandardAnswer> getStandardAnswer() {
+        return standardAnswer;
+    }
+
+    public void setStandardAnswer(List<StandardAnswer> standardAnswer) {
+        this.standardAnswer = standardAnswer;
+    }
+
+    /**
+     * 增加标答内容及分值
+     *
+     * @param content
+     *            文本内容
+     * @param score
+     *            格式化后的分数字符串
+     */
+    public void addStandardAnswer(@NotNull String content, @NotNull double score) {
+        StandardAnswer answer = new StandardAnswer();
+        answer.setContent(content);
+        answer.setScore(score);
+        this.standardAnswer.add(answer);
+    }
+
+    public String getStudentAnswer() {
+        return studentAnswer;
+    }
+
+    public void setStudentAnswer(String studentAnswer) {
+        this.studentAnswer = studentAnswer;
+    }
+
+    public double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public double getIntervalScore() {
+        return intervalScore;
+    }
+
+    public void setIntervalScore(double intervalScore) {
+        this.intervalScore = intervalScore;
+    }
+
+    public String getScoreGrades() {
+        return scoreGrades;
+    }
+
+    public void setScoreGrades(String scoreGrades) {
+        this.scoreGrades = scoreGrades;
+    }
+
+    public String getExt() {
+        return ext;
+    }
+
+    public void setExt(String ext) {
+        this.ext = ext;
+    }
+
+    public String getQuestionTitle() {
+        return questionTitle;
+    }
+
+    public void setQuestionTitle(String questionTitle) {
+        this.questionTitle = questionTitle;
+    }
+
+}

+ 24 - 0
src/main/java/cn/com/qmth/am/bean/ds/AutoScoreResult.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.am.bean.ds;
+
+public class AutoScoreResult {
+
+    private double totalScore;
+
+    private double[] stepScore;
+
+    public double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public double[] getStepScore() {
+        return stepScore;
+    }
+
+    public void setStepScore(double[] stepScore) {
+        this.stepScore = stepScore;
+    }
+}

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

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean.ds;
+
+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;
+    }
+
+}

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

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

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

+ 6 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatRole.java

@@ -0,0 +1,6 @@
+package cn.com.qmth.am.bean.ds;
+
+public enum ChatRole {
+
+    system, user, assistant;
+}

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

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

@@ -0,0 +1,31 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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 {
+}

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

@@ -0,0 +1,28 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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);
+    }
+}

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

@@ -0,0 +1,33 @@
+package cn.com.qmth.am.bean.ds;
+
+import javax.validation.constraints.NotNull;
+
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * 自动评分应用中按得分点构造的标答
+ */
+@Validated
+public class StandardAnswer {
+
+    @NotNull(message = "标答不能为空")
+    private String content;
+
+    private double score;
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public double getScore() {
+        return score;
+    }
+
+    public void setScore(double score) {
+        this.score = score;
+    }
+}

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

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

@@ -0,0 +1,46 @@
+package cn.com.qmth.am.config;
+
+import java.io.File;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StudentScoreService;
+
+@Component
+public class InitData implements CommandLineRunner {
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Override
+    public void run(String... args) throws Exception {
+        File dataDir = new File(sysProperty.getDataDir());
+        if (!dataDir.exists()) {
+            dataDir.mkdir();
+        }
+        File dir = new File(sysProperty.getDataDir() + "/" + "slice");
+        if (!dir.exists()) {
+            dir.mkdir();
+        }
+        File sheet = new File(sysProperty.getDataDir() + "/" + "sheet");
+        if (!sheet.exists()) {
+            sheet.mkdir();
+        }
+        resetTaskStatus();
+    }
+
+    private void resetTaskStatus() {
+        studentScoreService.resetStatus();
+        questionService.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();
+    }
+
+}

+ 25 - 0
src/main/java/cn/com/qmth/am/config/OcrServerCfg.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.am.bean.OcrServer;
+
+@ConfigurationProperties(prefix = "am.ocr")
+@Component
+public class OcrServerCfg {
+
+    private List<OcrServer> ocrConfig = new ArrayList<>();
+
+    public List<OcrServer> getOcrConfig() {
+        return ocrConfig;
+    }
+
+    public void setOcrConfig(List<OcrServer> ocrConfig) {
+        this.ocrConfig = ocrConfig;
+    }
+
+}

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

@@ -0,0 +1,174 @@
+package cn.com.qmth.am.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SysProperty {
+
+    @Value("${am.save-image}")
+    private Boolean saveImage;
+
+    @Value("${am.student.count}")
+    private Integer studentCount;
+
+    @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.marking-thread-count:2}")
+    private Integer markingThreadCount;
+
+    @Value("${am.ocr-thread-count:2}")
+    private Integer ocrThreadCount;
+
+    @Value("${am.marking-model:none}")
+    private String markingModel;
+
+    @Value("${am.marking-key:none}")
+    private String markingKey;
+
+    @Value("${am.marking-server:none}")
+    private String markingServer;
+
+    @Value("${am.ocr-model:none}")
+    private String ocrModel;
+
+    @Value("${am.ocr-key:none}")
+    private String ocrKey;
+
+    @Value("${am.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 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 getMarkingThreadCount() {
+        return markingThreadCount;
+    }
+
+    public void setMarkingThreadCount(Integer markingThreadCount) {
+        this.markingThreadCount = markingThreadCount;
+    }
+
+    public Integer getOcrThreadCount() {
+        return ocrThreadCount;
+    }
+
+    public void setOcrThreadCount(Integer ocrThreadCount) {
+        this.ocrThreadCount = ocrThreadCount;
+    }
+
+    public Integer getStudentCount() {
+        return studentCount;
+    }
+
+    public void setStudentCount(Integer studentCount) {
+        this.studentCount = studentCount;
+    }
+
+    public Boolean getSaveImage() {
+        return saveImage;
+    }
+
+    public void setSaveImage(Boolean saveImage) {
+        this.saveImage = saveImage;
+    }
+
+}

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

+ 48 - 0
src/main/java/cn/com/qmth/am/consumer/OcrConsumer.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.bean.OcrDto;
+import cn.com.qmth.am.service.StudentService;
+
+@Scope("prototype")
+@Service
+public class OcrConsumer implements Runnable {
+
+    private CountDownLatch endGate;
+
+    @Autowired
+    private StudentService studentService;
+
+    private OcrDto ocrDto;
+
+    @Override
+    public void run() {
+        try {
+            studentService.buildImage(ocrDto);
+        } finally {
+            endGate.countDown();
+        }
+    }
+
+    public CountDownLatch getEndGate() {
+        return endGate;
+    }
+
+    public void setEndGate(CountDownLatch endGate) {
+        this.endGate = endGate;
+    }
+
+    public OcrDto getOcrDto() {
+        return ocrDto;
+    }
+
+    public void setOcrDto(OcrDto ocrDto) {
+        this.ocrDto = ocrDto;
+    }
+
+}

+ 67 - 0
src/main/java/cn/com/qmth/am/consumer/UpdateMarkingScoreConsumer.java

@@ -0,0 +1,67 @@
+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.bean.StudentScoreVo;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.service.StudentService;
+
+@Scope("prototype")
+@Service
+public class UpdateMarkingScoreConsumer implements Runnable {
+
+    private CountDownLatch endGate;
+
+    private QuestionEntity question;
+
+    private StudentScoreVo vo;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Override
+    public void run() {
+        try {
+            studentService.updateMarkingScore(question, vo);
+        } finally {
+            endGate.countDown();
+        }
+    }
+
+    public CountDownLatch getEndGate() {
+        return endGate;
+    }
+
+    public void setEndGate(CountDownLatch endGate) {
+        this.endGate = endGate;
+    }
+
+    public QuestionEntity getQuestion() {
+        return question;
+    }
+
+    public void setQuestion(QuestionEntity question) {
+        this.question = question;
+    }
+
+    public StudentService getStudentService() {
+        return studentService;
+    }
+
+    public void setStudentService(StudentService studentService) {
+        this.studentService = studentService;
+    }
+
+    public StudentScoreVo getVo() {
+        return vo;
+    }
+
+    public void setVo(StudentScoreVo vo) {
+        this.vo = vo;
+    }
+
+}

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

@@ -0,0 +1,522 @@
+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.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.bean.ModelSpeed;
+import cn.com.qmth.am.bean.StudentScoreVo;
+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 ModelSpeed modelSpeed;
+
+    @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) String subjectCode, @RequestParam(required = false) Boolean exZero,
+            @RequestParam(required = false) Integer count, @RequestParam(required = false) Integer score) {
+        StringBuilder sb = new StringBuilder();
+        List<QuestionEntity> qs = questionService.findByExamIdAndSubject(examId, subjectCode);
+        if (CollectionUtils.isEmpty(qs)) {
+            sb.append("试题数:0");
+            returnJson(sb.toString(), response);
+            return;
+        }
+        sb.append("试题数:" + qs.size() + "\r\n");
+
+        for (QuestionEntity q : qs) {
+            sb.append(
+                    "---------------------------------------------------------------------------------------------------------------------------------\r\n");
+            List<StudentScoreEntity> scores = studentScoreService.findBy(q.getId(), 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(required = false) Long examId) {
+        StringBuilder sb = new StringBuilder();
+        List<QuestionEntity> qs = questionService.list();
+        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() ? "是" : "否"));
+        sb.append(" | 评分是否开启:" + (sysProperty.getMarkingTaskEnable() ? "是" : "否"));
+        sb.append(" | 科目总数:" + qsCourse);
+        sb.append(" | 小题总数:" + qstotal);
+        double ocrSpeed = modelSpeed.getOcrSpeed();
+        double markingSpeed = modelSpeed.getMarkingSpeed();
+        sb.append(" | ocr速度:" + Calculator.scale(ocrSpeed, 2) + "个/秒");
+        sb.append(" | 评分速度:" + Calculator.scale(markingSpeed, 2) + "个/秒");
+        List<StudentScoreVo> vos = studentScoreService.getInfoByExam(examId);
+        if (CollectionUtils.isEmpty(vos)) {
+            returnJson(sb.toString(), response);
+            return;
+        }
+        int ocrTodoCount = 0;
+        int markingTodoCount = 0;
+        if (examId != null) {
+            Map<String, List<StudentScoreVo>> map = new LinkedHashMap<>();
+            for (StudentScoreVo vo : vos) {
+                List<StudentScoreVo> tem = map.get(vo.getSubjectCode());
+                if (tem == null) {
+                    tem = new ArrayList<>();
+                    map.put(vo.getSubjectCode(), tem);
+                }
+                tem.add(vo);
+                if (!DataStatus.SUCCESS.equals(vo.getAnswerStatus())) {
+                    ocrTodoCount++;
+                }
+                if (!DataStatus.SUCCESS.equals(vo.getScoreStatus())) {
+                    markingTodoCount++;
+                }
+            }
+            sb.append(" | ocr剩余时间:" + getLeftTime(ocrSpeed, ocrTodoCount));
+            sb.append(" | 评分剩余时间:" + getLeftTime(markingSpeed, markingTodoCount));
+            sb.append("\r\n");
+            subjectInfo(sb, examId, map);
+        } else {
+            Map<Long, Map<String, List<StudentScoreVo>>> exams = new LinkedHashMap<>();
+            for (StudentScoreVo vo : vos) {
+                Map<String, List<StudentScoreVo>> sujects = exams.get(vo.getExamId());
+                if (sujects == null) {
+                    sujects = new LinkedHashMap<>();
+                    exams.put(vo.getExamId(), sujects);
+
+                }
+                List<StudentScoreVo> tem = sujects.get(vo.getSubjectCode());
+                if (tem == null) {
+                    tem = new ArrayList<>();
+                    sujects.put(vo.getSubjectCode(), tem);
+                }
+                tem.add(vo);
+                if (!DataStatus.SUCCESS.equals(vo.getAnswerStatus())) {
+                    ocrTodoCount++;
+                }
+                if (!DataStatus.SUCCESS.equals(vo.getScoreStatus())) {
+                    markingTodoCount++;
+                }
+            }
+            sb.append(" | ocr剩余时间:" + getLeftTime(ocrSpeed, ocrTodoCount));
+            sb.append(" | 评分剩余时间:" + getLeftTime(markingSpeed, markingTodoCount));
+            sb.append("\r\n");
+            for (Long exam : exams.keySet()) {
+                subjectInfo(sb, exam, exams.get(exam));
+            }
+        }
+        returnJson(sb.toString(), response);
+    }
+
+    private String getLeftTime(double speed, int count) {
+        if (speed == 0) {
+            return "-";
+        }
+        if (count == 0) {
+            return getText(0);
+        }
+        int totalSeconds = Double.valueOf(Calculator.divide(count, speed, 0) + "").intValue();
+        return getText(totalSeconds);
+    }
+
+    private String getText(int totalSeconds) {
+        int hours = totalSeconds / 3600;
+        // 计算剩余秒数
+        int remainingSecondsAfterHours = totalSeconds % 3600;
+        // 计算分钟数
+        int minutes = remainingSecondsAfterHours / 60;
+        // 计算剩余秒数
+        int seconds = remainingSecondsAfterHours % 60;
+
+        return String.format("%d小时%d分%d秒", hours, minutes, seconds);
+    }
+
+    private void subjectInfo(StringBuilder sb, Long examId, Map<String, List<StudentScoreVo>> subjects) {
+        sb.append("\r\n-------------------------------------------------------------------------------\r\n");
+        List<QuestionEntity> qs = questionService.findByExamId(examId);
+        sb.append("考试id:" + examId);
+        sb.append(" | 科目总数:" + subjects.size());
+        sb.append(" | 小题总数:" + qs.size());
+        Map<String, Integer> qmap = new HashMap<>();
+        for (QuestionEntity q : qs) {
+            Integer tem = qmap.get(q.getSubjectCode());
+            if (tem == null) {
+                tem = 0;
+            }
+            qmap.put(q.getSubjectCode(), tem + 1);
+        }
+        for (String subject : subjects.keySet()) {
+            sb.append("\r\n");
+            sb.append("\r\n");
+            sb.append("      科目代码:" + subject);
+            sb.append(" | 小题数:" + qmap.get(subject));
+            List<StudentScoreVo> vos = subjects.get(subject);
+            Set<String> studentSet = new HashSet<>();
+            int qocrsuc = 0;
+            int qocrfailed = 0;
+            int qsuc = 0;
+            int qfailed = 0;
+            int qtotal = 0;
+            int markingtotal = 0;
+            if (CollectionUtils.isNotEmpty(vos)) {
+                qtotal = vos.size();
+                for (StudentScoreVo vo : vos) {
+                    studentSet.add(vo.getExamNumber());
+                    if (DataStatus.SUCCESS.equals(vo.getAnswerStatus())) {
+                        qocrsuc++;
+                    }
+                    if (DataStatus.FAILED.equals(vo.getAnswerStatus())) {
+                        qocrfailed++;
+                    }
+                    if (DataStatus.SUCCESS.equals(vo.getScoreStatus())) {
+                        qsuc++;
+                    }
+                    if (DataStatus.FAILED.equals(vo.getScoreStatus())) {
+                        qfailed++;
+                    }
+                    if (vo.getMarkingScore() != null) {
+                        markingtotal++;
+                    }
+                }
+            }
+            if (studentSet.size() == 0) {
+                sb.append(" | 考生总数:0");
+                return;
+            }
+            sb.append(" | 考生总数:" + studentSet.size());
+            sb.append(" | 试题总数:" + qtotal);
+            sb.append(" | 已人评数:" + markingtotal);
+            sb.append(" | OCR成功总数:" + qocrsuc);
+            sb.append(" | OCR失败总数:" + qocrfailed);
+            sb.append(" | 评分成功总数:" + qsuc);
+            sb.append(" | 评分失败总数:" + qfailed);
+        }
+    }
+
+    @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/local/QuestionDao.java

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

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

@@ -0,0 +1,23 @@
+package cn.com.qmth.am.dao.local;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import cn.com.qmth.am.bean.StudentScoreInfo;
+import cn.com.qmth.am.bean.StudentScoreVo;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+
+public interface StudentScoreDao extends BaseMapper<StudentScoreEntity> {
+
+    List<StudentScoreInfo> getAllList();
+
+    void saveSheetPath(@Param("id") Long id, @Param("sheet") String sheet);
+
+    List<StudentScoreVo> getInfoByExam(@Param("examId") Long examId);
+
+    List<StudentScoreVo> getAllInfoForUpdateScore();
+
+}

+ 19 - 0
src/main/java/cn/com/qmth/am/dao/stmms/StmmsDao.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.am.dao.stmms;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Param;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import cn.com.qmth.am.bean.StudentVo;
+
+@DS("data-source-stmms")
+public interface StmmsDao {
+
+    List<Double> getMarkScore(@Param("studentId") Long studentId, @Param("mainNumber") Integer mainNumber);
+
+    List<StudentVo> getUploadStudent(@Param("examId") Long examId, @Param("subjectCode") String subjectCode,
+            @Param("mainNumber") Integer mainNumber, @Param("limitCount") Integer limitCount);
+
+}

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

@@ -0,0 +1,183 @@
+package cn.com.qmth.am.entity;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import cn.com.qmth.am.bean.ImageSlice;
+import cn.com.qmth.am.bean.ds.StandardAnswer;
+import cn.com.qmth.am.entity.base.IdEntity;
+import cn.com.qmth.am.enums.AnswerRangeType;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.PromptTemplate;
+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 DataStatus status;
+
+    // 试题评分提示词模版
+    private PromptTemplate promptTemplate;
+
+    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;
+
+    // 答题区域类型
+    private AnswerRangeType answerRangeType;
+
+    // 分档
+    private String scoreGrades;
+
+    // 补充说明
+    private String ext;
+
+    @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;
+    }
+
+    public DataStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(DataStatus status) {
+        this.status = status;
+    }
+
+    public PromptTemplate getPromptTemplate() {
+        return promptTemplate;
+    }
+
+    public void setPromptTemplate(PromptTemplate promptTemplate) {
+        this.promptTemplate = promptTemplate;
+    }
+
+    public AnswerRangeType getAnswerRangeType() {
+        return answerRangeType;
+    }
+
+    public void setAnswerRangeType(AnswerRangeType answerRangeType) {
+        this.answerRangeType = answerRangeType;
+    }
+
+    public String getScoreGrades() {
+        return scoreGrades;
+    }
+
+    public void setScoreGrades(String scoreGrades) {
+        this.scoreGrades = scoreGrades;
+    }
+
+    public String getExt() {
+        return ext;
+    }
+
+    public void setExt(String ext) {
+        this.ext = ext;
+    }
+
+}

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

@@ -0,0 +1,182 @@
+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.bean.ImagePosition;
+import cn.com.qmth.am.entity.base.IdEntity;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.handle.ImagePositionListTypeHandler;
+
+@TableName(value = "am_student_score", autoResultMap = true)
+public class StudentScoreEntity extends IdEntity {
+
+    private static final long serialVersionUID = -6261302618070108336L;
+
+    private Long questionId;
+
+    // 云阅卷学生id
+    private Long studentId;
+
+    private String examNumber;
+
+    // 机评分
+    private Double aiScore;
+
+    // 人评分
+    private Double markingScore;
+
+    // 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;
+
+    @TableField(updateStrategy = FieldStrategy.IGNORED, typeHandler = JacksonTypeHandler.class)
+    private List<String> sheet;
+
+    private String slice;
+
+    @TableField(value = "image_position", typeHandler = ImagePositionListTypeHandler.class)
+    private List<ImagePosition> imagePosition;
+
+    private Integer sheetCount;
+
+    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 String getErrMsg() {
+        return errMsg;
+    }
+
+    public void setErrMsg(String errMsg) {
+        this.errMsg = errMsg;
+    }
+
+    public String getExamNumber() {
+        return examNumber;
+    }
+
+    public void setExamNumber(String examNumber) {
+        this.examNumber = examNumber;
+    }
+
+    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;
+    }
+
+    public List<String> getSheet() {
+        return sheet;
+    }
+
+    public void setSheet(List<String> sheet) {
+        this.sheet = sheet;
+    }
+
+    public String getSlice() {
+        return slice;
+    }
+
+    public void setSlice(String slice) {
+        this.slice = slice;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public List<ImagePosition> getImagePosition() {
+        return imagePosition;
+    }
+
+    public void setImagePosition(List<ImagePosition> imagePosition) {
+        this.imagePosition = imagePosition;
+    }
+
+    public Integer getSheetCount() {
+        return sheetCount;
+    }
+
+    public void setSheetCount(Integer sheetCount) {
+        this.sheetCount = sheetCount;
+    }
+
+}

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

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

@@ -0,0 +1,30 @@
+package cn.com.qmth.am.enums;
+
+/**
+ * 答题区域类型
+ *
+ */
+public enum AnswerRangeType {
+
+    FIXED("固定区域"), TRACK("轨迹查找"), ALL("全卷"),;
+
+    private AnswerRangeType(String name) {
+        this.name = name;
+    }
+
+    private String name;
+
+    public String getName() {
+        return name;
+    }
+
+    public static AnswerRangeType getByName(String name) {
+        for (AnswerRangeType r : AnswerRangeType.values()) {
+            if (r.getName().equals(name)) {
+                return r;
+            }
+        }
+        return null;
+    }
+
+}

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

+ 46 - 0
src/main/java/cn/com/qmth/am/enums/FileType.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.am.enums;
+
+/**
+ * 所有管理的文件类型
+ */
+public enum FileType {
+
+    OPERATE_TASK_IMPORT_FILE("异步任务导入文件", "operate_task_import/%d/%s/%s/%s/%s"), OPERATE_TASK_FILE("异步任务结果文件",
+            "operate_task/%d/%s/%s/%s/%s"), SLICE("裁切图", "slice/%d/%s/%s/%s/%s/%d/%s-%d.%s"), REFERENCE_PAPER(
+                    "培训卷、rf卷、标准卷", "%s/%d/%s/%d/%s"), MARK_STANDARD("评分标准", "mark_standard/%d/%s/%d/standard.pdf"),;
+
+    private String name;
+
+    private String pattern;
+
+    private FileType(String name, String pattern) {
+        this.name = name;
+        this.pattern = pattern;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+
+    public boolean equals(String type) {
+        return toString().equalsIgnoreCase(type);
+    }
+
+    public static FileType findByText(String text) {
+        for (FileType type : values()) {
+            if (type.equals(text)) {
+                return type;
+            }
+        }
+        return null;
+    }
+
+    public String getPath(Object... param) {
+        return String.format(pattern, param);
+    }
+
+}

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

+ 25 - 0
src/main/java/cn/com/qmth/am/enums/PromptTemplate.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.enums;
+
+public enum PromptTemplate {
+
+    COMMON("common.ftl"), TRANSLATION("translation.ftl"),;
+
+    private PromptTemplate(String code) {
+        this.code = code;
+    }
+
+    private String code;
+
+    public String getCode() {
+        return code;
+    }
+
+    public static PromptTemplate getByCode(String code) {
+        for (PromptTemplate r : PromptTemplate.values()) {
+            if (r.getCode().equals(code)) {
+                return r;
+            }
+        }
+        return null;
+    }
+}

+ 16 - 0
src/main/java/cn/com/qmth/am/handle/ImagePositionListTypeHandler.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.ImagePosition;
+
+public class ImagePositionListTypeHandler extends ListTypeHandler<ImagePosition> {
+
+    @Override
+    protected JavaType specificType() {
+        return super.mapper.getTypeFactory().constructParametricType(List.class, ImagePosition.class);
+    }
+
+}

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

+ 16 - 0
src/main/java/cn/com/qmth/am/handle/StandardAnswerListTypeHandler.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.ds.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();
+    }
+
+}

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

@@ -0,0 +1,17 @@
+package cn.com.qmth.am.service;
+
+import cn.com.qmth.am.bean.OcrServer;
+import cn.com.qmth.am.bean.ds.AutoScoreRequest;
+import cn.com.qmth.am.bean.ds.AutoScoreResult;
+import cn.com.qmth.am.entity.QuestionEntity;
+
+/**
+ * 类注释
+ */
+public interface DsMarkingService {
+
+    AutoScoreResult autoScore(AutoScoreRequest req, QuestionEntity q);
+
+    String ocr(OcrServer ocrServer, String base64);
+
+}

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

@@ -0,0 +1,32 @@
+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;
+import cn.com.qmth.am.enums.DataStatus;
+
+/**
+ * 类注释
+ */
+public interface QuestionService extends IService<QuestionEntity> {
+
+    void importQuestion();
+
+    List<QuestionEntity> findToDispose();
+
+    public ImportResult disposeFile(InputStream inputStream);
+
+    List<QuestionEntity> findByExamId(Long examId);
+
+    void removeBy(Long examId, String subjectCode);
+
+    void updateStatus(Long id, DataStatus processing);
+
+    List<QuestionEntity> findByExamIdAndSubject(Long examId, String subjectCode);
+
+    void resetStatus();
+}

+ 16 - 0
src/main/java/cn/com/qmth/am/service/StmmsService.java

@@ -0,0 +1,16 @@
+package cn.com.qmth.am.service;
+
+import java.util.List;
+
+import cn.com.qmth.am.bean.StudentVo;
+
+/**
+ * 类注释
+ */
+public interface StmmsService {
+
+    List<Double> getMarkScore(Long studentId, Integer mainNumber);
+
+    List<StudentVo> getUploadStudent(Long examId, String subjectCode, Integer mainNumber, Integer limitCount);
+
+}

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

@@ -0,0 +1,54 @@
+package cn.com.qmth.am.service;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.bean.StudentScoreImageDto;
+import cn.com.qmth.am.bean.StudentScoreVo;
+import cn.com.qmth.am.bean.StudentVo;
+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 updateAnswerErr(Long id, String string);
+
+    void createSlice(OcrDto ocrDto);
+
+    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 questionId, Boolean exZero, Integer count, Integer score);
+
+    List<StudentScoreEntity> findAllToOcr();
+
+    void saveByQuestion(List<QuestionEntity> qlist, List<StudentVo> students);
+
+    StudentScoreEntity findUpdateMarkingScore();
+
+    void updateMarkingScore(StudentScoreVo score);
+
+    List<StudentScoreVo> getInfoByExam(Long examId);
+
+    List<StudentScoreVo> getAllInfoForUpdateScore();
+
+    void updateMarkingScoreAndTrack(StudentScoreVo score);
+
+}

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

@@ -0,0 +1,24 @@
+package cn.com.qmth.am.service;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.bean.StudentScoreVo;
+import cn.com.qmth.am.entity.QuestionEntity;
+
+/**
+ * 类注释
+ */
+public interface StudentService {
+
+    void importStudent();
+
+    void buildImage(OcrDto ocrDto);
+
+    void createSlice(OcrDto ocrDto);
+
+    void reset(Long examId, String subjectCode);
+
+    void clear(Long examId, String subjectCode);
+
+    void updateMarkingScore(QuestionEntity q, StudentScoreVo score);
+
+}

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

@@ -0,0 +1,227 @@
+package cn.com.qmth.am.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+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.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 com.alibaba.fastjson.JSONObject;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+
+import cn.com.qmth.am.bean.OcrServer;
+import cn.com.qmth.am.bean.ds.AutoScoreRequest;
+import cn.com.qmth.am.bean.ds.AutoScoreResult;
+import cn.com.qmth.am.bean.ds.ChatReq;
+import cn.com.qmth.am.bean.ds.ChatResult;
+import cn.com.qmth.am.bean.ds.ChatRole;
+import cn.com.qmth.am.bean.ds.DsChoice;
+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.entity.QuestionEntity;
+import cn.com.qmth.am.enums.PromptTemplate;
+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(OcrServer ocrServer, String base64) {
+        OcrReq dreq = new OcrReq(ocrServer.getModel());
+        dreq.addMsg(new OcrMessage(base64));
+        String res = ocr(ocrServer, 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, QuestionEntity q) {
+        String question = FreeMarkerUtil.getMarkingReq(request, q.getPromptTemplate());
+        MarkingReq dreq = new MarkingReq(sysProperty.getMarkingModel());
+        dreq.addMsg(ChatRole.user, question);
+        String res = marking(dreq);
+        DsChoice result = JSONObject.parseObject(res, DsChoice.class);
+        try {
+            String text = result.getMessage().getContent();
+            AutoScoreResult scoreResult = new AutoScoreResult();
+            if (PromptTemplate.COMMON.equals(q.getPromptTemplate())) {
+                // 依据总分与步骤分计算最大精度
+                int scale = Math.max(getDecimalPlaces(request.getIntervalScore()),
+                        getDecimalPlaces(request.getTotalScore()));
+                int stepCount = request.getStandardAnswer().size();
+                String scoreStr = null;
+                if (stepCount > 1) {
+                    scoreStr = fomatStrByRex(text);
+                } else {
+                    scoreStr = fomatStr(text);
+                }
+                String[] scores = StringUtils.split(scoreStr, ",");
+                double[] scoreArray = new double[stepCount];
+                for (int i = 0; i < stepCount; i++) {
+                    // 根据得分率与步骤总分计算实际得分,按最大精度保留小数位数
+                    double score = BigDecimal
+                            .valueOf(Math.min(Integer.parseInt(scores[i].trim()), 100)
+                                    * request.getStandardAnswer().get(i).getScore())
+                            .divide(BigDecimal.valueOf(100), scale, RoundingMode.HALF_UP).doubleValue();
+                    scoreArray[i] = score;
+                }
+                scoreResult.setStepScore(scoreArray);
+                scoreResult.setTotalScore(Arrays.stream(scoreArray).mapToObj(BigDecimal::new)
+                        .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(1, BigDecimal.ROUND_HALF_UP).doubleValue());
+            } else if (PromptTemplate.TRANSLATION.equals(q.getPromptTemplate())) {
+                String scoreStr = fomatStr(text);
+                double[] scoreArray = new double[1];
+                // 根据得分率与步骤总分计算实际得分,按最大精度保留小数位数
+                double score = Double.valueOf(scoreStr);
+                scoreArray[0] = score;
+                scoreResult.setStepScore(scoreArray);
+                scoreResult.setTotalScore(score);
+            } else {
+                throw new RuntimeException("模版类型错误");
+            }
+            return scoreResult;
+        } catch (Exception e) {
+            log.error(e.getMessage() + " | " + res);
+            return null;
+        }
+    }
+
+    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("返回格式错误");
+        }
+    }
+
+    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("返回格式错误");
+    // }
+    // }
+    private int getDecimalPlaces(double value) {
+        return Math.max(0, BigDecimal.valueOf(value).stripTrailingZeros().scale());
+    }
+
+    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(OcrServer ocrServer, ChatReq dreq) {
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + ocrServer.getKey());
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, ocrServer.getServer(), 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);
+        }
+    }
+}

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

@@ -0,0 +1,561 @@
+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.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.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.bean.ds.StandardAnswer;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.dao.local.QuestionDao;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.enums.AnswerRangeType;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.enums.PromptTemplate;
+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();
+            imp.setStatus(DataStatus.WAITING);
+            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]));
+            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]));
+            imp.setAnswer(getStandardAnswer(answer));
+
+            String scoreGrades = trimAndNullIfBlank(line.get(EXCEL_HEADER[9]));
+            imp.setScoreGrades(scoreGrades);
+
+            String ext = trimAndNullIfBlank(line.get(EXCEL_HEADER[10]));
+            imp.setExt(ext);
+
+            String rangeType = trimAndNullIfBlank(line.get(EXCEL_HEADER[11]));
+            AnswerRangeType answerRangeType = null;
+            if (StringUtils.isBlank(rangeType)) {
+                msg.append("  作答区域类型不能为空");
+            } else {
+                answerRangeType = AnswerRangeType.getByName(rangeType);
+                if (answerRangeType == null) {
+                    msg.append("  作答区域类型有误");
+                } else {
+                    imp.setAnswerRangeType(answerRangeType);
+                }
+            }
+
+            if (AnswerRangeType.FIXED.equals(answerRangeType)) {
+                String imageSlice = trimAndNullIfBlank(line.get(EXCEL_HEADER[12]));
+                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);
+                    }
+                }
+            }
+
+            String template = trimAndNullIfBlank(line.get(EXCEL_HEADER[13]));
+            if (StringUtils.isBlank(template)) {
+                msg.append("  模版文件不能为空");
+            } else {
+                PromptTemplate val = PromptTemplate.getByCode(template);
+                if (val == null) {
+                    msg.append("  模版文件有误");
+                } else {
+                    if (PromptTemplate.TRANSLATION.equals(val) && StringUtils.isBlank(imp.getScoreGrades())) {
+                        msg.append("  评分档次不能为空");
+                    } else {
+                        imp.setPromptTemplate(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());
+                up.setPromptTemplate(s.getPromptTemplate());
+                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);
+    }
+
+    @Override
+    public List<QuestionEntity> findByExamIdAndSubject(Long examId, String subjectCode) {
+        QueryWrapper<QuestionEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<QuestionEntity> lw = wrapper.lambda();
+        lw.eq(QuestionEntity::getExamId, examId);
+        if (StringUtils.isNotBlank(subjectCode)) {
+            lw.eq(QuestionEntity::getSubjectCode, subjectCode);
+        }
+        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);
+    }
+
+    @Override
+    public List<QuestionEntity> findToDispose() {
+        QueryWrapper<QuestionEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<QuestionEntity> lw = wrapper.lambda();
+        lw.in(QuestionEntity::getStatus, DataStatus.WAITING, DataStatus.FAILED);
+        lw.orderByAsc(QuestionEntity::getExamId).orderByAsc(QuestionEntity::getSubjectCode)
+                .orderByAsc(QuestionEntity::getMainNumber).orderByAsc(QuestionEntity::getSubNumber);
+        return this.list(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void updateStatus(Long id, DataStatus st) {
+        UpdateWrapper<QuestionEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<QuestionEntity> lw = wrapper.lambda();
+        lw.set(QuestionEntity::getStatus, st);
+        lw.eq(QuestionEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void resetStatus() {
+        UpdateWrapper<QuestionEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<QuestionEntity> lw = wrapper.lambda();
+        lw.set(QuestionEntity::getStatus, DataStatus.WAITING);
+        lw.eq(QuestionEntity::getStatus, DataStatus.PROCESSING);
+        this.update(wrapper);
+
+    }
+
+}

+ 31 - 0
src/main/java/cn/com/qmth/am/service/impl/StmmsServiceImpl.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.am.service.impl;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+
+import cn.com.qmth.am.bean.StudentVo;
+import cn.com.qmth.am.dao.stmms.StmmsDao;
+import cn.com.qmth.am.service.StmmsService;
+
+@Service
+@DS("data-source-stmms")
+public class StmmsServiceImpl implements StmmsService {
+
+    @Autowired
+    private StmmsDao stmmsDao;
+
+    @Override
+    public List<Double> getMarkScore(Long studentId, Integer mainNumber) {
+        return stmmsDao.getMarkScore(studentId, mainNumber);
+    }
+
+    @Override
+    public List<StudentVo> getUploadStudent(Long examId, String subjectCode, Integer mainNumber, Integer limitCount) {
+        return stmmsDao.getUploadStudent(examId, subjectCode, mainNumber, limitCount);
+    }
+
+}

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

@@ -0,0 +1,745 @@
+package cn.com.qmth.am.service.impl;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.collections4.CollectionUtils;
+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.alibaba.fastjson.JSONArray;
+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.concurrent.service.ConcurrentService;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+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.ImagePosition;
+import cn.com.qmth.am.bean.ImageSize;
+import cn.com.qmth.am.bean.ImageSlice;
+import cn.com.qmth.am.bean.ModelSpeed;
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.bean.StudentScoreImageDto;
+import cn.com.qmth.am.bean.StudentScoreInfo;
+import cn.com.qmth.am.bean.StudentScoreVo;
+import cn.com.qmth.am.bean.StudentVo;
+import cn.com.qmth.am.bean.ds.AutoScoreRequest;
+import cn.com.qmth.am.bean.ds.AutoScoreResult;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.dao.local.StudentScoreDao;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.entity.StudentScoreEntity;
+import cn.com.qmth.am.enums.AnswerRangeType;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.enums.FileType;
+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);
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private DsMarkingService dsMarkingService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Autowired
+    private ModelSpeed modelSpeed;
+
+    @Transactional
+    @Override
+    public void saveByQuestion(List<QuestionEntity> qlist, List<StudentVo> stuVo) {
+        if (CollectionUtils.isEmpty(qlist)) {
+            throw new StatusException("试题信息为空");
+        }
+        Set<String> allStudent = getAllStudent();
+
+        BatchSetDataUtil<StudentVo> bs = new BatchSetDataUtil<StudentVo>() {
+
+            @Override
+            protected void setData(List<StudentVo> dataList) {
+                List<StudentScoreEntity> adds = new ArrayList<>();
+                for (StudentVo vo : dataList) {
+                    for (QuestionEntity q : qlist) {
+                        String scorekey = q.getId() + "-" + vo.getExamNumber();
+                        if (!allStudent.contains(scorekey)) {
+                            StudentScoreEntity stu = new StudentScoreEntity();
+                            adds.add(stu);
+                            allStudent.add(scorekey);
+                            stu.setQuestionId(q.getId());
+                            stu.setAnswerStatus(DataStatus.WAITING);
+                            stu.setScoreStatus(DataStatus.WAITING);
+                            if (vo.getSheetCount() != null && vo.getSheetCount() != 0) {
+                                stu.setSheetCount(vo.getSheetCount());
+                            }
+                            stu.setExamNumber(vo.getExamNumber());
+                            stu.setStudentId(vo.getStudentId());
+                        }
+                    }
+                }
+                if (CollectionUtils.isNotEmpty(adds)) {
+                    saveBatch(adds);
+                }
+            }
+        };
+        bs.setDataForBatch(stuVo, 500);
+
+    }
+
+    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.getExamNUmber());
+        }
+        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);
+    }
+
+    private void updateMarkingErr(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);
+    }
+
+    @Transactional
+    @Override
+    public void createSlice(OcrDto ocrDto) {
+        StudentScoreImageDto dto = new StudentScoreImageDto();
+        dto.setStudentScoreId(ocrDto.getScore().getId());
+        dto.setOcrServer(ocrDto.getOcrServer());
+        getSlice(ocrDto.getScore(), ocrDto.getQuetion(), dto);
+        ocr(dto);
+        if (sysProperty.getSaveImage()) {
+            saveSliceImage(ocrDto.getQuetion(), ocrDto.getScore(), dto.getImage());
+        }
+    }
+
+    private void getSlice(StudentScoreEntity score, QuestionEntity q, StudentScoreImageDto dto) {
+        if (AnswerRangeType.FIXED.equals(q.getAnswerRangeType())) {
+            getFixedSlice(score, q, dto);
+        } else if (AnswerRangeType.TRACK.equals(q.getAnswerRangeType())) {
+            getTrackSlice(score, q, dto);
+        } else if (AnswerRangeType.ALL.equals(q.getAnswerRangeType())) {
+            getAllSheet(score, q, dto);
+        }
+    }
+
+    private void getFixedSlice(StudentScoreEntity score, QuestionEntity q, StudentScoreImageDto dto) {
+        List<byte[]> ret = new ArrayList<>();
+        String suff = null;
+        Map<Integer, AnswerImageDto> answerImages = new LinkedHashMap<>();
+        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()));
+        }
+        if (sysProperty.getSaveImage()) {
+            saveSheetImage(q, score, answerImages);
+        }
+        dto.setSuff(suff);
+        if (ret.size() > 1) {
+            dto.setImage(ImageUtil.joinImages(ret, suff));
+        } else {
+            dto.setImage(ret.get(0));
+        }
+    }
+
+    private void getTrackSlice(StudentScoreEntity score, QuestionEntity q, StudentScoreImageDto dto) {
+        List<byte[]> ret = new ArrayList<>();
+        String suff = null;
+        Map<Integer, AnswerImageDto> answerImages = new LinkedHashMap<>();
+        for (ImagePosition s : score.getImagePosition()) {
+            AnswerImageDto sheet = getSheet(score, q, s.getPageIndex(), answerImages);
+            ImageSize is = ImageUtil.getSize(sheet.getImage());
+            suff = sheet.getSuff();
+            int x = 0;
+            int y = 0;
+            int w = is.getWidth() / 2;
+            int h = is.getHeight();
+            if (!s.getLeft()) {
+                x = w;
+            }
+            ret.add(ImageUtil.cutImage(sheet.getImage(), sheet.getSuff(), x, y, w, h));
+        }
+        if (sysProperty.getSaveImage()) {
+            saveSheetImage(q, score, answerImages);
+        }
+        dto.setSuff(suff);
+        if (ret.size() > 1) {
+            dto.setImage(ImageUtil.joinImages(ret, suff));
+        } else {
+            dto.setImage(ret.get(0));
+        }
+    }
+
+    private void getAllSheet(StudentScoreEntity score, QuestionEntity q, StudentScoreImageDto dto) {
+        String suff = null;
+        Map<Integer, AnswerImageDto> answerImages = new LinkedHashMap<>();
+        AnswerImageDto sheet = getSheet(score, q, 1, answerImages);
+        suff = sheet.getSuff();
+        if (sysProperty.getSaveImage()) {
+            saveSheetImage(q, score, answerImages);
+        }
+        dto.setSuff(suff);
+        dto.setImage(sheet.getImage());
+    }
+
+    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);
+        try {
+            String tem = url.split("\\?")[0];
+            String suff = tem.substring(tem.lastIndexOf(".") + 1).toLowerCase();
+            ret.setImage(ByteArray.fromUrl(url).value());
+            ret.setPageIndex(pageIndex);
+            ret.setSuff(suff);
+            answerImages.put(pageIndex, ret);
+            return ret;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void saveSheetImage(QuestionEntity q, StudentScoreEntity s, Map<Integer, AnswerImageDto> answerImages) {
+        List<String> ss = new ArrayList<>();
+        for (AnswerImageDto ims : answerImages.values()) {
+            String path = "sheet/" + q.getExamId() + "/" + q.getSubjectCode() + "/" + q.getMainNumber() + "/"
+                    + q.getSubNumber() + "/" + s.getExamNumber() + "-" + ims.getPageIndex() + ".jpg";
+            ss.add(path);
+            File image = new File(sysProperty.getDataDir() + "/" + path);
+            if (image.exists()) {
+                continue;
+            }
+            boolean lock = concurrentService.getReadWriteLock(path).writeLock().tryLock();
+            if (lock) {
+                FileOutputStream out = null;
+                try {
+                    if (image.exists()) {
+                        continue;
+                    }
+                    if (!image.getParentFile().exists()) {
+                        image.getParentFile().mkdirs();
+                    }
+                    byte[] bs = Arrays.copyOf(ims.getImage(), ims.getImage().length);
+                    out = new FileOutputStream(image);
+                    out.write(bs, 0, bs.length);
+                    out.flush();
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                } finally {
+                    concurrentService.getReadWriteLock(path).writeLock().unlock();
+                    if (out != null) {
+                        try {
+                            out.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+            }
+        }
+        this.baseMapper.saveSheetPath(s.getId(), JSONArray.toJSONString(ss));
+    }
+
+    private void saveSliceImage(QuestionEntity q, StudentScoreEntity s, byte[] imageByte) {
+        String path = "slice/" + q.getExamId() + "/" + q.getSubjectCode() + "/" + q.getMainNumber() + "/"
+                + q.getSubNumber() + "/" + s.getExamNumber() + ".jpg";
+        updateSlice(s.getId(), path);
+        File image = new File(sysProperty.getDataDir() + "/" + path);
+        if (image.exists()) {
+            image.delete();
+        } else {
+            if (!image.getParentFile().exists()) {
+                image.getParentFile().mkdirs();
+            }
+        }
+        FileOutputStream out = null;
+        try {
+            byte[] bs = Arrays.copyOf(imageByte, imageByte.length);
+            out = new FileOutputStream(image);
+            out.write(bs, 0, bs.length);
+            out.flush();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    private void updateSlice(Long id, String path) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getSlice, path);
+        lw.eq(StudentScoreEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    private String getImageUrl(StudentScoreEntity score, QuestionEntity q) {
+        String examNumber = score.getExamNumber();
+        return sysProperty.getImageServer() + "/"
+                + FileType.SLICE.getPath(q.getExamId(), q.getSubjectCode(), examNumber.substring(0, 5),
+                        examNumber.substring(5, 6), examNumber.substring(10, 13), q.getMainNumber(), examNumber,
+                        q.getMainNumber(), "jpg");
+    }
+
+    private static String getSuffix(String input) {
+        return StringUtils.trimToEmpty(input).substring(Math.max(0, input.length() - 3));
+    }
+
+    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 = "2019301151012";
+        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);
+    // }
+    // }
+
+    @Override
+    public void ocr(StudentScoreImageDto dto) {
+        try {
+            String ret = ocrDispose(dto);
+            if (ret != null) {
+                updateAnswer(dto.getStudentScoreId(), ret);
+                modelSpeed.addOcrCOunt();
+            } 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) {
+        try {
+            String base64 = FileUtil.byteToBase64(dto.getImage(), dto.getSuff());
+            String ret = dsMarkingService.ocr(dto.getOcrServer(), 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);
+                    } 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);
+    // }
+
+    @Override
+    public void aiMarking(StudentScoreEntity score) {
+        AiMarkingDto dto = new AiMarkingDto();
+        dto.setScoreInfo(score);
+        try {
+            QuestionEntity q = questionService.getById(score.getQuestionId());
+            if (q == null) {
+                throw new StatusException("未找到试题信息");
+            }
+            dto.setQuestion(q);
+            AutoScoreRequest req = new AutoScoreRequest();
+            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());
+            req.setExt(q.getExt());
+            req.setScoreGrades(q.getScoreGrades());
+            AutoScoreResult ret = aiMarkingDispose(dto, req);
+            if (ret != null) {
+                List<Double> stepScore = new ArrayList<>();
+                for (Double d : ret.getStepScore()) {
+                    stepScore.add(d);
+                }
+                updateScore(score.getId(), ret.getTotalScore(), stepScore);
+                modelSpeed.addMarkingCount();
+            } else {
+                updateMarkingErr(score.getId(), "making失败,返回null");
+            }
+        } catch (Exception e) {
+            aiScoreErr(dto, e.getMessage());
+        }
+    }
+
+    private AutoScoreResult aiMarkingDispose(AiMarkingDto dto, AutoScoreRequest req) {
+        try {
+            return dsMarkingService.autoScore(req, dto.getQuestion());
+        } 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, req);
+                    } else {
+                        throw new StatusException("重试次数过多");
+                    }
+                } else {
+                    throw e;
+                }
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    private void updateScore(Long id, Double aiScore, List<Double> scoreStep) {
+        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, JSONArray.toJSONString(scoreStep));
+        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) {
+        List<QuestionEntity> qs = questionService.findByExamIdAndSubject(examId, null);
+        if (CollectionUtils.isEmpty(qs)) {
+            return 0;
+        }
+        List<Long> qids = qs.stream().map(e -> e.getId()).collect(Collectors.toList());
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        if (status != null) {
+            lw.eq(StudentScoreEntity::getScoreStatus, status);
+        }
+        lw.in(StudentScoreEntity::getQuestionId, qids);
+        return this.count(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void removeBy(Long examId, String subjectCode) {
+        List<QuestionEntity> qs = questionService.findByExamIdAndSubject(examId, subjectCode);
+        if (CollectionUtils.isEmpty(qs)) {
+            return;
+        }
+        List<Long> qids = qs.stream().map(e -> e.getId()).collect(Collectors.toList());
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.in(StudentScoreEntity::getQuestionId, qids);
+        this.remove(wrapper);
+    }
+
+    @Override
+    public int countOcrBy(Long examId, DataStatus status) {
+        List<QuestionEntity> qs = questionService.findByExamIdAndSubject(examId, null);
+        if (CollectionUtils.isEmpty(qs)) {
+            return 0;
+        }
+        List<Long> qids = qs.stream().map(e -> e.getId()).collect(Collectors.toList());
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        if (status != null) {
+            lw.eq(StudentScoreEntity::getAnswerStatus, status);
+        }
+        lw.in(StudentScoreEntity::getQuestionId, qids);
+        return this.count(wrapper);
+    }
+
+    @Override
+    public List<StudentScoreEntity> findBy(Long questionId, Boolean exZero, Integer count, Integer score) {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.eq(StudentScoreEntity::getQuestionId, questionId);
+        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.getExamNumber();
+                    String c2 = o2.getExamNumber();
+                    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;
+    }
+
+    @Override
+    public StudentScoreEntity findUpdateMarkingScore() {
+        QueryWrapper<StudentScoreEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.isNull(StudentScoreEntity::getMarkingScore);
+        wrapper.last("LIMIT 1");
+        return this.getOne(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void updateMarkingScore(StudentScoreVo score) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getMarkingScore, score.getMarkingScore());
+        lw.eq(StudentScoreEntity::getId, score.getId());
+        this.update(wrapper);
+    }
+
+    @Override
+    public List<StudentScoreVo> getInfoByExam(Long examId) {
+        return this.baseMapper.getInfoByExam(examId);
+    }
+
+    @Override
+    public List<StudentScoreVo> getAllInfoForUpdateScore() {
+        return this.baseMapper.getAllInfoForUpdateScore();
+    }
+
+    @Override
+    public void updateMarkingScoreAndTrack(StudentScoreVo score) {
+        UpdateWrapper<StudentScoreEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentScoreEntity> lw = wrapper.lambda();
+        lw.set(StudentScoreEntity::getMarkingScore, score.getMarkingScore());
+        lw.set(StudentScoreEntity::getImagePosition, JSONArray.toJSONString(score.getIps()));
+        lw.eq(StudentScoreEntity::getId, score.getId());
+        this.update(wrapper);
+    }
+}

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

@@ -0,0 +1,140 @@
+package cn.com.qmth.am.service.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+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 org.springframework.transaction.annotation.Transactional;
+
+import com.qmth.boot.core.exception.StatusException;
+
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.bean.StudentScoreVo;
+import cn.com.qmth.am.bean.StudentVo;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.entity.QuestionEntity;
+import cn.com.qmth.am.enums.AnswerRangeType;
+import cn.com.qmth.am.enums.DataStatus;
+import cn.com.qmth.am.service.QuestionService;
+import cn.com.qmth.am.service.StmmsService;
+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);
+
+    @Autowired
+    private StudentService studentService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private StmmsService stmmsService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Override
+    public void importStudent() {
+        List<QuestionEntity> qs = questionService.findToDispose();
+        if (CollectionUtils.isEmpty(qs)) {
+            return;
+        }
+        Map<String, List<QuestionEntity>> qmap = new LinkedHashMap<>();
+        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);
+        }
+        for (List<QuestionEntity> qlist : qmap.values()) {
+            List<StudentVo> students = stmmsService.getUploadStudent(qlist.get(0).getExamId(),
+                    qlist.get(0).getSubjectCode(), qlist.get(0).getMainNumber(), sysProperty.getStudentCount());
+            if (CollectionUtils.isEmpty(students)) {
+                continue;
+            }
+            updateQuestionsStatus(qlist, DataStatus.PROCESSING);
+            try {
+                studentScoreService.saveByQuestion(qlist, students);
+                updateQuestionsStatus(qlist, DataStatus.SUCCESS);
+            } catch (Exception e) {
+                log.error("同步考生出错", e);
+                updateQuestionsStatus(qlist, DataStatus.FAILED);
+            }
+        }
+    }
+
+    private void updateQuestionsStatus(List<QuestionEntity> qlist, DataStatus st) {
+        for (QuestionEntity q : qlist) {
+            questionService.updateStatus(q.getId(), st);
+        }
+    }
+
+    @Override
+    public void buildImage(OcrDto ocrDto) {
+        if (DataStatus.WAITING.equals(ocrDto.getScore().getAnswerStatus())
+                || DataStatus.FAILED.equals(ocrDto.getScore().getAnswerStatus())) {
+            if (!AnswerRangeType.ALL.equals(ocrDto.getQuetion().getAnswerRangeType())) {
+                return;
+            }
+            studentService.createSlice(ocrDto);
+        }
+    }
+
+    @Override
+    public void createSlice(OcrDto ocrDto) {
+        if (ocrDto.getQuetion() == null) {
+            studentScoreService.updateAnswerErr(ocrDto.getScore().getId(), "未找到试题信息");
+            return;
+        }
+        try {
+            studentScoreService.createSlice(ocrDto);
+        } catch (Exception e) {
+            if (e instanceof StatusException) {
+                studentScoreService.updateAnswerErr(ocrDto.getScore().getId(), e.getMessage());
+            } else {
+                log.error("系统异常", e);
+                studentScoreService.updateAnswerErr(ocrDto.getScore().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);
+    }
+
+    @Override
+    public void updateMarkingScore(QuestionEntity q, StudentScoreVo score) {
+        List<Double> list = stmmsService.getMarkScore(score.getStudentId(), q.getMainNumber());
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        score.setMarkingScore(list.get(0));
+        studentScoreService.updateMarkingScore(score);
+    }
+
+}

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

@@ -0,0 +1,103 @@
+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.bean.ModelSpeed;
+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;
+
+    @Autowired
+    private ModelSpeed modelSpeed;
+
+    @PostConstruct
+    public void initExecutor() {
+        int threadCount = sysProperty.getMarkingThreadCount();
+        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 = 1 * 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)) {
+            modelSpeed.markingStart();
+            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);
+            }
+            modelSpeed.markingEnd();
+        }
+    }
+
+}

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

@@ -0,0 +1,131 @@
+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.bean.ModelSpeed;
+import cn.com.qmth.am.bean.OcrDto;
+import cn.com.qmth.am.config.OcrServerCfg;
+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;
+
+    @Autowired
+    private OcrServerCfg ocrServerCfg;
+
+    @Autowired
+    private ModelSpeed modelSpeed;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        int threadCount = sysProperty.getOcrThreadCount();
+        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() || CollectionUtils.isEmpty(ocrServerCfg.getOcrConfig())) {
+            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;
+            }
+            modelSpeed.ocrStart();
+            this.dispose(scores, qs);
+            modelSpeed.ocrEnd();
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.OCR.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    private void dispose(List<StudentScoreEntity> scores, List<QuestionEntity> qs) {
+        Map<Long, QuestionEntity> qmap = new HashMap<>();
+        for (QuestionEntity q : qs) {
+            qmap.put(q.getId(), q);
+        }
+        int index = 0;
+        CountDownLatch endGate = new CountDownLatch(scores.size());
+        for (StudentScoreEntity score : scores) {
+            if (!sysProperty.getOcrTaskEnable()) {
+                return;
+            }
+            QuestionEntity q = qmap.get(score.getQuestionId());
+            if (q != null) {
+                OcrConsumer com = SpringContextHolder.getBean(OcrConsumer.class);
+                OcrDto dto = new OcrDto(score, q, ocrServerCfg.getOcrConfig().get(index));
+                com.setOcrDto(dto);
+                com.setEndGate(endGate);
+                executor.execute(com);
+                index = (index + 1) % ocrServerCfg.getOcrConfig().size();
+            }
+        }
+        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();
+            }
+        }
+    }
+
+}

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

@@ -0,0 +1,100 @@
+package cn.com.qmth.am.task;
+
+import java.util.LinkedHashMap;
+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.bean.StudentScoreVo;
+import cn.com.qmth.am.consumer.UpdateMarkingScoreConsumer;
+import cn.com.qmth.am.entity.QuestionEntity;
+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 StudentScoreImportJob {
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        int threadCount = 8;
+        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() {
+        boolean lock = concurrentService.getReadWriteLock(LockType.MARKING_SCORE_IMPORT.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            List<QuestionEntity> qs = questionService.list();
+            if (CollectionUtils.isEmpty(qs)) {
+                return;
+            }
+            Map<Long, QuestionEntity> qmap = new LinkedHashMap<>();
+            for (QuestionEntity q : qs) {
+                qmap.put(q.getId(), q);
+            }
+            List<StudentScoreVo> vos = studentScoreService.getAllInfoForUpdateScore();
+            if (CollectionUtils.isEmpty(vos)) {
+                return;
+            }
+            CountDownLatch endGate = new CountDownLatch(vos.size());
+            for (StudentScoreVo vo : vos) {
+                UpdateMarkingScoreConsumer com = SpringContextHolder.getBean(UpdateMarkingScoreConsumer.class);
+                com.setVo(vo);
+                com.setQuestion(qmap.get(vo.getQuestionId()));
+                com.setEndGate(endGate);
+                executor.execute(com);
+            }
+            try {
+                endGate.await();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        } 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);
+}

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

@@ -0,0 +1,214 @@
+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() + "%";
+    }
+
+    public static double scale(double v1, int len) {
+        BigDecimal b1 = new BigDecimal(v1);
+        return b1.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;
+    }
+
+}

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

@@ -0,0 +1,47 @@
+package cn.com.qmth.am.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import cn.com.qmth.am.bean.ds.AutoScoreRequest;
+import cn.com.qmth.am.enums.PromptTemplate;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+public class FreeMarkerUtil {
+
+    private static final String ENCODING = "UTF-8";
+
+    private static Configuration cfg;
+
+    private static Map<PromptTemplate, Template> ts = new HashMap<>();
+
+    static {
+
+        try {
+            cfg = new Configuration(Configuration.VERSION_2_3_25);
+            // 设置编码
+            cfg.setDefaultEncoding(ENCODING);
+            cfg.setDirectoryForTemplateLoading(new File("./templates"));
+            for (PromptTemplate p : PromptTemplate.values()) {
+                ts.put(p, cfg.getTemplate(p.getCode()));
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String getMarkingReq(AutoScoreRequest req, PromptTemplate p) {
+        StringWriter result = null;
+        try {
+            result = new StringWriter();
+            ts.get(p).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();
+    }
+
+}

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

@@ -0,0 +1,423 @@
+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.Arrays;
+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;
+
+import cn.com.qmth.am.bean.ImageSize;
+
+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) {
+            }
+        }
+    }
+
+    public static ImageSize getSize(byte[] iamge) {
+        InputStream is = null;
+        try {
+            byte[] bytes = Arrays.copyOf(iamge, iamge.length);
+            is = new ByteArrayInputStream(bytes);
+            BufferedImage image = ImageIO.read(is);
+            int width = image.getWidth();
+            int height = image.getHeight();
+            return new ImageSize(width, height);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (is != null) {
+                try {
+                    is.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;
+    }
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov