xiatian 2 gadi atpakaļ
revīzija
ac7df3494c
45 mainītis faili ar 2705 papildinājumiem un 0 dzēšanām
  1. 50 0
      .gitignore
  2. 123 0
      pom.xml
  3. 4 0
      shell/1-管理员用户导入.bat
  4. 5 0
      shell/10-将配置文件内的中文转为Unicode编码.bat
  5. 4 0
      shell/2-考试导入.bat
  6. 4 0
      shell/3-课程导入.bat
  7. 4 0
      shell/4-考生导入.bat
  8. 4 0
      shell/5-考生数量检查.bat
  9. 4 0
      shell/6-扫描图片检查&修复.bat
  10. 4 0
      shell/7-考生客观题作答阅卷数据导出.bat
  11. 4 0
      shell/8-考生清理.bat
  12. 4 0
      shell/9-Excel转为txt文件.bat
  13. 12 0
      src/main/java/cn/com/qmth/scancloud/tools/ToolApplication.java
  14. 46 0
      src/main/java/cn/com/qmth/scancloud/tools/config/ApplicationRunListener.java
  15. 121 0
      src/main/java/cn/com/qmth/scancloud/tools/config/SysProperty.java
  16. 43 0
      src/main/java/cn/com/qmth/scancloud/tools/config/TaskConfig.java
  17. 11 0
      src/main/java/cn/com/qmth/scancloud/tools/enums/ExamMode.java
  18. 18 0
      src/main/java/cn/com/qmth/scancloud/tools/enums/ExamType.java
  19. 15 0
      src/main/java/cn/com/qmth/scancloud/tools/enums/RoleType.java
  20. 51 0
      src/main/java/cn/com/qmth/scancloud/tools/enums/TaskType.java
  21. 42 0
      src/main/java/cn/com/qmth/scancloud/tools/model/Course.java
  22. 166 0
      src/main/java/cn/com/qmth/scancloud/tools/model/Exam.java
  23. 223 0
      src/main/java/cn/com/qmth/scancloud/tools/model/ExamStudent.java
  24. 42 0
      src/main/java/cn/com/qmth/scancloud/tools/model/Subject.java
  25. 68 0
      src/main/java/cn/com/qmth/scancloud/tools/model/User.java
  26. 34 0
      src/main/java/cn/com/qmth/scancloud/tools/service/AbstractTask.java
  27. 31 0
      src/main/java/cn/com/qmth/scancloud/tools/service/CommonService.java
  28. 130 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/CourseImportTask.java
  29. 180 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamImportTask.java
  30. 35 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentCleanTask.java
  31. 50 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentCountTask.java
  32. 193 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentImportTask.java
  33. 96 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExcelToTxtTask.java
  34. 114 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ObjectiveQuestionExportTask.java
  35. 124 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/ScanImageCheckTask.java
  36. 136 0
      src/main/java/cn/com/qmth/scancloud/tools/service/impl/UserImportTask.java
  37. 92 0
      src/main/java/cn/com/qmth/scancloud/tools/utils/FileHelper.java
  38. 91 0
      src/main/java/cn/com/qmth/scancloud/tools/utils/HttpClientBuilder.java
  39. 39 0
      src/main/java/cn/com/qmth/scancloud/tools/utils/HttpHelper.java
  40. 94 0
      src/main/java/cn/com/qmth/scancloud/tools/utils/JsonHelper.java
  41. 14 0
      src/main/java/cn/com/qmth/scancloud/tools/utils/StatusException.java
  42. 2 0
      src/main/resources/META-INF/spring.factories
  43. 27 0
      src/main/resources/application.properties
  44. 121 0
      src/test/java/cn/com/qmth/scancloud/tools/DemoTest.java
  45. 30 0
      src/test/java/cn/com/qmth/scancloud/tools/TaskTest.java

+ 50 - 0
.gitignore

@@ -0,0 +1,50 @@
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+*.class
+*.log
+
+
+### Eclipse & STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+
+### 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
+

+ 123 - 0
pom.xml

@@ -0,0 +1,123 @@
+<?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.scancloud</groupId>
+    <artifactId>cet-tools</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>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.12.0</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.11.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.1-jre</version>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.0.5</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <layout>ZIP</layout>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <testFailureIgnore>true</testFailureIgnore>
+                    <skipTests>true</skipTests>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <!--<repositories>
+        <repository>
+            <id>nexus</id>
+            <name>nexus</name>
+            <url>http://192.168.10.201:8081/repository/maven-public/</url>
+        </repository>
+    </repositories>
+    <pluginRepositories>
+        <pluginRepository>
+            <id>nexus</id>
+            <name>nexus</name>
+            <url>http://192.168.10.201:8081/repository/maven-public/</url>
+        </pluginRepository>
+    </pluginRepositories>-->
+
+</project>

+ 4 - 0
shell/1-管理员用户导入.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=USER_IMPORT
+
+pause

+ 5 - 0
shell/10-将配置文件内的中文转为Unicode编码.bat

@@ -0,0 +1,5 @@
+echo native2ascii -reverse
+
+call native2ascii application.properties application.properties
+
+pause

+ 4 - 0
shell/2-考试导入.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=EXAM_IMPORT
+
+pause

+ 4 - 0
shell/3-课程导入.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=COURSE_IMPORT
+
+pause

+ 4 - 0
shell/4-考生导入.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=EXAM_STUDENT_IMPORT
+
+pause

+ 4 - 0
shell/5-考生数量检查.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=EXAM_STUDENT_COUNT
+
+pause

+ 4 - 0
shell/6-扫描图片检查&修复.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=SCAN_IMAGE_CHECK
+
+pause

+ 4 - 0
shell/7-考生客观题作答阅卷数据导出.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=OBJECTIVE_QUESTION_EXPORT
+
+pause

+ 4 - 0
shell/8-考生清理.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=EXAM_STUDENT_CLEAN
+
+pause

+ 4 - 0
shell/9-Excel转为txt文件.bat

@@ -0,0 +1,4 @@
+
+call java -server -Xms512m -Xmx512m -jar tools.jar --scan.tool.stop=true --scan.tool.taskType=EXCEL_TO_TXT
+
+pause

+ 12 - 0
src/main/java/cn/com/qmth/scancloud/tools/ToolApplication.java

@@ -0,0 +1,12 @@
+package cn.com.qmth.scancloud.tools;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ToolApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ToolApplication.class, args);
+    }
+
+}

+ 46 - 0
src/main/java/cn/com/qmth/scancloud/tools/config/ApplicationRunListener.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.scancloud.tools.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringApplicationRunListener;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+public class ApplicationRunListener implements SpringApplicationRunListener, Ordered {
+
+    private static final Logger log = LoggerFactory.getLogger(ApplicationRunListener.class);
+
+    private final SpringApplication application;
+
+    public ApplicationRunListener(SpringApplication application, String[] args) {
+        this.application = application;
+    }
+
+    @Override
+    public void started(ConfigurableApplicationContext context) {
+        if (WebApplicationType.SERVLET != application.getWebApplicationType()) {
+            return;
+        }
+
+        ConfigurableEnvironment environment = context.getEnvironment();
+        log.info("started... {}", environment.getProperty("server.port"));
+
+        // 任务配置 & 执行
+        new TaskConfig().doTask();
+
+        if (StringUtils.isNotEmpty(environment.getProperty("scan.tool.taskType"))
+                && environment.getProperty("scan.tool.stop", Boolean.class, false)) {
+            context.stop();
+        }
+    }
+
+    @Override
+    public int getOrder() {
+        return LOWEST_PRECEDENCE;
+    }
+
+}

+ 121 - 0
src/main/java/cn/com/qmth/scancloud/tools/config/SysProperty.java

@@ -0,0 +1,121 @@
+package cn.com.qmth.scancloud.tools.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SysProperty {
+
+    // 设置扫描系统的接口地址前缀
+    public static String SCAN_SERVER_URL;
+
+    // 默认数据文件存放目录(导出时必要配置)
+    public static String DATA_DIR;
+
+    // 默认数据文件路径(导入时必要配置)
+    public static String DATA_FILE;
+
+    // 设置任务类型,默认为空(配了才会执行任务)
+    public static String TASK_TYPE;
+
+    // 默认学校ID,默认为1
+    public static Long SCHOOL_ID;
+
+    // 默认学校名称
+    public static String SCHOOL_NAME;
+
+    // 设置考试ID,默认为空
+    public static Long EXAM_ID;
+
+    // 设置更多参数,JSON字符串
+    public static String MORE_PARAMS;
+
+    //“卷型条码规则”
+    public static String PAPER_TYPE_BARCODE;
+
+    // 自定义导入模板的列分隔符,为空时采用默认分隔符"|",特殊字符请用双引号括起来!
+    public static String TEMPLATE_SEPARATOR;
+
+    // 自定义导入模板的列与索引位置,为空时采用默认模板,索引下标从0开始!
+    public static String TEMPLATE_COLUMNS;
+
+    // 默认线程数
+    public static Integer THREAD_SIZE;
+    
+    //年度后两位
+    public static String YEAR;
+    
+    //场次,上半年为1,下半年为2
+    public static String YEAR_HALF;
+
+    @Value("${scan.server.url}")
+    public void scanServerUrl(String scanServerUrl) {
+        SCAN_SERVER_URL = scanServerUrl;
+    }
+
+    @Value("${scan.tool.dataDir}")
+    public void dataDir(String dataDir) {
+        DATA_DIR = dataDir;
+    }
+
+    @Value("${scan.tool.dataFile}")
+    public void dataFile(String dataFile) {
+        DATA_FILE = dataFile;
+    }
+
+    @Value("${scan.tool.taskType}")
+    public void taskType(String taskType) {
+        TASK_TYPE = taskType;
+    }
+
+    @Value("${scan.tool.schoolId}")
+    public void schoolId(Long schoolId) {
+        SCHOOL_ID = schoolId;
+    }
+
+    @Value("${scan.tool.schoolName}")
+    public void schoolName(String schoolName) {
+        SCHOOL_NAME = schoolName;
+    }
+
+    @Value("${scan.tool.examId}")
+    public void examId(Long examId) {
+        EXAM_ID = examId;
+    }
+
+    @Value("${scan.tool.moreParams}")
+    public void moreParams(String moreParams) {
+        MORE_PARAMS = moreParams;
+    }
+
+    @Value("${scan.tool.paper-type-barcode}")
+    public void paperTypeBarcode(String paperTypeBarcode) {
+        PAPER_TYPE_BARCODE = paperTypeBarcode;
+    }
+
+    @Value("${scan.tool.template.separator:,}")
+    public void templateSeparator(String templateSeparator) {
+        TEMPLATE_SEPARATOR = templateSeparator;
+    }
+
+    @Value("${scan.tool.template.columns}")
+    public void templateColumns(String templateColumns) {
+        TEMPLATE_COLUMNS = templateColumns;
+    }
+
+    @Value("${scan.tool.thread.size:3}")
+    public void threadSize(Integer threadSize) {
+        THREAD_SIZE = threadSize;
+    }
+    
+    @Value("${scan.tool.year}")
+    public void year(String year) {
+        YEAR = year;
+    }
+    
+    @Value("${scan.tool.year.half}")
+    public void yearHalf(String yearHalf) {
+        YEAR_HALF = yearHalf;
+    }
+    
+}

+ 43 - 0
src/main/java/cn/com/qmth/scancloud/tools/config/TaskConfig.java

@@ -0,0 +1,43 @@
+package cn.com.qmth.scancloud.tools.config;
+
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TaskConfig {
+
+    private final static Logger log = LoggerFactory.getLogger(TaskConfig.class);
+
+    public void doTask() {
+        log.info("**************************************************");
+        log.info("scan.tool.paper-type-barcode={}", SysProperty.PAPER_TYPE_BARCODE);
+        log.info("scan.server.url={}", SysProperty.SCAN_SERVER_URL);
+        log.info("scan.tool.dataDir={}", SysProperty.DATA_DIR);
+        log.info("scan.tool.dataFile={}", SysProperty.DATA_FILE);
+        log.info("scan.tool.taskType={}", SysProperty.TASK_TYPE);
+        log.info("scan.tool.schoolId={}", SysProperty.SCHOOL_ID);
+        log.info("scan.tool.schoolName={}", SysProperty.SCHOOL_NAME);
+        log.info("scan.tool.examId={}", SysProperty.EXAM_ID);
+        log.info("scan.tool.moreParams={}", SysProperty.MORE_PARAMS);
+        log.info("scan.tool.template.separator={}", SysProperty.TEMPLATE_SEPARATOR);
+        log.info("scan.tool.template.columns={}", SysProperty.TEMPLATE_COLUMNS);
+        log.info("scan.tool.thread.size={}", SysProperty.THREAD_SIZE);
+        log.info("**************************************************");
+
+        TaskType taskType = TaskType.findByName(SysProperty.TASK_TYPE);
+        if (taskType == null) {
+            return;
+        }
+
+        AbstractTask task;
+        try {
+            task = (AbstractTask) taskType.getImpl().newInstance();
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            return;
+        }
+        task.run();
+    }
+
+}

+ 11 - 0
src/main/java/cn/com/qmth/scancloud/tools/enums/ExamMode.java

@@ -0,0 +1,11 @@
+package cn.com.qmth.scancloud.tools.enums;
+
+public enum ExamMode {
+
+    COMMON,//普通
+
+    K12,//基础教育
+
+    POSTGRADUATE;//研究生
+
+}

+ 18 - 0
src/main/java/cn/com/qmth/scancloud/tools/enums/ExamType.java

@@ -0,0 +1,18 @@
+package cn.com.qmth.scancloud.tools.enums;
+
+public enum ExamType {
+
+    学考,
+
+    CET;
+
+    public static ExamType findByName(String name) {
+        for (ExamType examType : values()) {
+            if (examType.name().equals(name)) {
+                return examType;
+            }
+        }
+        return null;
+    }
+
+}

+ 15 - 0
src/main/java/cn/com/qmth/scancloud/tools/enums/RoleType.java

@@ -0,0 +1,15 @@
+package cn.com.qmth.scancloud.tools.enums;
+
+public enum RoleType {
+
+    /**
+     * 学校管理员
+     */
+    SCHOOL_ADMIN,
+
+    /**
+     * 扫描管理员
+     */
+    SCAN_ADMIN;
+
+}

+ 51 - 0
src/main/java/cn/com/qmth/scancloud/tools/enums/TaskType.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.scancloud.tools.enums;
+
+import cn.com.qmth.scancloud.tools.service.impl.*;
+
+public enum TaskType {
+
+    USER_IMPORT("管理员用户导入", UserImportTask.class),
+
+    EXAM_IMPORT("考试导入", ExamImportTask.class),
+
+    COURSE_IMPORT("课程导入", CourseImportTask.class),
+
+    EXAM_STUDENT_IMPORT("考生导入", ExamStudentImportTask.class),
+
+    EXAM_STUDENT_COUNT("考生数量检查", ExamStudentCountTask.class),
+
+    EXAM_STUDENT_CLEAN("考生清理", ExamStudentCleanTask.class),
+
+    SCAN_IMAGE_CHECK("扫描图片检查&修复", ScanImageCheckTask.class),
+
+    OBJECTIVE_QUESTION_EXPORT("考生客观题作答阅卷数据导出", ObjectiveQuestionExportTask.class),
+
+    EXCEL_TO_TXT("Excel转为txt文件", ExcelToTxtTask.class);
+
+    private String title;
+
+    private Class<?> impl;
+
+    TaskType(String title, Class<?> impl) {
+        this.title = title;
+        this.impl = impl;
+    }
+
+    public static TaskType findByName(String name) {
+        for (TaskType taskType : values()) {
+            if (taskType.name().equals(name)) {
+                return taskType;
+            }
+        }
+        return null;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public Class<?> getImpl() {
+        return impl;
+    }
+
+}

+ 42 - 0
src/main/java/cn/com/qmth/scancloud/tools/model/Course.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.scancloud.tools.model;
+
+public class Course {
+
+    private Long examId;
+
+    private String subjectCode;
+
+    private String subjectName;
+
+    
+    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 getSubjectName() {
+        return subjectName;
+    }
+
+    
+    public void setSubjectName(String subjectName) {
+        this.subjectName = subjectName;
+    }
+
+    
+}

+ 166 - 0
src/main/java/cn/com/qmth/scancloud/tools/model/Exam.java

@@ -0,0 +1,166 @@
+package cn.com.qmth.scancloud.tools.model;
+
+import java.util.List;
+
+import cn.com.qmth.scancloud.tools.enums.ExamMode;
+
+public class Exam {
+
+    private Long schoolId;//学校ID
+
+    private String schoolName;//学校名称
+
+    private Long id;//考试ID
+
+    private String name;//考试名称
+
+    private ExamMode mode;//扫描模式
+
+    private Boolean scanByPackage;//是否整袋扫描
+
+    private Boolean enableSyncVerify;//是否启用实时审核
+
+    private Boolean allowUnexistPaper;//是否允许缺页
+
+    private Integer answerFrontCardType;//答题卡正面卡格式类型,0为不限制
+
+    private Integer answerPaperNumberFigure;//答题卡序号位数
+
+    private Boolean enableSinglePageAnswer;//答题卡允许单页模式
+
+    private List<String> paperTypeBarcodeContent;//试卷类型条码内容规则
+
+    private String absentBarcodeContent;//缺考条码内容规则
+
+    
+    public Long getSchoolId() {
+        return schoolId;
+    }
+
+    
+    public void setSchoolId(Long schoolId) {
+        this.schoolId = schoolId;
+    }
+
+    
+    public String getSchoolName() {
+        return schoolName;
+    }
+
+    
+    public void setSchoolName(String schoolName) {
+        this.schoolName = schoolName;
+    }
+
+    
+    public Long getId() {
+        return id;
+    }
+
+    
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    
+    public String getName() {
+        return name;
+    }
+
+    
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    
+    public ExamMode getMode() {
+        return mode;
+    }
+
+    
+    public void setMode(ExamMode mode) {
+        this.mode = mode;
+    }
+
+    
+    public Boolean getScanByPackage() {
+        return scanByPackage;
+    }
+
+    
+    public void setScanByPackage(Boolean scanByPackage) {
+        this.scanByPackage = scanByPackage;
+    }
+
+    
+    public Boolean getEnableSyncVerify() {
+        return enableSyncVerify;
+    }
+
+    
+    public void setEnableSyncVerify(Boolean enableSyncVerify) {
+        this.enableSyncVerify = enableSyncVerify;
+    }
+
+    
+    public Boolean getAllowUnexistPaper() {
+        return allowUnexistPaper;
+    }
+
+    
+    public void setAllowUnexistPaper(Boolean allowUnexistPaper) {
+        this.allowUnexistPaper = allowUnexistPaper;
+    }
+
+    
+    public Integer getAnswerFrontCardType() {
+        return answerFrontCardType;
+    }
+
+    
+    public void setAnswerFrontCardType(Integer answerFrontCardType) {
+        this.answerFrontCardType = answerFrontCardType;
+    }
+
+    
+    public Integer getAnswerPaperNumberFigure() {
+        return answerPaperNumberFigure;
+    }
+
+    
+    public void setAnswerPaperNumberFigure(Integer answerPaperNumberFigure) {
+        this.answerPaperNumberFigure = answerPaperNumberFigure;
+    }
+
+    
+    public Boolean getEnableSinglePageAnswer() {
+        return enableSinglePageAnswer;
+    }
+
+    
+    public void setEnableSinglePageAnswer(Boolean enableSinglePageAnswer) {
+        this.enableSinglePageAnswer = enableSinglePageAnswer;
+    }
+
+    
+    public List<String> getPaperTypeBarcodeContent() {
+        return paperTypeBarcodeContent;
+    }
+
+    
+    public void setPaperTypeBarcodeContent(List<String> paperTypeBarcodeContent) {
+        this.paperTypeBarcodeContent = paperTypeBarcodeContent;
+    }
+
+    
+    public String getAbsentBarcodeContent() {
+        return absentBarcodeContent;
+    }
+
+    
+    public void setAbsentBarcodeContent(String absentBarcodeContent) {
+        this.absentBarcodeContent = absentBarcodeContent;
+    }
+
+    
+}

+ 223 - 0
src/main/java/cn/com/qmth/scancloud/tools/model/ExamStudent.java

@@ -0,0 +1,223 @@
+package cn.com.qmth.scancloud.tools.model;
+
+import java.util.List;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+
+@ColumnWidth(18)
+public class ExamStudent {
+
+    @ExcelIgnore
+    private Long id;//考生ID
+
+    @ExcelIgnore
+    private Long examId;//考试ID
+
+    @ExcelProperty("科目代码")
+    private String subjectCode;//科目代码
+
+    @ExcelProperty("科目名称")
+    private String subjectName;//科目名称
+
+    @ExcelProperty("准考证号")
+    private String examNumber;//准考证号
+
+    @ExcelProperty("考生姓名")
+    private String name;//考生姓名
+
+    @ExcelProperty("考生编号")
+    private String studentCode;//考生编号
+
+    @ExcelProperty("卷袋号")
+    private String packageCode;//卷袋号
+
+    @ExcelProperty("座位号")
+    private String seatNumber;//座位号
+
+    private String campusCode;
+    
+    @ExcelProperty("学习中心")
+    private String campusName;//学习中心
+
+    @ExcelProperty("考场")
+    private String examSite;//考场
+    
+    private String examSiteName;
+
+    @ExcelProperty("考点")
+    private String examRoom;//考点
+
+    @ExcelIgnore
+    private String paperType;//卷型
+
+    @ExcelIgnore
+    private List<String> answer;//客观题作答阅卷数据
+
+    
+    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 String getSubjectName() {
+        return subjectName;
+    }
+
+    
+    public void setSubjectName(String subjectName) {
+        this.subjectName = subjectName;
+    }
+
+    
+    public String getExamNumber() {
+        return examNumber;
+    }
+
+    
+    public void setExamNumber(String examNumber) {
+        this.examNumber = examNumber;
+    }
+
+    
+    public String getName() {
+        return name;
+    }
+
+    
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    
+    public String getStudentCode() {
+        return studentCode;
+    }
+
+    
+    public void setStudentCode(String studentCode) {
+        this.studentCode = studentCode;
+    }
+
+    
+    public String getPackageCode() {
+        return packageCode;
+    }
+
+    
+    public void setPackageCode(String packageCode) {
+        this.packageCode = packageCode;
+    }
+
+    
+    public String getSeatNumber() {
+        return seatNumber;
+    }
+
+    
+    public void setSeatNumber(String seatNumber) {
+        this.seatNumber = seatNumber;
+    }
+
+    
+    public String getCampusName() {
+        return campusName;
+    }
+
+    
+    public void setCampusName(String campusName) {
+        this.campusName = campusName;
+    }
+
+    
+    public String getExamSite() {
+        return examSite;
+    }
+
+    
+    public void setExamSite(String examSite) {
+        this.examSite = examSite;
+    }
+
+    
+    public String getExamRoom() {
+        return examRoom;
+    }
+
+    
+    public void setExamRoom(String examRoom) {
+        this.examRoom = examRoom;
+    }
+
+    
+    public String getPaperType() {
+        return paperType;
+    }
+
+    
+    public void setPaperType(String paperType) {
+        this.paperType = paperType;
+    }
+
+    
+    public List<String> getAnswer() {
+        return answer;
+    }
+
+    
+    public void setAnswer(List<String> answer) {
+        this.answer = answer;
+    }
+
+
+    
+    public String getExamSiteName() {
+        return examSiteName;
+    }
+
+
+    
+    public void setExamSiteName(String examSiteName) {
+        this.examSiteName = examSiteName;
+    }
+
+
+    
+    public String getCampusCode() {
+        return campusCode;
+    }
+
+
+    
+    public void setCampusCode(String campusCode) {
+        this.campusCode = campusCode;
+    }
+
+    
+}

+ 42 - 0
src/main/java/cn/com/qmth/scancloud/tools/model/Subject.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.scancloud.tools.model;
+
+public class Subject {
+
+    private Long examId;
+
+    private String code;
+
+    private String name;
+
+    
+    public Long getExamId() {
+        return examId;
+    }
+
+    
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    
+    public String getCode() {
+        return code;
+    }
+
+    
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    
+    public String getName() {
+        return name;
+    }
+
+    
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    
+}

+ 68 - 0
src/main/java/cn/com/qmth/scancloud/tools/model/User.java

@@ -0,0 +1,68 @@
+package cn.com.qmth.scancloud.tools.model;
+
+import cn.com.qmth.scancloud.tools.enums.RoleType;
+
+public class User {
+
+    private Long schoolId;
+
+    private String loginName;
+
+    private String name;
+
+    private String password;
+
+    private RoleType role;
+
+    
+    public Long getSchoolId() {
+        return schoolId;
+    }
+
+    
+    public void setSchoolId(Long schoolId) {
+        this.schoolId = schoolId;
+    }
+
+    
+    public String getLoginName() {
+        return loginName;
+    }
+
+    
+    public void setLoginName(String loginName) {
+        this.loginName = loginName;
+    }
+
+    
+    public String getName() {
+        return name;
+    }
+
+    
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    
+    public String getPassword() {
+        return password;
+    }
+
+    
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    
+    public RoleType getRole() {
+        return role;
+    }
+
+    
+    public void setRole(RoleType role) {
+        this.role = role;
+    }
+
+    
+}

+ 34 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/AbstractTask.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.scancloud.tools.service;
+
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(AbstractTask.class);
+
+    public void run() {
+        long start = System.currentTimeMillis();
+
+        try {
+            System.out.println();
+            log.info("【{}】任务开始...", this.getTaskName());
+
+            this.execute();
+        } catch (StatusException e) {
+            log.error("【{}】任务失败,原因:{}", this.getTaskName(), e.getMessage());
+        } catch (Exception e) {
+            log.error("【{}】任务异常!", this.getTaskName(), e);
+        } finally {
+            long cost = (System.currentTimeMillis() - start) / 1000;
+            log.info("【{}】任务结束... 耗时:{}秒", this.getTaskName(), cost);
+            System.out.println();
+        }
+    }
+
+    protected abstract String getTaskName();
+
+    protected abstract void execute();
+
+}

+ 31 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/CommonService.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.scancloud.tools.service;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.model.Exam;
+import cn.com.qmth.scancloud.tools.model.Subject;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CommonService {
+
+    public static Exam findExam(Long examId) {
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/info?examId=" + examId;
+        String result = HttpHelper.post(url, null);
+        return JsonHelper.toObj(result, Exam.class);
+    }
+
+    public static Subject findCourse(Long examId, String subjectCode) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("examId", examId);
+        params.put("subjectCode", subjectCode);
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/subject/info";
+        String result = HttpHelper.post(url, JsonHelper.toJson(params));
+
+        return JsonHelper.toObj(result, Subject.class);
+    }
+
+}

+ 130 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/CourseImportTask.java

@@ -0,0 +1,130 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.Course;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.service.CommonService;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.io.LineIterator;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 课程导入
+ */
+public class CourseImportTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(CourseImportTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.COURSE_IMPORT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        if (CommonService.findExam(examId) == null) {
+            throw new StatusException("当前考试不存在!examId = " + examId);
+        }
+
+        if (!SysProperty.DATA_FILE.endsWith(".txt")) {
+            throw new StatusException("导入模板目前仅支持后缀名为“.txt”的文件!");
+        }
+
+        // 自定义模板 列分隔符
+        String templateSeparator = StringUtils.isNotEmpty(SysProperty.TEMPLATE_SEPARATOR) ? SysProperty.TEMPLATE_SEPARATOR : "|";
+        // 自定义模板 列与索引位置
+        Map<String, Integer> templateColumns = JsonHelper.toMap(SysProperty.TEMPLATE_COLUMNS, Integer.class);
+
+        int total = 0;
+        List<Course> list = new ArrayList<>();
+        File file = new File(SysProperty.DATA_FILE);
+        try (LineIterator lines = FileHelper.readLines(file);) {
+            while (lines.hasNext()) {
+                String line = lines.nextLine();
+
+                Course data = this.parseValues(total, line, templateColumns, templateSeparator);
+                if (data != null) {
+                    data.setExamId(examId);
+                    list.add(data);
+                }
+
+                total++;
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+
+        log.info("导入文件全部行数共 {} 行,实际有效数据共 {} 条!", total, list.size());
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/import/course";
+        String json = JsonHelper.toJson(list);
+        String result = HttpHelper.post(url, json);
+        log.info("已处理数:{} 进度:100% {}", list.size(), result);
+    }
+
+    private Course parseValues(int index, String line, Map<String, Integer> templateColumns, String templateSeparator) {
+        if (StringUtils.isBlank(line)) {
+            return null;
+        }
+
+        line = StringUtils.deleteWhitespace(line);
+        String[] values = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, templateSeparator);
+
+        try {
+            String subjectCode = null;
+            String subjectName = null;
+
+            if (MapUtils.isEmpty(templateColumns)) {
+                // 默认模板:科目代码|科目名称
+                subjectCode = values[0];
+                subjectName = values[1];
+            } else {
+                // 自定义模板
+                if (templateColumns.get("科目代码") != null) {
+                    subjectCode = values[templateColumns.get("科目代码")];
+                }
+                if (templateColumns.get("科目名称") != null) {
+                    subjectName = values[templateColumns.get("科目名称")];
+                }
+            }
+
+            if (StringUtils.isEmpty(subjectCode)) {
+                throw new StatusException(String.format("【第%s行】“科目代码”字段不能为空! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(subjectName)) {
+                throw new StatusException(String.format("【第%s行】“科目名称”字段不能为空! %s", index + 1, line));
+            }
+
+            Course data = new Course();
+            data.setSubjectCode(subjectCode);
+            data.setSubjectName(subjectName);
+            return data;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            log.error("【第{}行】内容格式错误! {} {}", index + 1, line, e.toString());
+            throw new StatusException("数据内容有误!");
+        }
+    }
+
+}

+ 180 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamImportTask.java

@@ -0,0 +1,180 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.ExamMode;
+import cn.com.qmth.scancloud.tools.enums.ExamType;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.Exam;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 考试导入
+ */
+public class ExamImportTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamImportTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.EXAM_IMPORT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long schoolId = SysProperty.SCHOOL_ID;
+        if (schoolId == null) {
+            throw new StatusException("【scan.tool.schoolId】未配置!");
+        }
+        String schoolName = SysProperty.SCHOOL_NAME;
+        if (StringUtils.isBlank(schoolName)) {
+            throw new StatusException("【scan.tool.schoolName】未配置!");
+        }
+        if (!SysProperty.DATA_FILE.endsWith(".txt")) {
+            throw new StatusException("导入模板目前仅支持后缀名为“.txt”的文件!");
+        }
+
+        // 自定义模板 列分隔符
+        String templateSeparator = StringUtils.isNotEmpty(SysProperty.TEMPLATE_SEPARATOR) ? SysProperty.TEMPLATE_SEPARATOR : "|";
+        // 自定义模板 列与索引位置
+        Map<String, Integer> templateColumns = JsonHelper.toMap(SysProperty.TEMPLATE_COLUMNS, Integer.class);
+
+        File file = new File(SysProperty.DATA_FILE);
+        List<String> lines = FileHelper.readAllLines(file);
+        if (CollectionUtils.isEmpty(lines)) {
+            log.warn("导入文件内无数据!");
+            return;
+        }
+
+        List<Exam> list = new ArrayList<>();
+        for (int n = 0; n < lines.size(); n++) {
+            Exam data = this.parseValues(n, lines.get(n), templateColumns, templateSeparator);
+            if (data != null) {
+                data.setSchoolId(schoolId);
+                data.setSchoolName(schoolName);
+                list.add(data);
+            }
+        }
+
+        log.info("导入文件全部行数共 {} 行,实际有效数据共 {} 条!", lines.size(), list.size());
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/import/exam";
+        for (int n = 0; n < list.size(); n++) {
+            String json = JsonHelper.toJson(list.get(n));
+            String result = HttpHelper.post(url, json);
+
+            float rate = (n + 1) * 100f / list.size();
+            log.info("已处理数:{} 进度:{}% {}", n + 1, rate, result);
+        }
+    }
+
+    private Exam parseValues(int index, String line, Map<String, Integer> templateColumns, String templateSeparator) {
+        if (StringUtils.isBlank(line)) {
+            return null;
+        }
+
+        line = StringUtils.deleteWhitespace(line);
+        String[] values = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, templateSeparator);
+
+        try {
+            Long examId = null;
+            String examName = null;
+            ExamType examType = null;
+
+            if (MapUtils.isEmpty(templateColumns)) {
+                // 默认模板:考试ID|考试名称|考试类型
+                examId = this.parseLong(values[0]);
+                examName = values[1];
+                examType = ExamType.findByName(values[2]);
+            } else {
+                // 自定义模板
+                if (templateColumns.get("考试ID") != null) {
+                    examId = this.parseLong(values[templateColumns.get("考试ID")]);
+                }
+                if (templateColumns.get("考试名称") != null) {
+                    examName = values[templateColumns.get("考试名称")];
+                }
+                if (templateColumns.get("考试类型") != null) {
+                    examType = ExamType.findByName(values[templateColumns.get("考试类型")]);
+                }
+            }
+
+            if (examId == null) {
+                throw new StatusException(String.format("【第%s行】“考试ID”字段值有误! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(examName)) {
+                throw new StatusException(String.format("【第%s行】“考试名称”字段不能为空! %s", index + 1, line));
+            }
+            if (examType == null) {
+                throw new StatusException(String.format("【第%s行】“考试类型”字段值有误! %s", index + 1, line));
+            }
+
+            Exam data = new Exam();
+            data.setId(examId);
+            data.setName(examName);
+
+            if (ExamType.CET == examType) {
+                data.setMode(ExamMode.K12);
+                data.setScanByPackage(true);
+                data.setEnableSyncVerify(false);
+                data.setAllowUnexistPaper(false);
+                data.setAnswerPaperNumberFigure(0);
+                data.setAnswerFrontCardType(1);
+                data.setEnableSinglePageAnswer(false);
+
+                if (StringUtils.isNotBlank(SysProperty.PAPER_TYPE_BARCODE)) {
+                    data.setPaperTypeBarcodeContent(Arrays.asList(SysProperty.PAPER_TYPE_BARCODE.split(",")));
+                }
+
+                // CET时“卷型条码规则”不能为空,且必须为6位数字!
+                if (CollectionUtils.isEmpty(data.getPaperTypeBarcodeContent())) {
+                    throw new StatusException("【exam.cet.params】配置“卷型条码规则”为空!");
+                }
+                for (String barcode : data.getPaperTypeBarcodeContent()) {
+                    if (StringUtils.length(barcode) != 6 || parseLong(barcode) == null) {
+                        throw new StatusException("【exam.cet.params】配置“卷型条码规则”值必须为6位数字!");
+                    }
+                }
+            }
+
+            if (data.getAbsentBarcodeContent() == null) {
+                data.setAbsentBarcodeContent("");
+            }
+
+            if (data.getPaperTypeBarcodeContent() == null) {
+                data.setPaperTypeBarcodeContent(new ArrayList<>());
+            }
+
+            return data;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            log.error("【第{}行】内容格式错误! {} {}", index + 1, line, e.toString());
+            throw new StatusException("数据内容有误!");
+        }
+    }
+
+    private Long parseLong(String str) {
+        try {
+            return Long.parseLong(str);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+}

+ 35 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentCleanTask.java

@@ -0,0 +1,35 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 考生清理
+ */
+public class ExamStudentCleanTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamStudentCleanTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.EXAM_STUDENT_CLEAN.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/clean?examId=" + examId;
+        HttpHelper.post(url, null);
+        log.info("考生已清空!examId = " + examId);
+    }
+
+}

+ 50 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentCountTask.java

@@ -0,0 +1,50 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 考生数量检查
+ */
+public class ExamStudentCountTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamStudentCountTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.EXAM_STUDENT_COUNT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        // 查询参数:{"examId":null,"subjectCode":"","examNumber":"","name":"","studentCode":"","packageCode":"",
+        // "seatNumber":"","campusName":"","examSite":"","examRoom":""}
+        Map<String, Object> params = JsonHelper.toMap(SysProperty.MORE_PARAMS, Object.class);
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        params.put("examId", examId);
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/count";
+        String json = JsonHelper.toJson(params);
+        log.info("查询考生数量参数:{}", json);
+
+        String result = HttpHelper.post(url, json);
+        log.info("查询考生数量结果:{}", result);
+    }
+
+}

+ 193 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExamStudentImportTask.java

@@ -0,0 +1,193 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.io.LineIterator;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.ExamStudent;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.service.CommonService;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+
+/**
+ * 考生导入
+ */
+public class ExamStudentImportTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamStudentImportTask.class);
+    private static Pattern examNumberRex = Pattern.compile("^[0-9]{15}$");
+    @Override
+    protected String getTaskName() {
+        return TaskType.EXAM_STUDENT_IMPORT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        if (CommonService.findExam(examId) == null) {
+            throw new StatusException("当前考试不存在!examId = " + examId);
+        }
+
+        if (!SysProperty.DATA_FILE.endsWith(".txt")) {
+            throw new StatusException("导入模板目前仅支持后缀名为“.txt”的文件!");
+        }
+
+        int total = 0;
+        List<ExamStudent> list = new ArrayList<>();
+        Set<String> subjectCodes = new HashSet<>();
+        Set<String> examNumbers = new HashSet<>();
+
+        File file = new File(SysProperty.DATA_FILE);
+        try (LineIterator lines = FileHelper.readLines(file);) {
+            while (lines.hasNext()) {
+                String line = lines.nextLine();
+
+                ExamStudent data = this.parseValues(total, line, examNumbers);
+                if (data != null) {
+                    data.setExamId(examId);
+                    list.add(data);
+
+                    subjectCodes.add(data.getSubjectCode());
+
+                    // 同个考试下考生的“准考证号”值唯一
+                    examNumbers.add(data.getExamNumber());
+                }
+
+                total++;
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+
+        log.info("导入文件全部行数共 {} 行,实际有效数据共 {} 条!", total, list.size());
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+
+        // 校验所有科目代码
+        boolean allExist = true;
+        for (String subjectCode : subjectCodes) {
+            if (CommonService.findCourse(examId, subjectCode) == null) {
+                log.warn("科目不存在! examId={} subjectCode={}", examId, subjectCode);
+                allExist = false;
+            }
+        }
+        if (!allExist) {
+            throw new StatusException("科目信息有误!");
+        }
+
+        // 分批保存
+        int batchSize = 5000;
+        List<ExamStudent> batchList = new ArrayList<>();
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/import/exam/student";
+        for (int n = 0; n < list.size(); n++) {
+            batchList.add(list.get(n));
+
+            if (batchList.size() % batchSize == 0) {
+                String json = JsonHelper.toJson(batchList);
+                String result = HttpHelper.post(url, json);
+                batchList.clear();
+
+                float rate = (n + 1) * 100f / list.size();
+                log.info("已处理数:{} 进度:{}% {}", n + 1, rate, result);
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(batchList)) {
+            String json = JsonHelper.toJson(batchList);
+            String result = HttpHelper.post(url, json);
+            log.info("已处理数:{} 进度:100% {}", list.size(), result);
+        }
+    }
+
+    private ExamStudent parseValues(int index, String line, Set<String> examNumbers) {
+        if (StringUtils.isBlank(line)) {
+            return null;
+        }
+
+        line = StringUtils.deleteWhitespace(line);
+        String[] values = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, SysProperty.TEMPLATE_SEPARATOR);
+
+        try {
+
+            // 默认模板:科目代码|科目名称|准考证号|考生姓名|考生编号|卷袋号|座位号|学习中心|考场|考点
+            String examNumber = values[0].trim();
+            String name = values[1].trim();
+            String examSiteName = values[2].trim();
+
+            if (StringUtils.isEmpty(examNumber)) {
+                throw new StatusException(String.format("【第%s行】“准考证号”字段不能为空! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(name)) {
+                throw new StatusException(String.format("【第%s行】“考生姓名”字段不能为空! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(examSiteName)) {
+                throw new StatusException(String.format("【第%s行】“考点名称”字段不能为空! %s", index + 1, line));
+            }
+            if (examNumbers.contains(examNumber)) {
+                throw new StatusException(String.format("【第%s行】考生信息存在重复! %s", index + 1, line));
+            }
+            try {
+                checkExamNumber(examNumber);
+            } catch (StatusException e) {
+                throw new StatusException(String.format("【第%s行】%s! %s", index + 1,e.getMessage(), line));
+            }
+
+            ExamStudent data = new ExamStudent();
+            data.setExamNumber(examNumber);
+            data.setName(name);
+            data.setExamSite(examNumber.substring(0, 5));
+            data.setExamSiteName(examSiteName);
+            data.setCampusCode(examNumber.substring(0, 6));
+            data.setCampusName(examSiteName+examNumber.substring(5, 6));
+            data.setExamRoom(examNumber.substring(0, 6)+examNumber.substring(10, 13));
+            data.setSeatNumber(examNumber.substring(13, 15));
+            data.setPackageCode(data.getExamRoom());
+            data.setSubjectCode(examNumber.substring(9, 10));
+            data.setStudentCode(examNumber);
+
+            return data;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            log.error("【第{}行】内容格式错误! {} {}", index + 1, line, e.toString());
+            throw new StatusException("数据内容有误!");
+        }
+    }
+    
+    private void checkExamNumber(String examNumber) {
+        if(!examNumberRex.matcher(examNumber).find()) {
+            throw new StatusException("准考证号不是15位数字");
+        }
+        String year=examNumber.substring(6, 8);
+        if(!year.equals(SysProperty.YEAR)) {
+            throw new StatusException("年度不正确");
+        }
+        String yearHalf=examNumber.substring(8, 9);
+        if(!yearHalf.equals(SysProperty.YEAR_HALF)) {
+            throw new StatusException("考次不正确");
+        }
+        int subjectCode=Integer.valueOf(examNumber.substring(9, 10));
+        if(subjectCode<1||subjectCode>6) {
+            throw new StatusException("语种不正确");
+        }
+    }
+
+}

+ 96 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ExcelToTxtTask.java

@@ -0,0 +1,96 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import org.apache.commons.io.FileUtils;
+import org.apache.poi.UnsupportedFileFormatException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Excel转为txt文件
+ */
+public class ExcelToTxtTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ExcelToTxtTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.EXCEL_TO_TXT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        String fileSuffix = FileHelper.getFileSuffix(SysProperty.DATA_FILE);
+        if (!fileSuffix.endsWith(".xls") && !fileSuffix.endsWith(".xlsx")) {
+            throw new StatusException("目前仅支持后缀名为“.xls“或”.xlsx”的文件!");
+        }
+
+        File excelFile = new File(SysProperty.DATA_FILE);
+        if (!excelFile.exists()) {
+            throw new StatusException("Excel文件不存在!" + SysProperty.DATA_FILE);
+        }
+
+        String txtFilePath = SysProperty.DATA_FILE.replace(fileSuffix, ".txt");
+        File txtFile = new File(txtFilePath);
+        if (txtFile.exists()) {
+            FileUtils.deleteQuietly(txtFile);
+        }
+
+        log.warn("***** Excel文件内容的第一行默认跳过不读!!! *****");
+
+        try {
+            EasyExcel.read(excelFile, new AnalysisEventListener<Map<Integer, String>>() {
+                private int total = 0;
+
+                private final int batchSize = 10000;
+
+                private List<String> list = new ArrayList<>();
+
+                @Override
+                public void invoke(Map<Integer, String> data, AnalysisContext context) {
+                    StringBuilder line = new StringBuilder();
+                    for (int n = 0; n < data.size(); n++) {
+                        String value = data.get(n);
+                        line.append(value != null ? value : "");
+
+                        if (n < data.size() - 1) {
+                            line.append(SysProperty.TEMPLATE_SEPARATOR);
+                        }
+                    }
+                    list.add(line.toString());
+                    total++;
+
+                    if (list.size() >= batchSize) {
+                        saveData();
+                        list.clear();
+                    }
+                }
+
+                @Override
+                public void doAfterAllAnalysed(AnalysisContext context) {
+                    saveData();
+                    log.info("共 {} 行,转换完成!{}", total, txtFilePath);
+                }
+
+                private void saveData() {
+                    FileHelper.writeLines(txtFile, list, true);
+                }
+            }).sheet().doRead();
+        } catch (UnsupportedFileFormatException e) {
+            log.error("Excel文件读取失败!" + e.getMessage());
+        }
+    }
+
+}

+ 114 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ObjectiveQuestionExportTask.java

@@ -0,0 +1,114 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.ExamStudent;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 考生客观题作答阅卷数据导出
+ */
+public class ObjectiveQuestionExportTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ObjectiveQuestionExportTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.OBJECTIVE_QUESTION_EXPORT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        // 查询参数:{"examId":null,"subjectCode":"","examNumber":"","name":"","studentCode":"","packageCode":"",
+        // "seatNumber":"","campusName":"","examSite":"","examRoom":""}
+        Map<String, Object> params = JsonHelper.toMap(SysProperty.MORE_PARAMS, Object.class);
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        params.put("examId", examId);
+        params.put("scanStatus", "SCANNED");
+        params.put("withAnswer", true);
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/count";
+        String json = JsonHelper.toJson(params);
+        String result = HttpHelper.post(url, json);
+        log.info("查询考生参数:{},结果:{}", json, result);
+
+        int total = 0;
+        JsonNode props = JsonHelper.getNode(result);
+        if (props != null && props.has("count")) {
+            total = props.get("count").asInt();
+        }
+        if (total == 0) {
+            log.warn("未查询到相关考生!");
+            return;
+        }
+
+        // 分批处理
+        String filePath = String.format("%s/export-%s.txt", SysProperty.DATA_DIR, System.currentTimeMillis());
+        String queryUrl = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/find";
+        params.put("pageSize", 100);
+        int pageNumber = 0;
+        int sum = 0;
+
+        while (true) {
+            params.put("pageNumber", ++pageNumber);
+            String queryJson = JsonHelper.toJson(params);
+            String queryResult = HttpHelper.post(queryUrl, queryJson);
+
+            List<ExamStudent> list = JsonHelper.toList(queryResult, ExamStudent.class);
+            if (CollectionUtils.isEmpty(list)) {
+                break;
+            }
+
+            this.export(list, filePath);
+
+            sum += list.size();
+            float rate = sum * 100f / total;
+            log.info("已处理数:{} 进度:{}%", sum, rate);
+        }
+
+        log.info("导出文件:" + filePath);
+    }
+
+    private void export(List<ExamStudent> list, String filePath) {
+        // OW客观题结果格式:准考证号,科目代码,客观题,卷型
+        String template = "%s,%s,%s,%s";
+
+        List<String> lines = new ArrayList<>();
+        for (ExamStudent data : list) {
+            // 客观题 按“;”分割
+            String answers = CollectionUtils.isNotEmpty(data.getAnswer())
+                    ? StringUtils.join(data.getAnswer(), ";") : "";
+
+            // 如果没有卷型识别结果默认用1,有的话用实际的识别结果
+            String paperType = (StringUtils.isNotEmpty(data.getPaperType())) ? data.getPaperType() : "1";
+
+            lines.add(String.format(template, data.getExamNumber(), data.getSubjectCode(), answers, paperType));
+        }
+
+        File file = new File(filePath);
+        FileHelper.writeLines(file, lines, true);
+    }
+
+}

+ 124 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/ScanImageCheckTask.java

@@ -0,0 +1,124 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.ExamStudent;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.commons.collections4.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 扫描图片检查&修复
+ */
+public class ScanImageCheckTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(ScanImageCheckTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.SCAN_IMAGE_CHECK.getTitle();
+    }
+
+    private ExecutorService threadPool = Executors.newFixedThreadPool(SysProperty.THREAD_SIZE);
+
+    private CompletionService<Boolean> threadService = new ExecutorCompletionService<>(threadPool);
+
+    @Override
+    protected void execute() {
+        Long examId = SysProperty.EXAM_ID;
+        if (examId == null) {
+            throw new StatusException("【scan.tool.examId】未配置!");
+        }
+
+        // 查询参数:{"examId":null,"subjectCode":"","examNumber":"","name":"","studentCode":"","packageCode":"",
+        // "seatNumber":"","campusName":"","examSite":"","examRoom":""}
+        Map<String, Object> params = JsonHelper.toMap(SysProperty.MORE_PARAMS, Object.class);
+        if (params == null) {
+            params = new HashMap<>();
+        }
+        params.put("examId", examId);
+        params.put("scanStatus", "SCANNED");
+        params.put("withAnswer", false);
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/count";
+        String json = JsonHelper.toJson(params);
+        String result = HttpHelper.post(url, json);
+        log.info("查询考生参数:{},结果:{}", json, result);
+
+        int total = 0;
+        JsonNode props = JsonHelper.getNode(result);
+        if (props != null && props.has("count")) {
+            total = props.get("count").asInt();
+        }
+        if (total == 0) {
+            log.warn("未查询到相关考生!");
+            return;
+        }
+
+        // 分批处理
+        String queryUrl = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/find";
+        params.put("pageSize", 100);
+        int pageNumber = 0;
+        int sum = 0, err = 0;
+
+        while (true) {
+            params.put("pageNumber", ++pageNumber);
+            String queryJson = JsonHelper.toJson(params);
+            String queryResult = HttpHelper.post(queryUrl, queryJson);
+
+            List<ExamStudent> list = JsonHelper.toList(queryResult, ExamStudent.class);
+            if (CollectionUtils.isEmpty(list)) {
+                break;
+            }
+
+            err += this.check(list);
+            sum += list.size();
+
+            float rate = sum * 100f / total;
+            log.info("已处理数:{} 异常数:{} 进度:{}%", sum, err, rate);
+        }
+
+        threadPool.shutdown();
+    }
+
+    private int check(List<ExamStudent> list) {
+        for (ExamStudent data : list) {
+            String url = SysProperty.SCAN_SERVER_URL + "/api/tool/exam/student/check?studentId=" + data.getId();
+            threadService.submit(() -> {
+                try {
+                    HttpHelper.post(url, null);
+                    // log.info("科目代码={}, 准考证号={}, 考生编号={}, ID={}, 扫描图片正常!", data.getSubjectCode(), data.getExamNumber(), data.getStudentCode(), data.getId());
+                    return true;
+                } catch (Exception e) {
+                    log.error("科目代码={}, 准考证号={}, 考生编号={}, ID={}, 扫描图片异常!", data.getSubjectCode(), data.getExamNumber(), data.getStudentCode(), data.getId());
+                    return false;
+                }
+            });
+        }
+
+        int err = 0;
+        for (int n = 0; n < list.size(); n++) {
+            try {
+                err += threadService.take().get() ? 0 : 1;
+            } catch (Exception e) {
+                log.error(e.getMessage());
+            }
+        }
+
+        return err;
+    }
+
+}

+ 136 - 0
src/main/java/cn/com/qmth/scancloud/tools/service/impl/UserImportTask.java

@@ -0,0 +1,136 @@
+package cn.com.qmth.scancloud.tools.service.impl;
+
+import cn.com.qmth.scancloud.tools.config.SysProperty;
+import cn.com.qmth.scancloud.tools.enums.RoleType;
+import cn.com.qmth.scancloud.tools.enums.TaskType;
+import cn.com.qmth.scancloud.tools.model.User;
+import cn.com.qmth.scancloud.tools.service.AbstractTask;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import cn.com.qmth.scancloud.tools.utils.HttpHelper;
+import cn.com.qmth.scancloud.tools.utils.JsonHelper;
+import cn.com.qmth.scancloud.tools.utils.StatusException;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.io.LineIterator;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理员用户导入
+ */
+public class UserImportTask extends AbstractTask {
+
+    private static final Logger log = LoggerFactory.getLogger(UserImportTask.class);
+
+    @Override
+    protected String getTaskName() {
+        return TaskType.USER_IMPORT.getTitle();
+    }
+
+    @Override
+    protected void execute() {
+        Long schoolId = SysProperty.SCHOOL_ID;
+        if (schoolId == null) {
+            throw new StatusException("【scan.tool.schoolId】未配置!");
+        }
+        if (!SysProperty.DATA_FILE.endsWith(".txt")) {
+            throw new StatusException("导入模板目前仅支持后缀名为“.txt”的文件!");
+        }
+
+        // 自定义模板 列分隔符
+        String templateSeparator = StringUtils.isNotEmpty(SysProperty.TEMPLATE_SEPARATOR) ? SysProperty.TEMPLATE_SEPARATOR : "|";
+        // 自定义模板 列与索引位置
+        Map<String, Integer> templateColumns = JsonHelper.toMap(SysProperty.TEMPLATE_COLUMNS, Integer.class);
+
+        int total = 0;
+        List<User> list = new ArrayList<>();
+        File file = new File(SysProperty.DATA_FILE);
+        try (LineIterator lines = FileHelper.readLines(file);) {
+            while (lines.hasNext()) {
+                String line = lines.nextLine();
+
+                User data = this.parseValues(total, line, templateColumns, templateSeparator);
+                if (data != null) {
+                    data.setSchoolId(schoolId);
+                    list.add(data);
+                }
+
+                total++;
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+
+        log.info("导入文件全部行数共 {} 行,实际有效数据共 {} 条!", total, list.size());
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+
+        String url = SysProperty.SCAN_SERVER_URL + "/api/tool/import/user";
+        String json = JsonHelper.toJson(list);
+        String result = HttpHelper.post(url, json);
+        log.info("已处理数:{} 进度:100% {}", list.size(), result);
+    }
+
+    private User parseValues(int index, String line, Map<String, Integer> templateColumns, String templateSeparator) {
+        if (StringUtils.isBlank(line)) {
+            return null;
+        }
+
+        line = StringUtils.deleteWhitespace(line);
+        String[] values = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, templateSeparator);
+
+        try {
+            String name = null;
+            String loginName = null;
+            String password = null;
+
+            if (MapUtils.isEmpty(templateColumns)) {
+                // 默认模板:姓名|账号|密码
+                name = values[0];
+                loginName = values[1];
+                password = values[2];
+            } else {
+                // 自定义模板
+                if (templateColumns.get("姓名") != null) {
+                    name = values[templateColumns.get("姓名")];
+                }
+                if (templateColumns.get("账号") != null) {
+                    loginName = values[templateColumns.get("账号")];
+                }
+                if (templateColumns.get("密码") != null) {
+                    password = values[templateColumns.get("密码")];
+                }
+            }
+
+            if (StringUtils.isEmpty(loginName)) {
+                throw new StatusException(String.format("【第%s行】“账号”字段不能为空! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(password)) {
+                throw new StatusException(String.format("【第%s行】“密码”字段不能为空! %s", index + 1, line));
+            }
+            if (StringUtils.isEmpty(name)) {
+                name = loginName;
+            }
+
+            User data = new User();
+            data.setName(name);
+            data.setLoginName(loginName);
+            data.setPassword(password);
+            // 默认“学校管理员”
+            data.setRole(RoleType.SCHOOL_ADMIN);
+            return data;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            log.error("【第{}行】内容格式错误! {} {}", index + 1, line, e.toString());
+            throw new StatusException("数据内容有误!");
+        }
+    }
+
+}

+ 92 - 0
src/main/java/cn/com/qmth/scancloud/tools/utils/FileHelper.java

@@ -0,0 +1,92 @@
+package cn.com.qmth.scancloud.tools.utils;
+
+import com.google.common.io.Files;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.LineIterator;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FileHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(FileHelper.class);
+
+    /**
+     * 获取文件后缀名,包括"."
+     */
+    public static String getFileSuffix(String fileName) {
+        if (StringUtils.isNotEmpty(fileName)) {
+            int index = fileName.lastIndexOf(".");
+            if (index > 0) {
+                return fileName.substring(index).toLowerCase();
+            }
+        }
+
+        return "";
+    }
+
+    /**
+     * 创建文件目录
+     */
+    public static void makeDirs(String dirPath) {
+        if (StringUtils.isBlank(dirPath)) {
+            return;
+        }
+        File dir = new File(dirPath);
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }
+    }
+
+    /**
+     * 读取文件内容(一次性读取)
+     */
+    public static List<String> readAllLines(File file) {
+        try {
+            return Files.readLines(file, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("读取文件失败! " + e.getMessage());
+        }
+    }
+
+    /**
+     * 读取文件内容(迭代按行读取)
+     */
+    public static LineIterator readLines(File file) throws IOException {
+        return FileUtils.lineIterator(file, StandardCharsets.UTF_8.name());
+    }
+
+    /**
+     * 写入文件内容
+     */
+    public static void writeLines(File file, List<String> lines) {
+        writeLines(file, lines, false);
+    }
+
+    /**
+     * 写入文件内容(支持追加)
+     */
+    public static void writeLines(File file, List<String> lines, boolean append) {
+        if (!file.exists()) {
+            makeDirs(file.getParent());
+        }
+
+        try {
+            if (lines == null) {
+                lines = new ArrayList<>();
+            }
+            FileUtils.writeLines(file, StandardCharsets.UTF_8.name(), lines, append);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            throw new StatusException("写入文件失败! " + e.getMessage());
+        }
+    }
+
+}

+ 91 - 0
src/main/java/cn/com/qmth/scancloud/tools/utils/HttpClientBuilder.java

@@ -0,0 +1,91 @@
+package cn.com.qmth.scancloud.tools.utils;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.*;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+public class HttpClientBuilder {
+
+    private static final Logger log = LoggerFactory.getLogger(HttpClientBuilder.class);
+
+    private static OkHttpClient client;
+
+    static {
+        client = Client.INSTANCE.getInstance();
+    }
+
+    public static OkHttpClient getClient() {
+        return client;
+    }
+
+    private enum Client {
+
+        INSTANCE;
+
+        Client() {
+            ConnectionPool connectionPool = new ConnectionPool(10, 5L, TimeUnit.MINUTES);
+
+            instance = new OkHttpClient
+                    .Builder()
+                    // .retryOnConnectionFailure(false)
+                    .connectionPool(connectionPool)
+                    .connectTimeout(30, TimeUnit.SECONDS)
+                    .readTimeout(60, TimeUnit.SECONDS)
+                    .writeTimeout(60, TimeUnit.SECONDS)
+                    .sslSocketFactory(sslSocketFactory(), trustAllCert())
+                    .hostnameVerifier(trustAllHost())
+                    .build();
+
+            log.debug("OkHttpClient init..");
+        }
+
+        private OkHttpClient instance;
+
+        public OkHttpClient getInstance() {
+            return instance;
+        }
+    }
+
+    private static SSLSocketFactory sslSocketFactory() {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[]{trustAllCert()}, new SecureRandom());
+            return sslContext.getSocketFactory();
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static HostnameVerifier trustAllHost() {
+        return (hostname, sslSession) -> true;
+    }
+
+    private static X509TrustManager trustAllCert() {
+        return new X509TrustManager() {
+
+            @Override
+            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+            }
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+            }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+            }
+        };
+    }
+
+}

+ 39 - 0
src/main/java/cn/com/qmth/scancloud/tools/utils/HttpHelper.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.scancloud.tools.utils;
+
+import okhttp3.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HttpHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(HttpHelper.class);
+
+    private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
+
+    public static String post(String url, String json) {
+        if (log.isDebugEnabled()) {
+            log.debug("[POST] {}, params={}", url, json);
+        }
+
+        RequestBody requestBody = FormBody.create(json != null ? json : "", MediaType.parse(CONTENT_TYPE));
+        Request request = new Request.Builder()
+                .post(requestBody)
+                .url(url)
+                .build();
+
+        try (Response response = HttpClientBuilder.getClient().newCall(request).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+            if (response.isSuccessful()) {
+                return bodyStr;
+            }
+
+            log.error("[POST] {}, response code = {}, body = {}", url, response.code(), bodyStr);
+        } catch (Exception e) {
+            log.error("[POST] {}, error = {}", url, e.getMessage(), e);
+        }
+
+        throw new StatusException("接口调用失败!");
+    }
+
+}

+ 94 - 0
src/main/java/cn/com/qmth/scancloud/tools/utils/JsonHelper.java

@@ -0,0 +1,94 @@
+package cn.com.qmth.scancloud.tools.utils;
+
+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 org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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转换失败!");
+        }
+    }
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/scancloud/tools/utils/StatusException.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.scancloud.tools.utils;
+
+public class StatusException extends RuntimeException {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = -5214086255435391334L;
+
+    public StatusException(String message) {
+        super(message);
+    }
+
+}

+ 2 - 0
src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.SpringApplicationRunListener=\
+cn.com.qmth.scancloud.tools.config.ApplicationRunListener

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

@@ -0,0 +1,27 @@
+#############system params config###############
+server.port=8080
+server.servlet.context-path=/
+server.tomcat.uri-encoding=UTF-8
+logging.level.org.springframework=error
+logging.level.cn.com.qmth.scancloud=info
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} | %clr(%-5level) | %msg%n
+logging.file.name=logs/debug.log
+scan.server.url=http://192.168.10.38:9099
+scan.tool.data-dir=/home/admin/scan-tool
+scan.tool.data-file=${scan.tool.data-dir}/data.txt
+scan.tool.task-type=
+scan.tool.thread.size=5
+#############system params config###############
+
+scan.tool.more-params=
+scan.tool.template.columns=
+
+#############params config###############
+scan.tool.template.separator=,
+scan.tool.school-id=1
+scan.tool.school-name=\u5b66\u68211
+scan.tool.exam-id=
+scan.tool.paper-type-barcode=
+scan.tool.year=22
+scan.tool.year.half=1
+#############params config###############

+ 121 - 0
src/test/java/cn/com/qmth/scancloud/tools/DemoTest.java

@@ -0,0 +1,121 @@
+package cn.com.qmth.scancloud.tools;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import cn.com.qmth.scancloud.tools.model.ExamStudent;
+import cn.com.qmth.scancloud.tools.utils.FileHelper;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import org.apache.commons.io.LineIterator;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DemoTest {
+
+    private final String dataDir = "/home/admin/scan-tool";
+
+    @Test
+    public void demo() throws Exception {
+        // writeLines();
+        // readLines();
+        // readAllLines();
+        // writeExcel();
+        // readExcel();
+    }
+
+    private void writeLines() {
+        List<String> lines = new ArrayList<>();
+
+        String template = "C00001|课程01|zkzh@|考生@|ksbh@|jdh@|zwh@|学习中心01|考场01|考点01";
+        for (int n = 0; n < 100; n++) {
+            lines.add(template.replace("@", String.valueOf(n + 1)));
+        }
+
+        File file = new File(dataDir + "/data.txt");
+        FileHelper.writeLines(file, lines, false);
+    }
+
+    private void readLines() {
+        File file = new File(dataDir + "/data.txt");
+
+        int total = 0;
+        try (LineIterator lines = FileHelper.readLines(file);) {
+            while (lines.hasNext()) {
+                String line = lines.nextLine();
+                // System.out.println(line);
+                total++;
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        System.out.println("total is " + total);
+    }
+
+    private void readAllLines() {
+        File file = new File(dataDir + "/data.txt");
+        List<String> lines = FileHelper.readAllLines(file);
+
+        // for (int n = 0; n < lines.size(); n++) {
+        //     System.out.println(lines.get(n));
+        // }
+
+        System.out.println("total is " + lines.size());
+    }
+
+    private void writeExcel() {
+        List<ExamStudent> list = new ArrayList<>();
+        for (int n = 0; n < 10; n++) {
+            ExamStudent data = new ExamStudent();
+            data.setName("考生" + (n + 1));
+            list.add(data);
+        }
+
+        File file = new File(dataDir + "/data.xlsx");
+        EasyExcel.write(file, ExamStudent.class).registerWriteHandler(new SheetWriteHandler() {
+            @Override
+            public void afterSheetCreate(WriteWorkbookHolder workbookHolder, WriteSheetHolder sheetHolder) {
+                Sheet sheet = sheetHolder.getSheet();
+                sheet.createFreezePane(1, 1);
+            }
+        }).sheet().doWrite(list);
+    }
+
+    private void readExcel() {
+        File file = new File(dataDir + "/data.xlsx");
+        EasyExcel.read(file, ExamStudent.class, new AnalysisEventListener<ExamStudent>() {
+            List<ExamStudent> list = new ArrayList<>();
+
+            @Override
+            public void invoke(ExamStudent data, AnalysisContext context) {
+                list.add(data);
+            }
+
+            @Override
+            public void doAfterAllAnalysed(AnalysisContext context) {
+                System.out.println("total is " + list.size());
+            }
+        }).sheet().doRead();
+    }
+
+    @Before
+    public void before() {
+        // Configurator.setRootLevel(Level.ALL);
+        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+        Logger logger = loggerContext.getLogger("cn.com.qmth");
+        logger.setLevel(Level.INFO);
+    }
+
+}

+ 30 - 0
src/test/java/cn/com/qmth/scancloud/tools/TaskTest.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.scancloud.tools;
+
+import cn.com.qmth.scancloud.tools.service.impl.*;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class TaskTest {
+
+    @Test
+    public void demo() throws Exception {
+        // processData();
+    }
+
+    private void processData() {
+        new UserImportTask().run();
+        new ExamImportTask().run();
+        new CourseImportTask().run();
+        new ExamStudentImportTask().run();
+        new ExamStudentCountTask().run();
+        new ExamStudentCleanTask().run();
+        new ScanImageCheckTask().run();
+        new ObjectiveQuestionExportTask().run();
+        new ExcelToTxtTask().run();
+    }
+
+}