Sfoglia il codice sorgente

merge from release_v4.0.1

deason 4 anni fa
parent
commit
70a68f61c6
69 ha cambiato i file con 3881 aggiunte e 2054 eliminazioni
  1. 31 8
      .gitignore
  2. 10 13
      examcloud-api-commons/.gitignore
  3. 6 1
      examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/ExamSpecialSettingsType.java
  4. 19 0
      examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/ExamStageStartExamStatus.java
  5. 19 0
      examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/SubmitType.java
  6. 10 12
      examcloud-commons/.gitignore
  7. 11 4
      examcloud-config-center-starter/.gitignore
  8. 112 112
      examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/ConfigCenterStarter.java
  9. 94 97
      examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/core/CommandInterpreter.java
  10. 92 97
      examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/core/ConfigCenterBootstrap.java
  11. 48 34
      examcloud-config-center-starter/src/main/resources/log4j2.xml
  12. 0 2
      examcloud-java-sdk/.gitignore
  13. 34 0
      examcloud-java-sdk/src/main/java/cn/com/qmth/sdk/request/OuterGetSubjectivePaperReq.java
  14. 64 12
      examcloud-java-sdk/src/main/resources/log4j2.xml
  15. 2 2
      examcloud-java-sdk/src/main/resources/qmth.properties
  16. 34 0
      examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectivePaper.java
  17. 3 2
      examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectivePaperStruct.java
  18. 4 4
      examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectiveQuestion.java
  19. 10 13
      examcloud-jenkins-build/.gitignore
  20. 14 20
      examcloud-parent/.gitignore
  21. 11 5
      examcloud-question-commons/.gitignore
  22. 11 5
      examcloud-reports-commons/.gitignore
  23. 13 7
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/BaseReport.java
  24. 6 15
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineExamStudentReport.java
  25. 6 12
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineStudentReport.java
  26. 6 11
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineUserReport.java
  27. 91 0
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OperateReport.java
  28. 43 0
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/OperateContent.java
  29. 70 0
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/Tag.java
  30. 2 12
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/Topic.java
  31. 0 29
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/handler/KafkaSendResultHandler.java
  32. 49 34
      examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/util/ReportsUtil.java
  33. 19 0
      examcloud-starters/.gitignore
  34. 64 0
      examcloud-starters/examcloud-geetest-starter/pom.xml
  35. 34 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/GeetestAutoConfiguration.java
  36. 112 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/GeetestProperties.java
  37. 12 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/IModel.java
  38. 58 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/RegisterReq.java
  39. 94 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/RegisterResp.java
  40. 102 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/ValidateReq.java
  41. 68 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/ValidateResp.java
  42. 28 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/GeetestService.java
  43. 19 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/GeetestSessionManager.java
  44. 223 0
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/impl/GeetestServiceImpl.java
  45. 2 0
      examcloud-starters/examcloud-geetest-starter/src/main/resources/META-INF/spring.factories
  46. 10 12
      examcloud-support/.gitignore
  47. 25 27
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/CacheHelper.java
  48. 158 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStageCacheBean.java
  49. 38 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStagesCacheBean.java
  50. 26 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentCacheBean.java
  51. 39 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/ExamProcess.java
  52. 94 10
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java
  53. 13 1
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingActivityTime.java
  54. 4 1
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingHeartbeat.java
  55. 264 226
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java
  56. 406 388
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java
  57. 144 92
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java
  58. 35 8
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/helper/ExamCacheTransferHelper.java
  59. 10 13
      examcloud-web/.gitignore
  60. 56 59
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoHolder.java
  61. 1 1
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSiteManager.java
  62. 78 52
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java
  63. 482 486
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java
  64. 55 0
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunRefreshCdn.java
  65. 98 109
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java
  66. 3 1
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/jpa/JpaEntity.java
  67. 4 0
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/ControllerSupport.java
  68. 66 0
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/IpUtil.java
  69. 12 5
      examcloud-ws-starter/.gitignore

+ 31 - 8
.gitignore

@@ -1,12 +1,35 @@
-*.class
-.project
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
 .classpath
+.factorypath
+.project
 .settings
-target/
-.idea/
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
 *.iml
-*.log
+*.ipr
 *.class
-*.jar
-*.war
-*.ear
+*.log
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 10 - 13
examcloud-api-commons/.gitignore

@@ -1,22 +1,19 @@
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.idea/
-*.iml
+*.class
 
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-*test/
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-
+*.war
+*.ear

+ 6 - 1
examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/ExamSpecialSettingsType.java

@@ -22,6 +22,11 @@ public enum ExamSpecialSettingsType {
 	/**
 	 * 课程维度
 	 */
-	COURSE_BASED
+	COURSE_BASED,
+
+	/**
+	 * 场次维度
+	 */
+	STAGE_BASED
 
 }

+ 19 - 0
examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/ExamStageStartExamStatus.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.examcloud.api.commons.enums;
+
+/***
+ * @Description 场次开考状态
+ * @Author lideyin
+ * @Date 2020/7/24 14:30
+ * @Version 1.0
+ */
+public enum ExamStageStartExamStatus {
+	/**
+	 * 未开考
+	 */
+	NOT_START,
+
+	/**
+	 * 已开考
+	 */
+	STARTED
+}

+ 19 - 0
examcloud-api-commons/src/main/java/cn/com/qmth/examcloud/api/commons/enums/SubmitType.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.examcloud.api.commons.enums;
+
+/**
+ * @Description 回收试卷类型
+ * @Author lideyin
+ * @Date 2020/7/21 19:03
+ * @Version 1.0
+ */
+public enum SubmitType {
+	/**
+	 * 定点收卷
+	 */
+	TIMING_END,
+
+	/**
+	 * 正常收卷
+	 */
+	NORMAL
+}

+ 10 - 12
examcloud-commons/.gitignore

@@ -1,21 +1,19 @@
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.idea/
-*.iml
+*.class
 
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-
+*.war
+*.ear

+ 11 - 4
examcloud-config-center-starter/.gitignore

@@ -1,12 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-# Package Files #
-*.jar
-logs/
-.springBeans
 
+# Log Files
+*.log
+*.class
 
+
+# Package Files #
+*.jar
+*.war
+*.ear

+ 112 - 112
examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/ConfigCenterStarter.java

@@ -1,9 +1,11 @@
 package cn.com.qmth.examcloud.config.center;
 
-import java.io.IOException;
-import java.io.PrintStream;
-import java.util.concurrent.TimeUnit;
-
+import cn.com.qmth.examcloud.commons.helpers.BlackHolePrintStreamBuilder;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.config.center.core.BootstrapSecurityManager;
+import cn.com.qmth.examcloud.config.center.core.CommandInterpreter;
+import cn.com.qmth.examcloud.config.center.core.ConfigCenterBootstrap;
 import org.apache.commons.compress.utils.IOUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.logging.log4j.ThreadContext;
@@ -18,12 +20,9 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
-import cn.com.qmth.examcloud.commons.helpers.BlackHolePrintStreamBuilder;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-import cn.com.qmth.examcloud.config.center.core.BootstrapSecurityManager;
-import cn.com.qmth.examcloud.config.center.core.CommandInterpreter;
-import cn.com.qmth.examcloud.config.center.core.ConfigCenterBootstrap;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 配置中心启动类
@@ -35,109 +34,110 @@ import cn.com.qmth.examcloud.config.center.core.ConfigCenterBootstrap;
 @SpringBootApplication
 @Configuration
 @ComponentScan(basePackages = {"cn.com.qmth"})
-@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class,
-		DataSourceAutoConfiguration.class})
+@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class, DataSourceAutoConfiguration.class})
 public class ConfigCenterStarter {
 
-	private static PrintStream stdOut = null;
-
-	private static PrintStream stdErrOut = null;
-
-	private static PrintStream blackHoleOut = null;
-
-	private static Long action = System.currentTimeMillis();
-
-	/**
-	 * 启动方法
-	 *
-	 * @author WANGWEI
-	 * @param args
-	 */
-	public static void run(String[] args) {
-		ExamCloudLog log = ExamCloudLogFactory.getLog(ConfigCenterStarter.class);
-		ThreadContext.put("TRACE_ID", Thread.currentThread().getName());
-		log.info("Starting...");
-
-		blackHoleOut = BlackHolePrintStreamBuilder.build();
-		stdOut = System.out;
-		stdErrOut = System.err;
-
-		Terminal terminal = null;
-		LineReader reader = null;
-		try {
-			System.setOut(blackHoleOut);
-			System.setErr(blackHoleOut);
-			terminal = TerminalBuilder.terminal();
-			reader = LineReaderBuilder.builder().terminal(terminal).build();
-			System.setOut(stdOut);
-			System.setErr(stdErrOut);
-		} catch (IOException e) {
-			System.setOut(stdOut);
-			System.setErr(stdErrOut);
-			IOUtils.closeQuietly(terminal);
-			e.printStackTrace();
-			System.exit(-1);
-		}
-
-		String secretKey = null;
-		while (true) {
-			try {
-				secretKey = reader.readLine("Enter secret key>", (char) 0);
-				System.setOut(blackHoleOut);
-				System.setErr(blackHoleOut);
-				args = (String[]) ArrayUtils.add(args,
-						"--examcloud.startup.secretKey=" + secretKey);
-
-				ConfigCenterBootstrap.run(ConfigCenterStarter.class, args);
-				System.setOut(stdOut);
-				System.setErr(stdErrOut);
-				break;
-			} catch (Exception e) {
-				System.setOut(stdOut);
-				System.setErr(stdErrOut);
-				System.out.println(e.getMessage());
-				System.out.println("Try again... ...");
-				continue;
-			}
-		}
-
-		System.out.println("I am running... ...");
-		String active = BootstrapSecurityManager.getInstance().getActive();
-		System.out.println("active=" + active);
-
-		new Thread() {
-			@Override
-			public void run() {
-				while (true) {
-					if (System.currentTimeMillis() - action > CommandInterpreter.getInstance()
-							.getSessionTimeout()) {
-						System.exit(0);
-					}
-					try {
-						TimeUnit.SECONDS.sleep(5);
-					} catch (InterruptedException e) {
-						e.printStackTrace();
-					}
-				}
-
-			};
-		}.start();
-
-		IOUtils.closeQuietly(blackHoleOut);
-		action = System.currentTimeMillis();
-
-		while (true) {
-			action = System.currentTimeMillis();
-			String cmd = reader.readLine("$>");
-			if (cmd.equals("q")) {
-				break;
-			}
-
-			CommandInterpreter.getInstance().interpret(cmd);
-		}
-
-		IOUtils.closeQuietly(terminal);
-		System.exit(0);
-	}
+    private static PrintStream stdOut = null;
+
+    private static PrintStream stdErrOut = null;
+
+    private static PrintStream blackHoleOut = null;
+
+    private static Long action = System.currentTimeMillis();
+
+    /**
+     * 启动方法
+     */
+    public static void run(String[] args) {
+        ExamCloudLog log = ExamCloudLogFactory.getLog(ConfigCenterStarter.class);
+        ThreadContext.put("TRACE_ID", Thread.currentThread().getName());
+        log.info("Starting...");
+
+        blackHoleOut = BlackHolePrintStreamBuilder.build();
+        stdOut = System.out;
+        stdErrOut = System.err;
+
+        Terminal terminal = null;
+        LineReader reader = null;
+        try {
+            System.setOut(blackHoleOut);
+            System.setErr(blackHoleOut);
+            terminal = TerminalBuilder.terminal();
+            reader = LineReaderBuilder.builder().terminal(terminal).build();
+            System.setOut(stdOut);
+            System.setErr(stdErrOut);
+        } catch (IOException e) {
+            System.setOut(stdOut);
+            System.setErr(stdErrOut);
+            IOUtils.closeQuietly(terminal);
+            e.printStackTrace();
+            System.exit(-1);
+        }
+
+        int errCount = 1;
+
+        String secretKey;
+        while (true) {
+            try {
+                secretKey = reader.readLine("Enter secret key>", (char) 0);
+                System.setOut(blackHoleOut);
+                System.setErr(blackHoleOut);
+                args = ArrayUtils.add(args, "--examcloud.startup.secretKey=" + secretKey);
+
+                ConfigCenterBootstrap.run(ConfigCenterStarter.class, args);
+                System.setOut(stdOut);
+                System.setErr(stdErrOut);
+                break;
+            } catch (Exception e) {
+                System.setOut(stdOut);
+                System.setErr(stdErrOut);
+                System.out.println(e.getMessage());
+
+                if (errCount > 5) {
+                    System.out.println("Too many wrong, exit!");
+                    System.exit(0);
+                }
+
+                System.out.println("Try again...");
+                errCount++;
+                continue;
+            }
+        }
+
+        System.out.println("Ok, running...");
+        String active = BootstrapSecurityManager.getInstance().getActive();
+        System.out.println("Env active = " + active);
+
+        new Thread(() -> {
+            while (true) {
+                if (System.currentTimeMillis() - action > CommandInterpreter.getInstance()
+                        .getSessionTimeout()) {
+                    System.exit(0);
+                }
+                try {
+                    TimeUnit.SECONDS.sleep(5);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+
+        }).start();
+
+        IOUtils.closeQuietly(blackHoleOut);
+        action = System.currentTimeMillis();
+
+        while (true) {
+            action = System.currentTimeMillis();
+            String cmd = reader.readLine("$>");
+            if (cmd.equals("exit") || cmd.equals("quit")) {
+                break;
+            }
+
+            CommandInterpreter.getInstance().interpret(cmd);
+        }
+
+        IOUtils.closeQuietly(terminal);
+        System.exit(0);
+    }
 
 }

+ 94 - 97
examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/core/CommandInterpreter.java

@@ -1,15 +1,14 @@
 package cn.com.qmth.examcloud.config.center.core;
 
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.apache.commons.lang3.StringUtils;
-
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
 import cn.com.qmth.examcloud.commons.util.MapUtil;
 import cn.com.qmth.examcloud.commons.util.RegExpUtil;
 import cn.com.qmth.examcloud.commons.util.StringUtil;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * 命令解释器
@@ -20,97 +19,95 @@ import cn.com.qmth.examcloud.commons.util.StringUtil;
  */
 public class CommandInterpreter {
 
-	private static ExamCloudLog log = ExamCloudLogFactory.getLog("COMMAND_LOGGER");
-
-	private static CommandInterpreter interpreter;
-
-	private static final Object LOCK = new Object();
-
-	private static long sessionTimeout = 1000 * 60 * 5;
-
-	/**
-	 * 构造函数
-	 */
-	private CommandInterpreter() {
-	}
-
-	/**
-	 * 获取单例
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	public static CommandInterpreter getInstance() {
-		if (null == interpreter) {
-			synchronized (LOCK) {
-				if (null == interpreter) {
-					interpreter = new CommandInterpreter();
-				}
-				return interpreter;
-			}
-		} else {
-			return interpreter;
-		}
-	}
-
-	/**
-	 * 解释
-	 *
-	 * @author WANGWEI
-	 */
-	public void interpret(String cmd) {
-		try {
-			process(cmd);
-		} catch (Exception e) {
-			log.error(e.getMessage());
-		}
-	}
-
-	/**
-	 * 处理
-	 *
-	 * @author WANGWEI
-	 */
-	public void process(String cmd) {
-		cmd = cmd.trim();
-		if (StringUtils.isBlank(cmd)) {
-			return;
-		}
-
-		// 设置进程退出:session 10
-		if (cmd.matches("\\s*session\\s+[1-9]\\d*")) {
-			String timeout = RegExpUtil.find(cmd, "\\s*session\\s+([1-9]\\d*)", 1);
-			timeout = timeout.length() > 4 ? "9999" : timeout;
-			long timeoutLong = StringUtil.toLong(timeout);
-			timeoutLong = timeoutLong > 60 ? 60 : timeoutLong;
-			log.info("sessionTimeout = " + timeoutLong + " minutes");
-			sessionTimeout = timeoutLong * 1000 * 60;
-		}
-
-		// 加载配置:load <appSimpleName>
-		else if (cmd.matches("\\s*load\\s+([^\\s]+)\\s*")) {
-			String appSimpleName = RegExpUtil.find(cmd, "\\s*load\\s+([^\\s]+)\\s*", 1);
-			String active = BootstrapSecurityManager.getInstance().getActive();
-			Map<String, String> properties = PropertiesLoader.getProperties(appSimpleName, active);
-
-			properties = MapUtil.sortMapByKey(properties, true);
-			StringBuilder sb = new StringBuilder("properties about ").append(appSimpleName)
-					.append(" [").append(active).append("]:");
-			for (Entry<String, String> entry : properties.entrySet()) {
-				sb.append("\n   ").append(entry.getKey()).append(" : ").append(entry.getValue());
-			}
-			log.info(sb.toString());
-		}
-
-		// 错误命令
-		else {
-			log.error("error command !");
-		}
-
-	}
-
-	public long getSessionTimeout() {
-		return sessionTimeout;
-	}
+    private static ExamCloudLog log = ExamCloudLogFactory.getLog("COMMAND_LOGGER");
+
+    private static CommandInterpreter interpreter;
+
+    private static final Object LOCK = new Object();
+
+    private static long sessionTimeout = 1000 * 60 * 10;
+
+    /**
+     * 构造函数
+     */
+    private CommandInterpreter() {
+
+    }
+
+    /**
+     * 获取单例
+     */
+    public static CommandInterpreter getInstance() {
+        if (null == interpreter) {
+            synchronized (LOCK) {
+                if (null == interpreter) {
+                    interpreter = new CommandInterpreter();
+                }
+                return interpreter;
+            }
+        } else {
+            return interpreter;
+        }
+    }
+
+    /**
+     * 解释
+     */
+    public void interpret(String cmd) {
+        try {
+            process(cmd);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 处理
+     */
+    public void process(String cmd) {
+        cmd = cmd.trim();
+        if (StringUtils.isBlank(cmd)) {
+            return;
+        }
+
+        if (cmd.matches("\\s*session\\s+[1-9]\\d*")) {
+            // 设置进程退出命令:session 分钟值
+            String timeoutStr = RegExpUtil.find(cmd, "\\s*session\\s+([1-9]\\d*)", 1);
+            timeoutStr = timeoutStr.length() > 4 ? "999" : timeoutStr;
+
+            // 默认最大值
+            long timeout = StringUtil.toLong(timeoutStr);
+            timeout = timeout > 360 ? 360 : timeout;
+
+            sessionTimeout = 1000 * 60 * timeout;
+            log.info("sessionTimeout = " + timeout + " minutes");
+        } else if (cmd.matches("\\s*load\\s+([^\\s]+)\\s*")) {
+            // 加载配置:load <appSimpleName>
+            String appSimpleName = RegExpUtil.find(cmd, "\\s*load\\s+([^\\s]+)\\s*", 1);
+            String active = BootstrapSecurityManager.getInstance().getActive();
+            Map<String, String> properties = PropertiesLoader.getProperties(appSimpleName, active);
+
+            properties = MapUtil.sortMapByKey(properties, true);
+            StringBuilder props = new StringBuilder()
+                    .append(appSimpleName)
+                    .append(" ")
+                    .append(active)
+                    .append(" Properties: ");
+
+            for (Entry<String, String> entry : properties.entrySet()) {
+                props.append("\n  ").append(entry.getKey()).append("=").append(entry.getValue());
+            }
+
+            log.info(props.toString());
+        } else {
+            // 错误命令
+            log.error("error command !");
+        }
+
+    }
+
+    public long getSessionTimeout() {
+        return sessionTimeout;
+    }
 
 }

+ 92 - 97
examcloud-config-center-starter/src/main/java/cn/com/qmth/examcloud/config/center/core/ConfigCenterBootstrap.java

@@ -1,19 +1,17 @@
 package cn.com.qmth.examcloud.config.center.core;
 
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
-
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.SpringApplication;
-import org.springframework.context.ConfigurableApplicationContext;
-
-import com.google.common.collect.Sets;
-
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
 import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
 import cn.com.qmth.examcloud.web.bootstrap.BootstrapSecurityUtil;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
 
 /**
  * 配置中心启动器
@@ -24,92 +22,89 @@ import cn.com.qmth.examcloud.web.bootstrap.BootstrapSecurityUtil;
  */
 public class ConfigCenterBootstrap {
 
-	private static ExamCloudLog log = ExamCloudLogFactory.getLog(ConfigCenterBootstrap.class);
-
-	/**
-	 * 启动配置中心
-	 *
-	 * @author WANGWEI
-	 * @param primarySource
-	 * @param password
-	 * @return
-	 */
-	public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
-
-		Properties props = new Properties();
-		PropertiesUtil.loadFromResource("application.properties", props);
-
-		String active = null;
-		String secretKey = null;
-		if (null != args) {
-			for (String s : args) {
-				s = s.trim();
-				if (s.startsWith("--examcloud.startup.secretKey=")) {
-					secretKey = s.substring(s.indexOf("=") + 1);
-				}
-				if (s.startsWith("--spring.profiles.active=")) {
-					active = s.substring(s.indexOf("=") + 1);
-				}
-			}
-		}
-
-		// active
-		if (null == active) {
-			String value = props.getProperty("spring.profiles.active");
-			if (StringUtils.isNotBlank(value)) {
-				active = value.trim();
-			}
-		}
-		log.info("active=" + active);
-		if (StringUtils.isBlank(active)) {
-			log.error("property[spring.profiles.active] is not specified");
-			System.exit(-1);
-		}
-
-		if (StringUtils.isBlank(secretKey)) {
-			throw new RuntimeException("secret key is specified !");
-		}
-
-		PropertiesUtil.loadFromResource("application-" + active + ".properties", props);
-		props.setProperty("spring.profiles.active", active);
-
-		String test = props.getProperty("$$.examcloud.test");
-		if (StringUtils.isBlank(test)) {
-			log.error("property[$$.examcloud.test] is not configured");
-			System.exit(-1);
-		}
-		try {
-			BootstrapSecurityUtil.decrypt(test.trim(), secretKey);
-		} catch (Exception e) {
-			throw new RuntimeException("secret key is wrong !");
-		}
-
-		try {
-			BootstrapSecurityUtil.decrypt(props, secretKey);
-		} catch (Exception e) {
-			log.error("fail to decrypt !");
-			System.exit(-1);
-		}
-
-		Set<String> argSet = Sets.newLinkedHashSet();
-		for (Entry<Object, Object> p : props.entrySet()) {
-			String arg = "--" + p.getKey() + "=" + p.getValue();
-			argSet.add(arg);
-		}
-
-		String[] newArgs = argSet.toArray(new String[argSet.size()]);
-
-		BootstrapSecurityManager.getInstance().setSecretKey(secretKey);
-		BootstrapSecurityManager.getInstance().setActive(active);
-
-		ConfigurableApplicationContext context = null;
-		try {
-			context = SpringApplication.run(primarySource, newArgs);
-		} catch (Exception e) {
-			log.error("fail to run spring app.", e);
-			System.exit(-1);
-		}
-		return context;
-	}
+    private static ExamCloudLog log = ExamCloudLogFactory.getLog(ConfigCenterBootstrap.class);
+
+    /**
+     * 启动配置中心
+     */
+    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
+
+        Properties props = new Properties();
+        PropertiesUtil.loadFromResource("application.properties", props);
+
+        String active = null;
+        String secretKey = null;
+        if (null != args) {
+            for (String s : args) {
+                s = s.trim();
+                if (s.startsWith("--examcloud.startup.secretKey=")) {
+                    secretKey = s.substring(s.indexOf("=") + 1);
+                }
+                if (s.startsWith("--spring.profiles.active=")) {
+                    active = s.substring(s.indexOf("=") + 1);
+                }
+            }
+        }
+
+        if (null == active) {
+            String value = props.getProperty("spring.profiles.active");
+            if (StringUtils.isNotBlank(value)) {
+                active = value.trim();
+            }
+        }
+        log.info("active=" + active);
+
+        if (StringUtils.isBlank(active)) {
+            log.error("[spring.profiles.active] value must not empty!");
+            System.exit(-1);
+        }
+
+        if (StringUtils.isBlank(secretKey)) {
+            throw new RuntimeException("Secret key must not empty!");
+        }
+
+        PropertiesUtil.loadFromResource("application-" + active + ".properties", props);
+        props.setProperty("spring.profiles.active", active);
+
+        String test = props.getProperty("$$.examcloud.test");
+        if (StringUtils.isBlank(test)) {
+            log.error("[$$.examcloud.test] value must not empty!");
+            System.exit(-1);
+        }
+
+        try {
+            BootstrapSecurityUtil.decrypt(test.trim(), secretKey);
+        } catch (Exception e) {
+            throw new RuntimeException("Secret key is wrong!");
+        }
+
+        try {
+            BootstrapSecurityUtil.decrypt(props, secretKey);
+        } catch (Exception e) {
+            log.error("Secret key decrypt fail!");
+            System.exit(-1);
+        }
+
+        Set<String> argSet = Sets.newLinkedHashSet();
+        for (Entry<Object, Object> p : props.entrySet()) {
+            String arg = "--" + p.getKey() + "=" + p.getValue();
+            argSet.add(arg);
+        }
+
+        String[] newArgs = argSet.toArray(new String[argSet.size()]);
+
+        BootstrapSecurityManager.getInstance().setSecretKey(secretKey);
+        BootstrapSecurityManager.getInstance().setActive(active);
+
+        ConfigurableApplicationContext context = null;
+        try {
+            context = SpringApplication.run(primarySource, newArgs);
+        } catch (Exception e) {
+            log.error("Project startup fail!", e);
+            System.exit(-1);
+        }
+
+        return context;
+    }
 
 }

+ 48 - 34
examcloud-config-center-starter/src/main/resources/log4j2.xml

@@ -1,37 +1,51 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="WARN" monitorInterval="30">
 
-	<Appenders>
-		<Console name="CONSOLE" target="SYSTEM_OUT">
-			<PatternLayout pattern="%n%d{HH:mm:ss}| %level | %X{TRACE_ID} | %m | %l%n$>" />
-		</Console>
-		<Console name="APP_STARTUP_CONSOLE" target="SYSTEM_OUT">
-			<PatternLayout pattern="%n%d{HH:mm:ss}| %level | %X{TRACE_ID} | %m%n$>" />
-		</Console>
-		<Console name="COMMAND_CONSOLE" target="SYSTEM_OUT">
-			<PatternLayout pattern="%d{HH:mm:ss}| %level | CMD | %m%n" />
-		</Console>
-	</Appenders>
-
-	<Loggers>
-		<Logger name="APP_STARTUP_LOGGER" level="DEBUG" additivity="false">
-			<AppenderRef ref="APP_STARTUP_CONSOLE" />
-		</Logger>
-
-		<Logger name="COMMAND_LOGGER" level="DEBUG" additivity="false">
-			<AppenderRef ref="COMMAND_CONSOLE" />
-		</Logger>
-
-		<Logger name="INTERFACE_LOGGER" level="OFF" additivity="false">
-			<AppenderRef ref="CONSOLE" />
-		</Logger>
-
-		<Logger name="org.quartz.core" level="ERROR" />
-
-		<Root level="ERROR">
-			<AppenderRef ref="CONSOLE" />
-		</Root>
-
-	</Loggers>
-
-</Configuration>
+    <Appenders>
+        <Console name="CONSOLE" target="SYSTEM_OUT">
+            <PatternLayout
+                    pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} | %level | %X{TRACE_ID} | %clr{%c{1.1}:%L}{cyan} | %m%n $>"
+                    charset="UTF-8"/>
+        </Console>
+
+        <Console name="APP_STARTUP_CONSOLE" target="SYSTEM_OUT">
+            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} | %level | %X{TRACE_ID} | %m%n $>"
+                           charset="UTF-8"/>
+        </Console>
+
+        <Console name="COMMAND_CONSOLE" target="SYSTEM_OUT">
+            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} | %level | CMD | %m%n"/>
+        </Console>
+    </Appenders>
+
+    <Loggers>
+        <logger name="springfox.documentation" level="ERROR"/>
+        <logger name="org.springframework" level="ERROR"/>
+        <logger name="org.hibernate" level="ERROR"/>
+        <logger name="org.apache" level="ERROR"/>
+        <logger name="org.quartz" level="ERROR"/>
+        <logger name="org.docx4j" level="ERROR"/>
+        <logger name="cn.afterturn" level="ERROR"/>
+        <logger name="com.netflix" level="ERROR"/>
+        <logger name="com.aliyun" level="ERROR"/>
+        <logger name="io.lettuce" level="ERROR"/>
+        <logger name="io.netty" level="ERROR"/>
+
+        <Logger name="APP_STARTUP_LOGGER" level="DEBUG" additivity="false">
+            <AppenderRef ref="APP_STARTUP_CONSOLE"/>
+        </Logger>
+
+        <Logger name="COMMAND_LOGGER" level="DEBUG" additivity="false">
+            <AppenderRef ref="COMMAND_CONSOLE"/>
+        </Logger>
+
+        <Logger name="INTERFACE_LOGGER" level="ERROR" additivity="false">
+            <AppenderRef ref="CONSOLE"/>
+        </Logger>
+
+        <Root level="ERROR">
+            <AppenderRef ref="CONSOLE"/>
+        </Root>
+    </Loggers>
+
+</Configuration>

+ 0 - 2
examcloud-java-sdk/.gitignore

@@ -17,5 +17,3 @@ target/
 *.jar
 *.war
 *.ear
-logs/
-

+ 34 - 0
examcloud-java-sdk/src/main/java/cn/com/qmth/sdk/request/OuterGetSubjectivePaperReq.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.sdk.request;
+
+import cn.com.qmth.sdk.exchange.EnterpriseRequest;
+
+/**
+ * @Description 获取主观题试卷请求类
+ * @Author lideyin
+ * @Date 2020/6/23 15:25
+ * @Version 1.0
+ */
+public class OuterGetSubjectivePaperReq extends EnterpriseRequest {
+
+    private static final long serialVersionUID = 8892205616387684966L;
+
+    private Long examId;
+
+    private String subjectCode;
+
+    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;
+    }
+}

+ 64 - 12
examcloud-java-sdk/src/main/resources/log4j2.xml

@@ -2,32 +2,84 @@
 <Configuration status="WARN" monitorInterval="30">
 
     <Properties>
+        <Property name="commonLevel" value="DEBUG"/>
         <Property name="logPattern">
-            %d{yyyy-MM-dd HH:mm:ss.SSS} | %level | %m%n
+            %d{yyyy-MM-dd HH:mm:ss.SSS} | %clr{%level} | %X{TRACE_ID} %X{CALLER} | %clr{%c{1.1}:%L}{cyan} | %m%n
         </Property>
     </Properties>
 
     <Appenders>
-        <Console name="CONSOLE" target="SYSTEM_OUT">
-            <PatternLayout pattern="${logPattern}"/>
+        <!-- 控制台 日志 -->
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
         </Console>
 
-        <RollingFile name="DEBUG_APPERDER" fileName="./logs/debug/debug.log"
+        <!-- debug 日志 -->
+        <RollingFile name="DEBUG_APPENDER"
+                     fileName="./logs/debug/debug.log"
                      filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
-            <PatternLayout pattern="${logPattern}"/>
-            <SizeBasedTriggeringPolicy size="10MB"/>
-            <DefaultRolloverStrategy max="50"/>
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
+                <SizeBasedTriggeringPolicy size="100 MB"/>
+            </Policies>
+            <DefaultRolloverStrategy max="1000">
+                <Delete basePath="./logs/debug" maxDepth="1">
+                    <IfFileName glob="debug-*.log">
+                        <IfAccumulatedFileSize exceeds="2 GB"/>
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+        </RollingFile>
+
+        <!-- 接口日志 -->
+        <RollingFile name="INTERFACE_APPENDER" fileName="./logs/interface/interface.log"
+                     filePattern="./logs/interface/interface-%d{yyyy.MM.dd.HH}-%i.log">
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
+                <SizeBasedTriggeringPolicy size="100 MB"/>
+            </Policies>
+            <DefaultRolloverStrategy max="1000">
+                <Delete basePath="./logs/interface" maxDepth="1">
+                    <IfFileName glob="interface-*.log">
+                        <IfAccumulatedFileSize exceeds="10 GB"/>
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
         </RollingFile>
     </Appenders>
 
     <Loggers>
-        <Logger name="cn.com.qmth" level="DEBUG" additivity="false">
-            <AppenderRef ref="DEBUG_APPERDER"/>
-            <AppenderRef ref="CONSOLE"/>
+        <logger name="springfox.documentation" level="ERROR"/>
+        <logger name="org.springframework" level="ERROR"/>
+        <logger name="org.hibernate" level="ERROR"/>
+        <logger name="org.apache" level="ERROR"/>
+        <logger name="org.quartz" level="ERROR"/>
+        <logger name="org.docx4j" level="ERROR"/>
+        <logger name="cn.afterturn" level="ERROR"/>
+        <logger name="com.netflix" level="ERROR"/>
+        <logger name="com.aliyun" level="ERROR"/>
+        <logger name="io.lettuce" level="ERROR"/>
+        <logger name="io.netty" level="ERROR"/>
+
+        <!--<logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>-->
+        <!--<logger name="org.springframework.data.mongodb" level="DEBUG"/>-->
+        <!--<logger name="org.springframework.data.redis" level="DEBUG"/>-->
+
+        <Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+            <AppenderRef ref="DEBUG_APPENDER"/>
+            <AppenderRef ref="Console"/>
+        </Logger>
+
+        <Logger name="INTERFACE_LOGGER" level="${commonLevel}" additivity="false">
+            <AppenderRef ref="INTERFACE_APPENDER"/>
+            <AppenderRef ref="Console"/>
         </Logger>
 
-        <Root level="ERROR">
-            <AppenderRef ref="CONSOLE"/>
+        <Root level="${commonLevel}">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="DEBUG_APPENDER"/>
         </Root>
     </Loggers>
 

+ 2 - 2
examcloud-java-sdk/src/main/resources/qmth.properties

@@ -6,8 +6,8 @@ qmth.commonUserAccessUrl=http://192.168.10.39:8000/api/ecs_core/auth/thirdPartyA
 qmth.studentAccessUrl=http://192.168.10.39:8000/api/ecs_core/auth/thirdPartyStudentAccess
 qmth.loginUrl=http://192.168.10.201:8000/api/ecs_core/user/login
 
-qmth.server.host=localhost
-qmth.server.port=8007
+qmth.server.host=ecs-dev.qmth.com.cn
+qmth.server.port=80
 
 
 

+ 34 - 0
examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectivePaper.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.sdk.test;
+
+import cn.com.qmth.sdk.request.OuterGetSubjectivePaperReq;
+import cn.com.qmth.sdk.util.HttpMethod;
+import cn.com.qmth.sdk.util.JsonUtil;
+import cn.com.qmth.sdk.util.OKHttpUtil;
+import cn.com.qmth.sdk.util.QmthUtil;
+import okhttp3.Response;
+import org.apache.commons.io.IOUtils;
+
+public class GetSubjectivePaper {
+
+    public static void main(String[] args) {
+        //{'examId': 2,'subjectCode': 'ldyCos001'}
+        //
+        OuterGetSubjectivePaperReq reqBody = new OuterGetSubjectivePaperReq();
+        reqBody.setExamId(2L);
+        reqBody.setSubjectCode("ldyCos001");
+
+        String url = QmthUtil.buildUrl("/api/exchange/outer/question/getSubjectivePaper");
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, url, QmthUtil.getSecurityHeaders(),
+                    JsonUtil.toJson(reqBody));
+            System.out.println(resp.code());
+            System.out.println(resp.body().string());
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            IOUtils.closeQuietly(resp);
+        }
+    }
+
+}

+ 3 - 2
examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectivePaperStruct.java

@@ -11,8 +11,9 @@ import org.apache.commons.io.IOUtils;
 public class GetSubjectivePaperStruct {
 
     public static void main(String[] args) {
+        //{'examId': 2,'subjectCode': 'ldyCos001'}
         OuterGetSubjectivePaperStructReq reqBody = new OuterGetSubjectivePaperStructReq();
-        reqBody.setExamId(19L);
+        reqBody.setExamId(2L);
 
         String url = QmthUtil.buildUrl("/api/exchange/outer/question/getSubjectivePaperStruct");
         Response resp = null;
@@ -27,4 +28,4 @@ public class GetSubjectivePaperStruct {
         }
     }
 
-}
+}

+ 4 - 4
examcloud-java-sdk/src/test/java/cn/com/qmth/sdk/test/GetSubjectiveQuestion.java

@@ -13,13 +13,13 @@ import okhttp3.Response;
 public class GetSubjectiveQuestion {
 
 	public static void main(String[] args) {
-		//{'des': 'string', 'examId': 57, 'size': 1, 'startId': 0, 'subjectCode': 'CSKC'}
+		//{'des': 'string', 'examId': 2, 'size': 10, 'startId': 1, 'subjectCode': 'ldyCos001'}
 		//
 		OuterGetSubjectiveQuestionReq reqBody = new OuterGetSubjectiveQuestionReq();
-		reqBody.setExamId(57L);
-		reqBody.setSubjectCode("CSKC");
+		reqBody.setExamId(2L);
+		reqBody.setSubjectCode("ldyCos001");
 		reqBody.setStartId(1L);
-		reqBody.setSize(1);
+		reqBody.setSize(10);
 
 		String url = QmthUtil.buildUrl("/api/exchange/outer/question/getSubjectiveQuestion");
 		Response resp = null;

+ 10 - 13
examcloud-jenkins-build/.gitignore

@@ -1,22 +1,19 @@
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.idea/
-*.iml
+*.class
 
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-*test/
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-
+*.war
+*.ear

+ 14 - 20
examcloud-parent/.gitignore

@@ -1,25 +1,19 @@
-/target/
-!.mvn/wrapper/maven-wrapper.jar
+*.class
 
-### STS ###
-.apt_generated
-.classpath
-.factorypath
+# Proguard folder generated by ide
 .project
+.classpath
 .settings
-.springBeans
-.sts4-cache
-
-### IntelliJ IDEA ###
-.idea
-*.iws
+target/
+.idea/
 *.iml
-*.ipr
 
-### NetBeans ###
-/nbproject/private/
-/build/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
+# Log Files
+*.log
+*.class
+
+
+# Package Files #
+*.jar
+*.war
+*.ear

+ 11 - 5
examcloud-question-commons/.gitignore

@@ -1,13 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-*test/
-# Package Files #
-*.jar
-logs/
-.springBeans
 
+# Log Files
+*.log
+*.class
 
+
+# Package Files #
+*.jar
+*.war
+*.ear

+ 11 - 5
examcloud-reports-commons/.gitignore

@@ -1,13 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-*test/
-# Package Files #
-*.jar
-logs/
-.springBeans
 
+# Log Files
+*.log
+*.class
 
+
+# Package Files #
+*.jar
+*.war
+*.ear

+ 13 - 7
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/BaseReport.java

@@ -4,9 +4,11 @@ import java.util.Date;
 
 public class BaseReport {
 	protected String topic;
-	private Boolean reportOnException=false;
+	protected String tag;
+	private Boolean reportOnException = false;
 	private Date reportTime;
 	private String reportHost;
+	private String remoteHost;
 
 	public Date getReportTime() {
 		return reportTime;
@@ -32,16 +34,20 @@ public class BaseReport {
 		this.reportOnException = reportOnException;
 	}
 
-	
 	public String getTopic() {
 		return topic;
 	}
 
-	public BaseReport(Boolean reportOnException, Date reportTime, String reportHost) {
-		super();
-		this.reportOnException = reportOnException;
-		this.reportTime = reportTime;
-		this.reportHost = reportHost;
+	public String getTag() {
+		return tag;
+	}
+
+	public String getRemoteHost() {
+		return remoteHost;
+	}
+
+	public void setRemoteHost(String remoteHost) {
+		this.remoteHost = remoteHost;
 	}
 
 	public BaseReport() {

+ 6 - 15
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineExamStudentReport.java

@@ -1,7 +1,6 @@
 package cn.com.qmth.examcloud.reports.commons.bean;
 
-import java.util.Date;
-
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
 import cn.com.qmth.examcloud.reports.commons.enums.Topic;
 
 public class OnlineExamStudentReport extends BaseReport {
@@ -42,25 +41,17 @@ public class OnlineExamStudentReport extends BaseReport {
 	public void setExamStudentId(Long examStudentId) {
 		this.examStudentId = examStudentId;
 	}
-
-	public OnlineExamStudentReport(Long rootOrgId, Long studentId,
-			Long examId, Long examStudentId) {
+	public OnlineExamStudentReport() {
 		super();
-		this.rootOrgId = rootOrgId;
-		this.studentId = studentId;
-		this.examId = examId;
-		this.examStudentId = examStudentId;
-		this.topic = Topic.EXAM_STUDENT.getCode();
 	}
-
-	public OnlineExamStudentReport(Boolean reportOnException, Date reportTime, String reportHost, Long rootOrgId,
-			Long studentId, Long examId, Long examStudentId) {
-		super(reportOnException, reportTime, reportHost);
+	public OnlineExamStudentReport(Long rootOrgId, Long studentId, Long examId, Long examStudentId) {
+		super();
 		this.rootOrgId = rootOrgId;
 		this.studentId = studentId;
 		this.examId = examId;
 		this.examStudentId = examStudentId;
-		this.topic = Topic.EXAM_STUDENT.getCode();
+		this.topic = Topic.REPORT_TOPIC.getCode();
+		this.tag = Tag.ONLINE_EXAM_STUDENT.getCode();
 	}
 
 }

+ 6 - 12
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineStudentReport.java

@@ -1,7 +1,6 @@
 package cn.com.qmth.examcloud.reports.commons.bean;
 
-import java.util.Date;
-
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
 import cn.com.qmth.examcloud.reports.commons.enums.Topic;
 
 public class OnlineStudentReport extends BaseReport {
@@ -24,20 +23,15 @@ public class OnlineStudentReport extends BaseReport {
 		this.studentId = studentId;
 	}
 
-
-	public OnlineStudentReport(Long rootOrgId, Long studentId) {
+	public OnlineStudentReport() {
 		super();
-		this.rootOrgId = rootOrgId;
-		this.studentId = studentId;
-		this.topic = Topic.STUDENT.getCode();
 	}
-
-	public OnlineStudentReport(Boolean reportOnException, Date reportTime, String reportHost, Long rootOrgId,
-			Long studentId) {
-		super(reportOnException, reportTime, reportHost);
+	public OnlineStudentReport(Long rootOrgId, Long studentId) {
+		super();
 		this.rootOrgId = rootOrgId;
 		this.studentId = studentId;
-		this.topic = Topic.STUDENT.getCode();
+		this.topic = Topic.REPORT_TOPIC.getCode();
+		this.tag = Tag.ONLINE_STUDENT.getCode();
 	}
 
 }

+ 6 - 11
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OnlineUserReport.java

@@ -1,13 +1,15 @@
 package cn.com.qmth.examcloud.reports.commons.bean;
 
-import java.util.Date;
-
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
 import cn.com.qmth.examcloud.reports.commons.enums.Topic;
 
 public class OnlineUserReport extends BaseReport {
 	private Long rootOrgId;
 	private Long userId;
 
+	public OnlineUserReport() {
+		super();
+	}
 	public Long getRootOrgId() {
 		return rootOrgId;
 	}
@@ -28,15 +30,8 @@ public class OnlineUserReport extends BaseReport {
 		super();
 		this.rootOrgId = rootOrgId;
 		this.userId = userId;
-		this.topic = Topic.USER.getCode();
-	}
-
-	public OnlineUserReport(Boolean reportOnException, Date reportTime, String reportHost, Long rootOrgId,
-			Long userId) {
-		super(reportOnException, reportTime, reportHost);
-		this.rootOrgId = rootOrgId;
-		this.userId = userId;
-		this.topic = Topic.USER.getCode();
+		this.topic = Topic.REPORT_TOPIC.getCode();
+		this.tag = Tag.ONLINE_USER.getCode();
 	}
 
 }

+ 91 - 0
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/bean/OperateReport.java

@@ -0,0 +1,91 @@
+package cn.com.qmth.examcloud.reports.commons.bean;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.UserType;
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
+import cn.com.qmth.examcloud.reports.commons.enums.Topic;
+
+public class OperateReport extends BaseReport {
+	private Long operateUserId;
+	private Long examStudentId;
+	private Long studentId;
+	private UserType operateUserType;
+	private String operate;
+	private Long rootOrgId;
+
+
+	public OperateReport(Long rootOrgId,Long operateUserId,Long studentId,Long examStudentId, UserType operateClient,String operate) {
+		super();
+		this.rootOrgId = rootOrgId;
+		this.operateUserId = operateUserId;
+		this.studentId = studentId;
+		this.examStudentId=examStudentId;
+		this.operateUserType = operateClient;
+		this.operate = operate;
+		this.topic = Topic.REPORT_TOPIC.getCode();
+		this.tag = Tag.OPERATE_INFO.getCode();
+	}
+	public OperateReport() {
+		super();
+	}
+	public Long getOperateUserId() {
+		return operateUserId;
+	}
+
+
+	public void setOperateUserId(Long operateUserId) {
+		this.operateUserId = operateUserId;
+	}
+
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+
+
+	public UserType getOperateUserType() {
+		return operateUserType;
+	}
+
+
+	public void setOperateUserType(UserType operateUserType) {
+		this.operateUserType = operateUserType;
+	}
+
+
+	public String getOperate() {
+		return operate;
+	}
+
+
+	public void setOperate(String operate) {
+		this.operate = operate;
+	}
+
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+
+}

+ 43 - 0
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/OperateContent.java

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.reports.commons.enums;
+
+/**
+ * 操作内容
+ * 
+ * @author chenken
+ *
+ */
+public enum OperateContent {
+
+	EXAM_STUDENT_ADD("考生新增"), 
+	EXAM_STUDENT_COPY("考生复制"), 
+	EXAM_STUDENT_UPDATE("考生更新"), 
+	EXAM_STUDENT_DISABLE("考生禁用"),
+	STUDENT_DISABLE("学生禁用"), 
+	EXAM_STUDENT_ENABLE("考生启用"), 
+	STUDENT_ENABLE("学生启用"), 
+	STUDENT_LOGIN("学生登录"),
+	STUDENT_PHOTO_IMPORT("学生照片导入"), 
+	STUDENT_PASSWORD_RESET("学生密码重置"), 
+	EXAM_STUDENT_IMPORT("考生导入"),
+	STUDENT_UNBIND_PHONE("解绑安全手机"), 
+	STUDENT_UNBIND_STUDENTCODE("解绑学号"), 
+	STUDENT_PASSWORD_UPDATE("学生修改密码");
+
+	/**
+	 * 描述
+	 */
+	private String desc;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param desc
+	 */
+	private OperateContent(String desc) {
+		this.desc = desc;
+	}
+
+	public String getDesc() {
+		return desc;
+	}
+}

+ 70 - 0
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/Tag.java

@@ -0,0 +1,70 @@
+package cn.com.qmth.examcloud.reports.commons.enums;
+
+public enum Tag {
+
+	/**
+	 * 学生
+	 */
+	ONLINE_STUDENT("ONLINE_STUDENT","ONLINE_STUDENT_GROUP", "学生"),
+
+	/**
+	 * 考生
+	 */
+	ONLINE_EXAM_STUDENT("ONLINE_EXAM_STUDENT","ONLINE_EXAM_STUDENT_GROUP", "考生"),
+
+	/**
+	 * 后台用户
+	 */
+	ONLINE_USER("ONLINE_USER","ONLINE_USER_GROUP", "后台用户"),
+
+	/**
+	 * 操作日志
+	 */
+	OPERATE_INFO("OPERATE_INFO","OPERATE_INFO_GROUP","操作日志"),
+
+	/**
+	 * 考试过程记录
+	 */
+	EXAM_PROCESS_RECORD("EXAM_PROCESS_RECORD","EXAM_PROCESS_RECORD_GROUP","考试过程记录")
+	;
+
+	// ===========================================================================
+
+	/**
+	 * 码
+	 */
+	private String code;
+
+	/**
+	 * 描述
+	 */
+	private String group;
+	/**
+	 * 描述
+	 */
+	private String desc;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param desc
+	 */
+	private Tag(String code, String group,String desc) {
+		this.code = code;
+		this.group=group;
+		this.desc = desc;
+	}
+
+	public String getDesc() {
+		return desc;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public String getGroup() {
+		return group;
+	}
+
+}

+ 2 - 12
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/enums/Topic.java

@@ -3,19 +3,9 @@ package cn.com.qmth.examcloud.reports.commons.enums;
 public enum Topic {
 
 	/**
-	 * 学生
+	 * 打点通用
 	 */
-	STUDENT("STUDENT", "学生"),
-
-	/**
-	 * 考生
-	 */
-	EXAM_STUDENT("EXAM_STUDENT", "考生"),
-
-	/**
-	 * 后台用户
-	 */
-	USER("USER", "后台用户");
+	REPORT_TOPIC("REPORT_TOPIC", "打点通用");
 
 	// ===========================================================================
 

+ 0 - 29
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/handler/KafkaSendResultHandler.java

@@ -1,29 +0,0 @@
-package cn.com.qmth.examcloud.reports.commons.handler;
-
-import org.apache.kafka.clients.producer.Callback;
-import org.apache.kafka.clients.producer.RecordMetadata;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class KafkaSendResultHandler implements Callback {
-	private final static Logger logger = LoggerFactory.getLogger(KafkaSendResultHandler.class);
-	private final String topic;
-    private final String message;
-    
-    
-	public KafkaSendResultHandler(String topic,String message) {
-		super();
-		this.topic = topic;
-		this.message = message;
-	}
-
-
-	@Override
-	public void onCompletion(RecordMetadata metadata, Exception exception) {
-		if(exception==null) {
-			logger.debug("kafka message send success : "+" "+topic+" "+message);
-		}else {
-			logger.error("kafka message send error :"+" "+topic+" "+message,exception);
-		}
-	}
-}

+ 49 - 34
examcloud-reports-commons/src/main/java/cn/com/qmth/examcloud/reports/commons/util/ReportsUtil.java

@@ -2,61 +2,71 @@ package cn.com.qmth.examcloud.reports.commons.util;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Properties;
 
+import javax.servlet.http.HttpServletRequest;
+
+import cn.com.qmth.examcloud.web.support.IpUtil;
 import org.apache.commons.collections.CollectionUtils;
-import org.apache.kafka.clients.producer.KafkaProducer;
-import org.apache.kafka.clients.producer.ProducerRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.alibaba.fastjson.JSON;
+import com.aliyun.openservices.ons.api.Message;
+import com.aliyun.openservices.ons.api.ONSFactory;
+import com.aliyun.openservices.ons.api.OnExceptionContext;
+import com.aliyun.openservices.ons.api.Producer;
+import com.aliyun.openservices.ons.api.PropertyKeyConst;
+import com.aliyun.openservices.ons.api.SendCallback;
+import com.aliyun.openservices.ons.api.SendResult;
 
 import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
 import cn.com.qmth.examcloud.reports.commons.bean.BaseReport;
 import cn.com.qmth.examcloud.reports.commons.enums.MqType;
-import cn.com.qmth.examcloud.reports.commons.handler.KafkaSendResultHandler;
 import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import sun.net.util.IPAddressUtil;
 
 public class ReportsUtil {
 
 	private final static Logger logger = LoggerFactory.getLogger(ReportsUtil.class);
 
-	private static final String KEY = "report";
+	private static final String KEY = "mq-report-data";
 
-	private static KafkaProducer<String, String> kafkaProducer;
+	private static Producer producer;
 
-	private static String mqType = PropertyHolder.getString("$report.mq-type", "kafka");
+	private static String mqType = PropertyHolder.getString("$report.mq-type", "rocketmq");
 
 	private static Boolean reportEnable = PropertyHolder.getBoolean("$report.enable", false);
 
 	static {
 		if (reportEnable) {
-			if (MqType.KAFKA.getCode().equals(mqType)) {
-
-				Properties props = new Properties();
-
-				props.put("bootstrap.servers",
-						PropertyHolder.getString("$kafka-bootstrap-servers"));
-				props.put("key.serializer", PropertyHolder.getString("$kafka-key-serializer",
-						"org.apache.kafka.common.serialization.StringSerializer"));
-				props.put("value.serializer", PropertyHolder.getString("$kafka-value-serializer",
-						"org.apache.kafka.common.serialization.StringSerializer"));
-
-				kafkaProducer = new KafkaProducer<String, String>(props);
-
-			} else if (MqType.ROCKETMQ.getCode().equals(mqType)) {
-				// TODO
+			if (MqType.ROCKETMQ.getCode().equals(mqType)) {
+				Properties properties = new Properties();
+				// AccessKeyId 阿里云身份验证,在阿里云服务器管理控制台创建。
+				properties.put(PropertyKeyConst.AccessKey, PropertyHolder.getString("$rocketmq-accesskey"));
+				// AccessKeySecret 阿里云身份验证,在阿里云服务器管理控制台创建。
+				properties.put(PropertyKeyConst.SecretKey, PropertyHolder.getString("$rocketmq-secretkey"));
+				// 设置发送超时时间,单位毫秒。
+				properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, "3000");
+				// 设置 TCP 接入域名,进入控制台的实例详情页面的 TCP 协议客户端接入点区域查看。
+				properties.put(PropertyKeyConst.NAMESRV_ADDR, PropertyHolder.getString("$rocketmq-namesrv-addr"));
+
+				producer = ONSFactory.createProducer(properties);
+				// 在发送消息前,必须调用 start 方法来启动 Producer,只需调用一次即可。
+				producer.start();
 			} else {
 				logger.error("value of property[$report.mq-type] is wrong!");
 			}
 		}
 	}
 
-	private static void sendReportKafka(Boolean onException) {
+
+	private static void sendReportRocket(Boolean onException) {
 		@SuppressWarnings("unchecked")
 		List<BaseReport> list = (List<BaseReport>) ThreadLocalUtil.get(KEY);
 		if (CollectionUtils.isNotEmpty(list)) {
@@ -64,8 +74,19 @@ public class ReportsUtil {
 				if (!onException || (onException && b.getReportOnException())) {
 					try {
 						String messageStr = JSON.toJSONString(b);
-						kafkaProducer.send(new ProducerRecord<>(b.getTopic(), messageStr),
-								new KafkaSendResultHandler(b.getTopic(), messageStr));
+						Message msg = new Message(b.getTopic(), b.getTag(), messageStr.getBytes("utf-8"));
+						producer.sendAsync(msg, new SendCallback() {
+							@Override
+							public void onSuccess(final SendResult sendResult) {
+								// 消息发送成功。
+							}
+
+							@Override
+							public void onException(OnExceptionContext context) {
+								// 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
+								logger.error("SendReport failed msgId:"+msg+" " + messageStr, context.getException());
+							}
+						});
 					} catch (Exception e) {
 						logger.error("SendReport Error:" + JSON.toJSONString(b), e);
 					}
@@ -75,10 +96,6 @@ public class ReportsUtil {
 		}
 	}
 
-	private static void sendReportRocket(Boolean onException) {
-		// TODO
-	}
-
 	public static void report(BaseReport report) {
 		try {
 			if (!reportEnable) {
@@ -98,23 +115,21 @@ public class ReportsUtil {
 	}
 
 	public static void sendReport(Boolean onException) {
-		if (MqType.KAFKA.getCode().equals(mqType)) {
-			sendReportKafka(onException);
-		} else if (MqType.ROCKETMQ.getCode().equals(mqType)) {
+		if (MqType.ROCKETMQ.getCode().equals(mqType)) {
 			sendReportRocket(onException);
 		}
 	}
 
 	private static void setReportCommonData(BaseReport report) {
-		String ip = null;
+		String ip = "";
 		try {
 			InetAddress localHost = Inet4Address.getLocalHost();
 			ip = localHost.getHostAddress();
 		} catch (Exception e) {
-			ip = "";
+			logger.debug("获取本机IP出错", e);
 		}
 		report.setReportHost(ip);
+		report.setRemoteHost(IpUtil.getRemoteIp(ServletUtil.getRequest()));
 		report.setReportTime(new Date());
 	}
-
 }

+ 19 - 0
examcloud-starters/.gitignore

@@ -0,0 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
+.project
+.classpath
+.settings
+target/
+.idea/
+*.iml
+
+# Log Files
+*.log
+*.class
+
+
+# Package Files #
+*.jar
+*.war
+*.ear

+ 64 - 0
examcloud-starters/examcloud-geetest-starter/pom.xml

@@ -0,0 +1,64 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>cn.com.qmth.examcloud.starters</groupId>
+    <artifactId>examcloud-geetest-starter</artifactId>
+    <version>v4.0.1-RELEASE</version>
+    <packaging>jar</packaging>
+
+    <parent>
+        <groupId>cn.com.qmth.examcloud</groupId>
+        <artifactId>examcloud-parent</artifactId>
+        <version>v4.0.1-RELEASE</version>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>${basedir}/src/main/resources</directory>
+                <includes>
+                    <include>**/*.*</include>
+                </includes>
+            </resource>
+        </resources>
+    </build>
+
+</project>

+ 34 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/GeetestAutoConfiguration.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest;
+
+import cn.com.qmth.examcloud.starters.greetest.service.GeetestService;
+import cn.com.qmth.examcloud.starters.greetest.service.GeetestSessionManager;
+import cn.com.qmth.examcloud.starters.greetest.service.impl.GeetestServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties(GeetestProperties.class)
+public class GeetestAutoConfiguration {
+
+    private final static Logger log = LoggerFactory.getLogger(GeetestAutoConfiguration.class);
+
+    @Bean
+    public GeetestService geetestService(@Autowired GeetestProperties properties, @Autowired GeetestSessionManager sessionManager) {
+        log.info("geetestService init...");
+
+        GeetestServiceImpl impl = new GeetestServiceImpl();
+        impl.setProperties(properties);
+        impl.setSessionManager(sessionManager);
+        return impl;
+    }
+
+}

+ 112 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/GeetestProperties.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.io.Serializable;
+
+@ConfigurationProperties(prefix = "examcloud.starters.geetest", ignoreUnknownFields = false)
+public class GeetestProperties implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 极验-接口地址
+     */
+    private String apiUrl;
+
+    /**
+     * 极验-公钥
+     */
+    private String id;
+
+    /**
+     * 极验-私钥
+     */
+    private String key;
+
+    /**
+     * 极验-客户端类型
+     * 默认值:web
+     */
+    private String client_type = "web";
+
+    /**
+     * 极验-字符串的签名算法
+     * 默认值:md5
+     */
+    private String digestmod = "md5";
+
+    /**
+     * 极验-json格式化标识
+     * 默认值:1
+     */
+    private String json_format = "1";
+
+    /**
+     * 极验-sdk代码版本号
+     * 默认值:jave-servlet:3.1.0
+     */
+    private String sdk = "jave-servlet:3.1.0";
+
+    public String getApiUrl() {
+        return apiUrl;
+    }
+
+    public void setApiUrl(String apiUrl) {
+        this.apiUrl = apiUrl;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getClient_type() {
+        return client_type;
+    }
+
+    public void setClient_type(String client_type) {
+        this.client_type = client_type;
+    }
+
+    public String getDigestmod() {
+        return digestmod;
+    }
+
+    public void setDigestmod(String digestmod) {
+        this.digestmod = digestmod;
+    }
+
+    public String getJson_format() {
+        return json_format;
+    }
+
+    public void setJson_format(String json_format) {
+        this.json_format = json_format;
+    }
+
+    public String getSdk() {
+        return sdk;
+    }
+
+    public void setSdk(String sdk) {
+        this.sdk = sdk;
+    }
+
+}

+ 12 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/IModel.java

@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-24 10:06:25
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.model;
+
+import java.io.Serializable;
+
+public interface IModel extends Serializable {
+
+}

+ 58 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/RegisterReq.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.model;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class RegisterReq implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 作为终端用户的唯一标识,确定用户的唯一性;
+     * 作用于提供进阶数据分析服务,可在api1 或 api2 接口传入,不传入也不影响验证服务的使用;
+     * 若担心用户信息风险,可作预处理(如哈希处理)再提供到极验
+     */
+    @ApiModelProperty(value = "账号唯一标识", required = true)
+    protected String user_id;
+
+    /**
+     * 客户端类型,web(pc浏览器),h5(手机浏览器,包括webview),native(原生app),unknown(未知)
+     */
+    @ApiModelProperty(value = "客户端类型(选填)")
+    protected String client_type;
+
+    /**
+     * 客户端请求SDK服务器的ip地址
+     */
+    @ApiModelProperty(hidden = true)
+    protected String ip_address;
+
+    public String getUser_id() {
+        return user_id;
+    }
+
+    public void setUser_id(String user_id) {
+        this.user_id = user_id;
+    }
+
+    public String getClient_type() {
+        return client_type;
+    }
+
+    public void setClient_type(String client_type) {
+        this.client_type = client_type;
+    }
+
+    public String getIp_address() {
+        return ip_address;
+    }
+
+    public void setIp_address(String ip_address) {
+        this.ip_address = ip_address;
+    }
+
+}

+ 94 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/RegisterResp.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.model;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+public class RegisterResp implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程正常、异常标识;true表示正常,false表示异常、后续走宕机模式
+     */
+    protected Boolean success;
+
+    /**
+     * 结果提示信息
+     */
+    protected String msg;
+
+    /**
+     * 生成唯一流水号的参考字符串,为”0”表示传参账号id有误
+     */
+    private String challenge;
+
+    /**
+     * 新版验证码标识,固定不变
+     */
+    private Boolean new_captcha;
+
+    /**
+     * 向极验申请的账号id
+     */
+    private String gt;
+
+    public RegisterResp(Boolean success, String msg) {
+        this.success = success;
+        this.msg = msg;
+    }
+
+    public RegisterResp() {
+
+    }
+
+    public Boolean getSuccess() {
+        return success;
+    }
+
+    public void setSuccess(Boolean success) {
+        this.success = success;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public String getChallenge() {
+        return challenge;
+    }
+
+    public void setChallenge(String challenge) {
+        this.challenge = challenge;
+    }
+
+    public Boolean getNew_captcha() {
+        return new_captcha;
+    }
+
+    public void setNew_captcha(Boolean new_captcha) {
+        this.new_captcha = new_captcha;
+    }
+
+    public String getGt() {
+        return gt;
+    }
+
+    public void setGt(String gt) {
+        this.gt = gt;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 102 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/ValidateReq.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.model;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+public class ValidateReq implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 作为终端用户的唯一标识,确定用户的唯一性;
+     * 作用于提供进阶数据分析服务,可在api1 或 api2 接口传入,不传入也不影响验证服务的使用;
+     * 若担心用户信息风险,可作预处理(如哈希处理)再提供到极验
+     */
+    @ApiModelProperty(value = "账号唯一标识", required = true)
+    protected String user_id;
+
+    /**
+     * 客户端类型,web(pc浏览器),h5(手机浏览器,包括webview),native(原生app),unknown(未知)
+     */
+    @ApiModelProperty(value = "客户端类型(选填)")
+    protected String client_type;
+
+    /**
+     * 客户端请求SDK服务器的ip地址
+     */
+    @ApiModelProperty(hidden = true)
+    protected String ip_address;
+
+    /**
+     * 待校验的核心数据,客户端向极验请求的 api.geetest.com/ajax.php 接口返回得到
+     */
+    @ApiModelProperty(required = true)
+    protected String validate;
+
+    /**
+     * 待校验的核心数据,validate加上 |jordan 组成的字符串
+     */
+    @ApiModelProperty(required = true)
+    protected String seccode;
+
+    /**
+     * 流水号,一次完整验证流程的唯一标识
+     */
+    @ApiModelProperty(required = true)
+    protected String challenge;
+
+    public String getUser_id() {
+        return user_id;
+    }
+
+    public void setUser_id(String user_id) {
+        this.user_id = user_id;
+    }
+
+    public String getClient_type() {
+        return client_type;
+    }
+
+    public void setClient_type(String client_type) {
+        this.client_type = client_type;
+    }
+
+    public String getIp_address() {
+        return ip_address;
+    }
+
+    public void setIp_address(String ip_address) {
+        this.ip_address = ip_address;
+    }
+
+    public String getValidate() {
+        return validate;
+    }
+
+    public void setValidate(String validate) {
+        this.validate = validate;
+    }
+
+    public String getSeccode() {
+        return seccode;
+    }
+
+    public void setSeccode(String seccode) {
+        this.seccode = seccode;
+    }
+
+    public String getChallenge() {
+        return challenge;
+    }
+
+    public void setChallenge(String challenge) {
+        this.challenge = challenge;
+    }
+
+}

+ 68 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/model/ValidateResp.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.model;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+public class ValidateResp implements IModel {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程正常、异常标识;true表示正常,false表示异常、后续走宕机模式
+     */
+    protected Boolean success;
+
+    /**
+     * 结果提示信息
+     */
+    protected String msg;
+
+    /**
+     * 服务端sdk版本
+     */
+    private String version;
+
+    public ValidateResp(Boolean success, String msg) {
+        this.success = success;
+        this.msg = msg;
+    }
+
+    public ValidateResp() {
+
+    }
+
+    public Boolean getSuccess() {
+        return success;
+    }
+
+    public void setSuccess(Boolean success) {
+        this.success = success;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 28 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/GeetestService.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.service;
+
+import cn.com.qmth.examcloud.starters.greetest.model.RegisterReq;
+import cn.com.qmth.examcloud.starters.greetest.model.RegisterResp;
+import cn.com.qmth.examcloud.starters.greetest.model.ValidateReq;
+import cn.com.qmth.examcloud.starters.greetest.model.ValidateResp;
+
+/**
+ * 极验验证码相关接口
+ */
+public interface GeetestService {
+
+    /**
+     * 验证初始化
+     */
+    RegisterResp register(RegisterReq req);
+
+    /**
+     * 二次验证
+     */
+    ValidateResp validate(ValidateReq req);
+
+}

+ 19 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/GeetestSessionManager.java

@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-24 09:43:49
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.service;
+
+/**
+ * 极验-验证会话管理
+ */
+public interface GeetestSessionManager {
+
+    void setSession(String key, Boolean status);
+
+    Boolean getSession(String key);
+
+    void removeSession(String key);
+
+}

+ 223 - 0
examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/impl/GeetestServiceImpl.java

@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-07-22 16:19:27
+ */
+
+package cn.com.qmth.examcloud.starters.greetest.service.impl;
+
+import cn.com.qmth.examcloud.starters.greetest.GeetestProperties;
+import cn.com.qmth.examcloud.starters.greetest.model.RegisterReq;
+import cn.com.qmth.examcloud.starters.greetest.model.RegisterResp;
+import cn.com.qmth.examcloud.starters.greetest.model.ValidateReq;
+import cn.com.qmth.examcloud.starters.greetest.model.ValidateResp;
+import cn.com.qmth.examcloud.starters.greetest.service.GeetestService;
+import cn.com.qmth.examcloud.starters.greetest.service.GeetestSessionManager;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.*;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 极验验证码相关接口
+ */
+public class GeetestServiceImpl implements GeetestService {
+
+    private final static Logger log = LoggerFactory.getLogger(GeetestServiceImpl.class);
+
+    private GeetestProperties properties;
+
+    private GeetestSessionManager sessionManager;
+
+    /**
+     * 验证初始化
+     */
+    @Override
+    public RegisterResp register(RegisterReq req) {
+        if (StringUtils.isEmpty(req.getUser_id())) {
+            return new RegisterResp(false, "参数“user_id”值不能为空");
+        }
+
+        String params = this.buildRegisterParams(req);
+        log.info("[Geetest register] " + params);
+        String requestUrl = properties.getApiUrl() + "/register.php" + params;
+
+        String challenge = null;
+        Request.Builder request = new Request.Builder().url(requestUrl).get();
+        try (Response response = this.httpClient().newCall(request.build()).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+            log.info("[Geetest register] response:" + response.code() + " body:" + bodyStr);
+
+            if (response.isSuccessful()) {
+                challenge = this.parseJsonValue(bodyStr, "challenge");
+            }
+        } catch (Exception e) {
+            log.error("[Geetest register] error:" + e.getMessage(), e);
+        }
+
+        // 封装结果
+        RegisterResp resp = new RegisterResp();
+        resp.setNew_captcha(true);
+        resp.setGt(properties.getId());
+        if (StringUtils.isEmpty(challenge) || "0".equals(challenge)) {
+            // 请求极验register接口失败,后续流程走宕机模式
+            resp.setChallenge(UUID.randomUUID().toString().replaceAll("-", ""));
+            resp.setSuccess(false);
+        } else {
+            // 默认实现MD5签名算法
+            resp.setChallenge(DigestUtils.md5Hex(challenge + properties.getKey()));
+            resp.setSuccess(true);
+        }
+
+        // 设置会话信息
+        sessionManager.setSession(req.getUser_id(), resp.getSuccess());
+
+        return resp;
+    }
+
+    /**
+     * 二次验证
+     */
+    @Override
+    public ValidateResp validate(ValidateReq req) {
+        if (StringUtils.isEmpty(req.getUser_id())) {
+            return new ValidateResp(false, "参数“user_id”值不能为空");
+        }
+        if (StringUtils.isEmpty(req.getChallenge())) {
+            return new ValidateResp(false, "参数“challenge”值不能为空");
+        }
+        if (StringUtils.isEmpty(req.getValidate())) {
+            return new ValidateResp(false, "参数“validate”值不能为空");
+        }
+        if (StringUtils.isEmpty(req.getSeccode())) {
+            return new ValidateResp(false, "参数“seccode”值不能为空");
+        }
+
+        // 校验会话信息
+        Boolean status = sessionManager.getSession(req.getUser_id());
+        if (status == null) {
+            return new ValidateResp(false, "验证码已过期,请重试!");
+        }
+        if (status == false) {
+            return new ValidateResp(true, "宕机通过");
+        }
+
+        String params = this.buildValidateParams(req);
+        log.info("[Geetest validate] " + params);
+        String requestUrl = properties.getApiUrl() + "/validate.php" + params;
+
+        String seccode = null;
+        RequestBody formBody = new FormBody.Builder(Charset.forName("UTF-8")).build();
+        Request.Builder request = new Request.Builder().url(requestUrl).post(formBody);
+        try (Response response = this.httpClient().newCall(request.build()).execute();
+             ResponseBody body = response.body();) {
+            String bodyStr = body.string();
+            log.info("[Geetest validate] response:" + response.code() + " body:" + bodyStr);
+
+            if (response.isSuccessful()) {
+                seccode = this.parseJsonValue(bodyStr, "seccode");
+            }
+        } catch (Exception e) {
+            log.error("[Geetest validate] error:" + e.getMessage(), e);
+        }
+
+        // 封装结果
+        ValidateResp resp = new ValidateResp();
+        if (StringUtils.isEmpty(seccode) || "false".equals(seccode)) {
+            resp.setSuccess(false);
+            resp.setMsg("验证不通过");
+            resp.setVersion(properties.getSdk());
+
+            // 清理会话信息
+            sessionManager.removeSession(req.getUser_id());
+        } else {
+            resp.setSuccess(true);
+            resp.setMsg("验证通过");
+        }
+
+        return resp;
+    }
+
+    private String buildRegisterParams(RegisterReq req) {
+        StringBuilder params = new StringBuilder();
+
+        try {
+            final String charset = "UTF-8";
+            params.append("?").append("user_id").append("=").append(StringUtils.isNotEmpty(req.getUser_id())
+                    ? URLEncoder.encode(req.getUser_id(), charset) : "");
+            params.append("&").append("client_type").append("=").append(StringUtils.isNotEmpty(req.getClient_type())
+                    ? URLEncoder.encode(req.getClient_type(), charset) : properties.getClient_type());
+            params.append("&").append("ip_address").append("=").append(StringUtils.isNotEmpty(req.getIp_address())
+                    ? URLEncoder.encode(req.getIp_address(), charset) : "");
+
+            params.append("&").append("digestmod").append("=").append(URLEncoder.encode(properties.getDigestmod(), charset));
+            params.append("&").append("gt").append("=").append(URLEncoder.encode(properties.getId(), charset));
+            params.append("&").append("json_format").append("=").append(URLEncoder.encode(properties.getJson_format(), charset));
+            params.append("&").append("sdk").append("=").append(URLEncoder.encode(properties.getSdk(), charset));
+        } catch (Exception e) {
+            // ignore
+        }
+
+        return params.toString();
+    }
+
+    private String buildValidateParams(ValidateReq req) {
+        StringBuilder params = new StringBuilder();
+
+        try {
+            final String charset = "UTF-8";
+            params.append("?").append("user_id").append("=").append(StringUtils.isNotEmpty(req.getUser_id())
+                    ? URLEncoder.encode(req.getUser_id(), charset) : "");
+            params.append("&").append("client_type").append("=").append(StringUtils.isNotEmpty(req.getClient_type())
+                    ? URLEncoder.encode(req.getClient_type(), charset) : properties.getClient_type());
+            params.append("&").append("ip_address").append("=").append(StringUtils.isNotEmpty(req.getIp_address())
+                    ? URLEncoder.encode(req.getIp_address(), charset) : "");
+
+            params.append("&").append("seccode").append("=").append(URLEncoder.encode(req.getSeccode(), charset));
+            params.append("&").append("challenge").append("=").append(URLEncoder.encode(req.getChallenge(), charset));
+            params.append("&").append("captchaid").append("=").append(URLEncoder.encode(properties.getId(), charset));
+            params.append("&").append("json_format").append("=").append(URLEncoder.encode(properties.getJson_format(), charset));
+            params.append("&").append("sdk").append("=").append(URLEncoder.encode(properties.getSdk(), charset));
+        } catch (Exception e) {
+            // ignore
+        }
+
+        return params.toString();
+    }
+
+    public void setProperties(GeetestProperties properties) {
+        this.properties = properties;
+    }
+
+    public void setSessionManager(GeetestSessionManager sessionManager) {
+        this.sessionManager = sessionManager;
+    }
+
+    private OkHttpClient httpClient() {
+        return new OkHttpClient
+                .Builder()
+                .connectTimeout(15, TimeUnit.SECONDS)
+                .readTimeout(15, TimeUnit.SECONDS)
+                .writeTimeout(15, TimeUnit.SECONDS)
+                .build();
+    }
+
+    private String parseJsonValue(String json, String fieldName) {
+        try {
+            JsonNode jsonNode = new ObjectMapper().readTree(json);
+            return jsonNode.get(fieldName).asText();
+        } catch (Exception e) {
+            log.warn(String.format("[%s] json value not exist...", fieldName));
+        }
+        return null;
+    }
+
+}

+ 2 - 0
examcloud-starters/examcloud-geetest-starter/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+cn.com.qmth.examcloud.starters.greetest.GeetestAutoConfiguration

+ 10 - 12
examcloud-support/.gitignore

@@ -1,21 +1,19 @@
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.idea/
-*.iml
+*.class
 
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-
+*.war
+*.ear

+ 25 - 27
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/CacheHelper.java

@@ -1,29 +1,6 @@
 package cn.com.qmth.examcloud.support.cache;
 
-import cn.com.qmth.examcloud.support.cache.bean.AppCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.BasePaperCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamOrgPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamOrgSettingsCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamRecordPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamSettingsCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamStudentCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamStudentPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamStudentSettingsCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigPaperCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.OrgCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.OrgPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.PrivilegeRolesCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.QuestionAnswerCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.RootOrgCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.RootOrgPrivilegesCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.SmsAssemblyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ThirdPartyAccessCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.*;
 import cn.com.qmth.examcloud.web.cache.HashRedisCacheProcessor;
 import cn.com.qmth.examcloud.web.cache.ObjectRedisCacheProcessor;
 
@@ -63,7 +40,7 @@ public class CacheHelper {
 	/**
 	 * 根据考试记录id获取考试信息<br>
 	 *
-	 * @param propKey
+	 * @param examRecordDataId
 	 * @return
 	 * @author WANGWEI
 	 */
@@ -201,8 +178,7 @@ public class CacheHelper {
 	/**
 	 * 查询学生
 	 *
-	 * @param courseId
-	 * @param rootOrgId
+	 * @param studentId
 	 * @return
 	 * @author WANGWEI
 	 */
@@ -366,4 +342,26 @@ public class CacheHelper {
 				"cn.com.qmth.examcloud.core.oe.admin.service.cache.ExamStudentCache");
 	}
 
+	/**
+	 * 获取场次信息
+	 * @param examId
+	 * @param examStageId
+	 * @return
+	 */
+	public static ExamStageCacheBean getExamStage(Long examId,Long examStageId) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_STAGE:", new Object[]{examId, examStageId},
+				ExamStageCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamStageCache");
+	}
+
+	/**
+	 * 获取考试下的场次集合
+	 * @param examId
+	 * @return
+	 */
+	public static ExamStagesCacheBean getExamStages(Long examId) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_STAGE:", new Object[]{examId},
+				ExamStagesCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamStagesCache");
+	}
 }

+ 158 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStageCacheBean.java

@@ -0,0 +1,158 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamStageStartExamStatus;
+import cn.com.qmth.examcloud.api.commons.enums.SubmitType;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.persistence.Column;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.util.Date;
+
+/**
+ * @Description 场次缓存
+ * @Author lideyin
+ * @Date 2020/8/14 16:14
+ * @Version 1.0
+ */
+public class ExamStageCacheBean extends RandomCacheBean {
+	private static final long serialVersionUID = 7837714620786777681L;
+
+
+	private Long id;
+	/**
+	 * 顶级机构Id
+	 */
+	private Long rootOrgId;
+
+	/**
+	 * 考试id
+	 */
+	private Long examId;
+
+	/**
+	 * 场次号
+	 */
+	private Integer stageOrder;
+
+	/**
+	 * 开始进入考试时间
+	 */
+	private Date startTime;
+
+	/**
+	 * 考试批次结束时间
+	 */
+	private Date endTime;
+
+	private Boolean enable;
+
+	/**
+	 * 是否特殊设置
+	 */
+	private Boolean specialSetting;
+
+	/**
+	 * 回收试卷类型
+	 */
+	private String submitType;
+
+	/**
+	 * 统一收卷考试时长(分钟)
+	 */
+	private Integer submitDuration;
+
+	/**
+	 * 场次开考状态
+	 */
+	private String startExamStatus;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Integer getStageOrder() {
+		return stageOrder;
+	}
+
+	public void setStageOrder(Integer stageOrder) {
+		this.stageOrder = stageOrder;
+	}
+
+	public Date getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public Boolean getSpecialSetting() {
+		return specialSetting;
+	}
+
+	public void setSpecialSetting(Boolean specialSetting) {
+		this.specialSetting = specialSetting;
+	}
+
+	public String getSubmitType() {
+		return submitType;
+	}
+
+	public void setSubmitType(String submitType) {
+		this.submitType = submitType;
+	}
+
+	public Integer getSubmitDuration() {
+		return submitDuration;
+	}
+
+	public void setSubmitDuration(Integer submitDuration) {
+		this.submitDuration = submitDuration;
+	}
+
+	public String getStartExamStatus() {
+		return startExamStatus;
+	}
+
+	public void setStartExamStatus(String startExamStatus) {
+		this.startExamStatus = startExamStatus;
+	}
+}

+ 38 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStagesCacheBean.java

@@ -0,0 +1,38 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+import java.util.List;
+
+/**
+ * @Description 场次集合缓存
+ * @Author lideyin
+ * @Date 2020/8/14 16:14
+ * @Version 1.0
+ */
+public class ExamStagesCacheBean extends RandomCacheBean {
+	private static final long serialVersionUID = 7837714620786777681L;
+
+	/**
+	 * 考试id
+	 */
+	private Long examId;
+
+	private List<ExamStageCacheBean> examStages;
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public List<ExamStageCacheBean> getExamStages() {
+		return examStages;
+	}
+
+	public void setExamStages(List<ExamStageCacheBean> examStages) {
+		this.examStages = examStages;
+	}
+}

+ 26 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentCacheBean.java

@@ -58,6 +58,16 @@ public class ExamStudentCacheBean extends RandomCacheBean {
 	 */
 	private String grade;
 
+	/**
+	 * 场次id
+	 */
+	private Long examStageId;
+
+	/**
+	 * 场次号
+	 */
+	private Integer examStageOrder;
+
 	public Long getExamStudentId() {
 		return examStudentId;
 	}
@@ -129,4 +139,20 @@ public class ExamStudentCacheBean extends RandomCacheBean {
 	public void setGrade(String grade) {
 		this.grade = grade;
 	}
+
+	public Long getExamStageId() {
+		return examStageId;
+	}
+
+	public void setExamStageId(Long examStageId) {
+		this.examStageId = examStageId;
+	}
+
+	public Integer getExamStageOrder() {
+		return examStageOrder;
+	}
+
+	public void setExamStageOrder(Integer examStageOrder) {
+		this.examStageOrder = examStageOrder;
+	}
 }

+ 39 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/ExamProcess.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.support.enums;
+
+/**
+ * @Description 考试过程
+ * @Author lideyin
+ * @Date 2020/8/20 15:26
+ * @Version 1.0
+ */
+public enum ExamProcess {
+    START("START", "开始考试"),
+    IP_CHANGE("IP_CHANGE", "IP变动"),
+    BREAK_OFF("BREAK_OFF", "考试中断"),
+    CONTINUE("CONTINUE", "断点续考"),
+    AUTO_HAND_IN("AUTO_HAND_IN", "服务端交卷"),
+    MANUAL_HAND_IN("MANUAL_HAND_IN", "考生端交卷");
+    private String code;
+    private String desc;
+
+    ExamProcess(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    /**
+     * 考试过程代码
+     * @return
+     */
+    public String getCode(){
+        return this.code;
+    }
+
+    /**
+     * 考试过程描述
+     * @return
+     */
+    public String getDesc(){
+        return this.desc;
+    }
+}

+ 94 - 10
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java

@@ -12,7 +12,7 @@ import cn.com.qmth.examcloud.support.enums.SyncStatus;
 public class ExamRecordData implements JsonSerializable {
 
     /**
-     * 
+     *
      */
     private static final long serialVersionUID = 3881189287358373638L;
 
@@ -67,7 +67,7 @@ public class ExamRecordData implements JsonSerializable {
     private String paperType;
 
     /**
-     * 考试开始时间
+     * 考试开始时间(即开始作答时间)
      */
     private Date startTime;
 
@@ -77,7 +77,7 @@ public class ExamRecordData implements JsonSerializable {
     private Date endTime;
 
     /**
-     * 考试时长
+     * 考试时长(毫秒)
      */
     private Long usedExamTime;
 
@@ -188,17 +188,47 @@ public class ExamRecordData implements JsonSerializable {
      * 数据同步状态
      */
     private SyncStatus syncStatus;
-    
+
     /**
      * 试卷题目数量(校验提交答案的order)
      */
     private Integer questionCount;
-    
+
     /**
      * 是否是全客观题卷  1:是   0:否
      */
     private Boolean isAllObjectivePaper;
 
+    /**
+     * 场次id
+     */
+    private Long examStageId;
+
+    /**
+     * 场次号
+     */
+    private Integer examStageOrder;
+
+    /**
+     * 学生最后活动时间
+     */
+    private Date lastActiveTime;
+
+    /**
+     * 断点续考时间
+     */
+    private Date continuedTime;
+
+    /**
+     * 进入考试时间
+     */
+    private Date enterExamTime;
+
+    /**
+     * 切屏次数
+     */
+    private Integer switchScreenCount;
+
     public Long getId() {
         return id;
     }
@@ -479,26 +509,74 @@ public class ExamRecordData implements JsonSerializable {
         this.baiduFaceLivenessSuccessPercent = baiduFaceLivenessSuccessPercent;
     }
 
-    
+
     public Integer getQuestionCount() {
         return questionCount;
     }
 
-    
+
     public void setQuestionCount(Integer questionCount) {
         this.questionCount = questionCount;
     }
 
-    
+
     public Boolean getIsAllObjectivePaper() {
         return isAllObjectivePaper;
     }
 
-    
+
     public void setIsAllObjectivePaper(Boolean isAllObjectivePaper) {
         this.isAllObjectivePaper = isAllObjectivePaper;
     }
 
+    public Long getExamStageId() {
+        return examStageId;
+    }
+
+    public void setExamStageId(Long examStageId) {
+        this.examStageId = examStageId;
+    }
+
+    public Integer getExamStageOrder() {
+        return examStageOrder;
+    }
+
+    public void setExamStageOrder(Integer examStageOrder) {
+        this.examStageOrder = examStageOrder;
+    }
+
+    public Date getLastActiveTime() {
+        return lastActiveTime;
+    }
+
+    public void setLastActiveTime(Date lastActiveTime) {
+        this.lastActiveTime = lastActiveTime;
+    }
+
+    public Date getContinuedTime() {
+        return continuedTime;
+    }
+
+    public void setContinuedTime(Date continuedTime) {
+        this.continuedTime = continuedTime;
+    }
+
+    public Date getEnterExamTime() {
+        return enterExamTime;
+    }
+
+    public void setEnterExamTime(Date enterExamTime) {
+        this.enterExamTime = enterExamTime;
+    }
+
+    public Integer getSwitchScreenCount() {
+        return switchScreenCount;
+    }
+
+    public void setSwitchScreenCount(Integer switchScreenCount) {
+        this.switchScreenCount = switchScreenCount;
+    }
+
     @Override
     public String toString() {
         return "ExamRecordData{" +
@@ -539,6 +617,12 @@ public class ExamRecordData implements JsonSerializable {
                 ", syncStatus=" + syncStatus +
                 ", questionCount=" + questionCount +
                 ", isAllObjectivePaper=" + isAllObjectivePaper +
+                ", examStageId=" + examStageId +
+                ", examStageOrder=" + examStageOrder +
+                ", lastActiveTime=" + lastActiveTime +
+                ", continuedTime=" + continuedTime +
+                ", enterExamTime=" + enterExamTime +
+                ", switchScreenCount=" + switchScreenCount +
                 '}';
     }
-}
+}

+ 13 - 1
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingActivityTime.java

@@ -23,6 +23,11 @@ public class ExamingActivityTime implements JsonSerializable {
 	 */
 	private Long activeTime;
 
+	/**
+	 *  真实ip
+	 */
+	private String realIp;
+
 	public Long getExamRecordDataId() {
 		return examRecordDataId;
 	}
@@ -39,4 +44,11 @@ public class ExamingActivityTime implements JsonSerializable {
 		this.activeTime = activeTime;
 	}
 
-}
+	public String getRealIp() {
+		return realIp;
+	}
+
+	public void setRealIp(String realIp) {
+		this.realIp = realIp;
+	}
+}

+ 4 - 1
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingHeartbeat.java

@@ -20,6 +20,7 @@ public class ExamingHeartbeat implements JsonSerializable {
 	/**
 	 * 心跳次数
 	 */
+	@Deprecated
 	private Long times;
 
 	/**
@@ -35,10 +36,12 @@ public class ExamingHeartbeat implements JsonSerializable {
 		this.examRecordDataId = examRecordDataId;
 	}
 
+	@Deprecated
 	public Long getTimes() {
 		return times;
 	}
 
+	@Deprecated
 	public void setTimes(Long times) {
 		this.times = times;
 	}
@@ -51,4 +54,4 @@ public class ExamingHeartbeat implements JsonSerializable {
 		this.cost = cost;
 	}
 
-}
+}

+ 264 - 226
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java

@@ -1,9 +1,9 @@
 package cn.com.qmth.examcloud.support.examing;
 
-import java.util.Date;
-
 import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
 
+import java.util.Date;
+
 /**
  * 考试会话
  *
@@ -13,239 +13,277 @@ import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
  */
 public class ExamingSession implements JsonSerializable {
 
-	private static final long serialVersionUID = -7960639278784327099L;
-
-	/**
-	 * 全局唯一考试标识符
-	 */
-	private String key;
-
-	/**
-	 * 顶级机构ID
-	 */
-	private Long rootOrgId;
-
-	/**
-	 * 学习中心ID
-	 */
-	private Long orgId;
-
-	/**
-	 * 考试状态
-	 */
-	private ExamingStatus examingStatus;
-
-	/**
-	 * 学生ID
-	 */
-	private Long studentId;
-
-	/**
-	 * 考试ID
-	 */
-	private Long examId;
-
-	/**
-	 * 课程ID
-	 */
-	private Long courseId;
-
-	/**
-	 * 课程CODE
-	 */
-	private String courseCode;
-
-	/**
-	 * 考生ID
-	 */
-	private Long examStudentId;
-
-	/**
-	 * 创建时间
-	 */
-	private Date creationTime;
-
-	/**
-	 * 试卷类型
-	 */
-	private String paperType;
-
-	/**
-	 * 考试记录DataID
-	 */
-	private Long examRecordDataId;
-
-	/**
-	 * 考试开始时间
-	 */
-	private Long startTime;
-
-	/**
-	 * 冻结时间:分钟
-	 */
-	private Integer freezeTime;
-
-	/**
-	 * 考试类型
-	 */
-	private String examType;
-
-	/**
-	 * 断点续考时间:分钟
-	 */
-	private Integer examReconnectTime;
-
-	/**
-	 * 考试时长:毫秒
-	 */
-	private Long examDuration;
-
-	/**
-	 * 构建key
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	public String buildKey() {
-		this.key = new StringBuilder().append("EXAMING:").append(rootOrgId).append(":")
-				.append(studentId).toString();
-		return this.key;
-	}
-
-	public String getKey() {
-		return key;
-	}
-
-	public void setKey(String key) {
-		this.key = key;
-	}
-
-	public Long getRootOrgId() {
-		return rootOrgId;
-	}
-
-	public void setRootOrgId(Long rootOrgId) {
-		this.rootOrgId = rootOrgId;
-	}
-
-	public ExamingStatus getExamingStatus() {
-		return examingStatus;
-	}
-
-	public void setExamingStatus(ExamingStatus examingStatus) {
-		this.examingStatus = examingStatus;
-	}
-
-	public Long getStudentId() {
-		return studentId;
-	}
-
-	public void setStudentId(Long studentId) {
-		this.studentId = studentId;
-	}
-
-	public Long getExamId() {
-		return examId;
-	}
-
-	public void setExamId(Long examId) {
-		this.examId = examId;
-	}
-
-	public Long getCourseId() {
-		return courseId;
-	}
-
-	public void setCourseId(Long courseId) {
-		this.courseId = courseId;
-	}
-
-	public Long getExamStudentId() {
-		return examStudentId;
-	}
-
-	public void setExamStudentId(Long examStudentId) {
-		this.examStudentId = examStudentId;
-	}
-
-	public Date getCreationTime() {
-		return creationTime;
-	}
-
-	public void setCreationTime(Date creationTime) {
-		this.creationTime = creationTime;
-	}
-
-	public String getPaperType() {
-		return paperType;
-	}
-
-	public void setPaperType(String paperType) {
-		this.paperType = paperType;
-	}
-
-	public Long getOrgId() {
-		return orgId;
-	}
-
-	public void setOrgId(Long orgId) {
-		this.orgId = orgId;
-	}
-
-	public Long getExamRecordDataId() {
-		return examRecordDataId;
-	}
-
-	public void setExamRecordDataId(Long examRecordDataId) {
-		this.examRecordDataId = examRecordDataId;
-	}
-
-	public Long getStartTime() {
-		return startTime;
-	}
-
-	public void setStartTime(Long startTime) {
-		this.startTime = startTime;
-	}
+    private static final long serialVersionUID = -7960639278784327099L;
+
+    /**
+     * 全局唯一考试标识符
+     */
+    private String key;
+
+    /**
+     * 顶级机构ID
+     */
+    private Long rootOrgId;
+
+    /**
+     * 学习中心ID
+     */
+    private Long orgId;
+
+    /**
+     * 考试状态
+     */
+    private ExamingStatus examingStatus;
+
+    /**
+     * 学生ID
+     */
+    private Long studentId;
+
+    /**
+     * 考试ID
+     */
+    private Long examId;
+
+    /**
+     * 课程ID
+     */
+    private Long courseId;
+
+    /**
+     * 课程CODE
+     */
+    private String courseCode;
+
+    /**
+     * 考生ID
+     */
+    private Long examStudentId;
+
+    /**
+     * 创建时间
+     */
+    private Date creationTime;
+
+    /**
+     * 试卷类型
+     */
+    private String paperType;
+
+    /**
+     * 考试记录DataID
+     */
+    private Long examRecordDataId;
+
+    /**
+     * 考试开始时间(开始答题时间)
+     */
+    private Long startTime;
+
+    /**
+     * 冻结时间:分钟
+     */
+    private Integer freezeTime;
+
+    /**
+     * 考试类型
+     */
+    private String examType;
+
+    /**
+     * 断点续考时间:分钟
+     */
+    private Integer examReconnectTime;
+
+    /**
+     * 考试时长:毫秒
+     */
+    private Long examDuration;
+
+    /**
+     * 场次id
+     */
+    private Long examStageId;
+
+    /**
+     * 是否定点交卷
+     */
+    private boolean timingEnd;
+
+    /**
+     * 定点交卷的时间
+     */
+    private Date fixedSubmitTime;
+
+    /**
+     * 构建key
+     *
+     * @return
+     * @author WANGWEI
+     */
+    public String buildKey() {
+        this.key = new StringBuilder().append("EXAMING:").append(rootOrgId).append(":")
+                .append(studentId).toString();
+        return this.key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public Long getRootOrgId() {
+        return rootOrgId;
+    }
+
+    public void setRootOrgId(Long rootOrgId) {
+        this.rootOrgId = rootOrgId;
+    }
+
+    public ExamingStatus getExamingStatus() {
+        return examingStatus;
+    }
+
+    public void setExamingStatus(ExamingStatus examingStatus) {
+        this.examingStatus = examingStatus;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public Long getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Long courseId) {
+        this.courseId = courseId;
+    }
+
+    public Long getExamStudentId() {
+        return examStudentId;
+    }
+
+    public void setExamStudentId(Long examStudentId) {
+        this.examStudentId = examStudentId;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    public String getPaperType() {
+        return paperType;
+    }
+
+    public void setPaperType(String paperType) {
+        this.paperType = paperType;
+    }
+
+    public Long getOrgId() {
+        return orgId;
+    }
+
+    public void setOrgId(Long orgId) {
+        this.orgId = orgId;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public Long getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Long startTime) {
+        this.startTime = startTime;
+    }
+
+    public Integer getFreezeTime() {
+        return freezeTime;
+    }
+
+    public void setFreezeTime(Integer freezeTime) {
+        this.freezeTime = freezeTime;
+    }
+
+    public String getExamType() {
+        return examType;
+    }
+
+    public void setExamType(String examType) {
+        this.examType = examType;
+    }
+
+    public Integer getExamReconnectTime() {
+        return examReconnectTime;
+    }
 
-	public Integer getFreezeTime() {
-		return freezeTime;
-	}
+    public void setExamReconnectTime(Integer examReconnectTime) {
+        this.examReconnectTime = examReconnectTime;
+    }
 
-	public void setFreezeTime(Integer freezeTime) {
-		this.freezeTime = freezeTime;
-	}
+    public Long getExamDuration() {
+        return examDuration;
+    }
 
-	public String getExamType() {
-		return examType;
-	}
+    public void setExamDuration(Long examDuration) {
+        this.examDuration = examDuration;
+    }
 
-	public void setExamType(String examType) {
-		this.examType = examType;
-	}
+    public String getCourseCode() {
+        return courseCode;
+    }
 
-	public Integer getExamReconnectTime() {
-		return examReconnectTime;
-	}
+    public void setCourseCode(String courseCode) {
+        this.courseCode = courseCode;
+    }
 
-	public void setExamReconnectTime(Integer examReconnectTime) {
-		this.examReconnectTime = examReconnectTime;
-	}
+    public Long getExamStageId() {
+        return examStageId;
+    }
 
-	public Long getExamDuration() {
-		return examDuration;
-	}
+    public void setExamStageId(Long examStageId) {
+        this.examStageId = examStageId;
+    }
 
-	public void setExamDuration(Long examDuration) {
-		this.examDuration = examDuration;
-	}
+    public Date getFixedSubmitTime() {
+        return fixedSubmitTime;
+    }
 
-	public String getCourseCode() {
-		return courseCode;
-	}
+    public void setFixedSubmitTime(Date fixedSubmitTime) {
+        this.fixedSubmitTime = fixedSubmitTime;
+    }
 
-	public void setCourseCode(String courseCode) {
-		this.courseCode = courseCode;
-	}
+    public boolean getTimingEnd() {
+        return timingEnd;
+    }
 
+    public void setTimingEnd(boolean timingEnd) {
+        this.timingEnd = timingEnd;
+    }
 }

+ 406 - 388
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java

@@ -1,405 +1,423 @@
 package cn.com.qmth.examcloud.support.filestorage;
 
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-import org.apache.commons.lang3.StringUtils;
-
 import cn.com.qmth.examcloud.commons.exception.StatusException;
 import cn.com.qmth.examcloud.commons.util.MD5;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
 import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
 import cn.com.qmth.examcloud.web.aliyun.AliyunSiteManager;
-import cn.com.qmth.examcloud.web.filestorage.FileStorage;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageHelper;
-import cn.com.qmth.examcloud.web.filestorage.FileStoragePathEnvInfo;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageType;
-import cn.com.qmth.examcloud.web.filestorage.YunHttpRequest;
-import cn.com.qmth.examcloud.web.filestorage.YunPathInfo;
+import cn.com.qmth.examcloud.web.filestorage.*;
 import cn.com.qmth.examcloud.web.support.SpringContextHolder;
 import cn.com.qmth.examcloud.web.upyun.UpyunSiteManager;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
 
 /**
  * 文件存储服务接口工具类
- * 
- * @author 86182
- *
  */
 public class FileStorageUtil {
 
-//	private static String fileStorageType = PropertyHolder.getString("$filestorage-type");
-
-//	private static String tempDir = PropertyHolder.getString("$filestorage-trans-tempdir");
-
-	private static String beanSuff = "FileStorage";
-
-	/**
-	 * 根据当前配置存储类型保存文件到存储服务器
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param in     文件流
-	 * @param md5    文件MD5 可为空
-	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
-	 */
-	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) {
-		FileStorageType fsType = getFileStorageType();
-		return saveFile(siteId, env, in, fsType, md5);
-	}
-
-	/**
-	 * 根据当前配置存储类型保存文件到存储服务器
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param file    文件
-	 * @param withMd5 是否校验MD5
-	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
-	 */
-	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, boolean withMd5) {
-		FileStorageType fsType = getFileStorageType();
-		String md5 = null;
-		if (withMd5) {
-			md5 = MD5.md5Hex(file);
-		}
-		return saveFile(siteId, env, file, fsType, md5);
-	}
-
-	/**
-	 * 根据当前配置存储类型保存文件到存储服务器
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param file   文件
-	 * @param md5    文件MD5 可为空
-	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
-	 */
-	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5) {
-		FileStorageType fsType = getFileStorageType();
-		return saveFile(siteId, env, file, fsType, md5);
-	}
-
-	/**
-	 * 根据存储类型保存文件到存储服务器
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param in     文件流
-	 * @param fsType 存储类型
-	 * @param md5    文件MD5 可为空
-	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
-	 */
-	private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in,
-			FileStorageType fsType, String md5) {
-
-		if (siteId == null) {
-			throw new StatusException("1000", "siteId是空");
-		}
-		if (in == null) {
-			throw new StatusException("1001", "文件流是空");
-		}
-		if (env == null) {
-			throw new StatusException("1002", "文件上传路径信息是空");
-		}
-		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
-		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
-		return fs.saveFile(siteId, env, in, md5);
-	}
-
-	/**
-	 * 根据存储类型保存文件到存储服务器
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param file   文件
-	 * @param fsType 存储类型
-	 * @param md5    文件MD5 可为空
-	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
-	 */
-	private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, FileStorageType fsType,
-			String md5) {
-		if (siteId == null) {
-			throw new StatusException("2000", "siteId是空");
-		}
-		if (file == null) {
-			throw new StatusException("2001", "文件是空");
-		}
-		if (env == null) {
-			throw new StatusException("2002", "文件上传路径信息是空");
-		}
-		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
-		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
-		return fs.saveFile(siteId, env, file, md5);
-	}
-
-	/**
-	 * 获取文件访问路径
-	 * 
-	 * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
-	 * @return 可直接访问的文件地址
-	 */
-	public static String realPath(String path) {
-
-		if (StringUtils.isBlank(path)) {
-			return null;
-		}
-		// 兼容处理老数据文件路径
-		path = diposeOldPath(path);
-		// 如果是全路径直接返回
-		if (path.startsWith("http") || path.startsWith("https")) {
-			return path;
-		}
-		// 根据路径头获取对应的处理类
-		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
-				FileStorage.class);
-		// 返回处理类处理结果
-		return fs.realPath(path);
-
-	}
-
-	/**
-	 * 将文件转换到指定存储服务上
-	 * 
-	 * @param path        数据库保存的路径
-	 *                    如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
-	 * @param transFsType 要转换的类型
-	 * @return 转换之后的路径
-	 */
-//	public static String transPath(String path, FileStorageType transFsType) {
-//		
-//		File file = null;
-//		if (StringUtils.isBlank(path)) {
-//			throw new StatusException("4001", "文件路径是空");
-//		}
-//		if (transFsType == null) {
-//			throw new StatusException("4002", "转换类型是空");
-//		}
-//		// 兼容处理老数据文件路径
-//		path = diposeOldPath(path);
-//		// 如果是全路径直接返回
-//		if (path.startsWith("http") || path.startsWith("https")) {
-//			return path;
-//		}
-//
-//		FileStorageType sourseType = FileStorageType.valueOf(FileStorageHelper.getHead(path));
-//		// 相同的类型直接返回
-//		if (sourseType.equals(transFsType)) {
-//			return path;
-//		}
-//		// 获取对应的处理类
-//		FileStorage fs = SpringContextHolder.getBean(transFsType.name().toLowerCase() + beanSuff, FileStorage.class);
-//		try {
-//			// 下载文件到本地
-//			String httppath = fs.realPath(FileStorageHelper.getPath(path));
-//			file = downFile(httppath);
-//			// 保存文件到指定存储服务器,并返回路径
-//			YunPathInfo ret = fs.saveFile(file, "from" + sourseType.name().toLowerCase() + "/" + FileStorageHelper.getPath(path));
-//			return ret.getRelativePath();
-//		} catch (IOException e) {
-//			throw new StatusException("4003", "下载文件出错 " + e.getMessage());
-//		} finally {
-//			if (file != null) {
-//				file.delete();
-//			}
-//		}
-//
-//	}
-
-//	private static File downFile(String url) throws IOException {
-//		String name = url.substring(url.lastIndexOf("/") + 1);
-//		File file = new File(tempDir + "/" + name);
-//		if (file.exists()) {
-//			file.delete();
-//		}
-//		file.createNewFile();
-//		saveUrlAs(url, file.getAbsolutePath());
-//		return file;
-//	}
-
-	/**
-	 * @param path 数据库存储的路径。路径必须是包含根路径的
-	 * @return 处理后的路径。补全路径头。
-	 */
-	private static String diposeOldPath(String path) {
-		if (StringUtils.isBlank(path)) {
-			return null;
-		}
-		// 如果是全路径直接返回
-		if (path.startsWith("http") || path.startsWith("https")) {
-			return path;
-		}
-		// 如果是半路径,转换成又拍云类型
-		if (path.indexOf(":") == -1) {
-			if (path.startsWith("/")) {
-				path = path.substring(1);
-			}
-			return FileStorageHelper.getUpyunSiteOne(path);
-		}
-		// 无需处理
-		return path;
-	}
-
-	/**
-	 * 将网络文件保存到本地
-	 *
-	 * @param fileUrl       网络文件URL
-	 * @param localFilePath 例如D:/123.txt
-	 * @throws IOException
-	 */
-	private static void saveUrlAs(String fileUrl, String localFilePath) throws IOException {
-		URL url = new URL(fileUrl);
-
-		HttpURLConnection connection;
-		connection = (HttpURLConnection) url.openConnection();
-
-		try (DataInputStream dataInputStream = new DataInputStream(connection.getInputStream());
-				FileOutputStream fileOutputStream = new FileOutputStream(localFilePath);
-				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();
-		} finally {
-			if (connection != null) {
-				connection.disconnect();
-			}
-		}
-	}
-
-	/**
-	 * 将网络文件保存到本地
-	 * 
-	 * @param fileUrl   网络文件URL
-	 * @param localFile 本地文件对象
-	 * @throws IOException
-	 */
-	public static void saveUrlAs(String fileUrl, File localFile) {
-		try {
-			saveUrlAs(fileUrl, localFile.getAbsolutePath());
-		} catch (IOException e) {
-			throw new StatusException("5001", "下载文件出错", e);
-		}
-	}
-
-	/**
-	 * 获取指定云存储的签名
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param md5
-	 * @return
-	 */
-	public static YunHttpRequest getSignature(FileStorageType fsType, String siteId, FileStoragePathEnvInfo env,
-			String md5) {
-		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
-		// 获取对应的处理类
-		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
-		return fs.getSignature(siteId, env, md5);
-	}
-
-	/**
-	 * 获取完整的不带域名的路径
-	 * 
-	 * @param prefixPath 路径前缀
-	 * @param sourcePath 数据库存储的路径
-	 * @return 处理之后的路径
-	 */
-	public static String getIntactPath(String prefixPath, String sourcePath) {
-		if (StringUtils.isBlank(sourcePath)) {
-			return null;
-		}
-		// 如果是全路径直接返回
-		if (sourcePath.startsWith("http") || sourcePath.startsWith("https")) {
-			return sourcePath;
-		}
-		// 如果是半路径则拼接
-		if (sourcePath.indexOf(":") == -1) {
-			if (sourcePath.startsWith("/")) {
-				sourcePath = sourcePath.substring(1);
-			}
-			if (prefixPath.endsWith("/")) {
-				prefixPath = prefixPath.substring(0, prefixPath.length());
-			}
-			return prefixPath + "/" + sourcePath;
-		}
-		// 新协议无需处理
-		return sourcePath;
-	}
-
-	/**
-	 * 删除云存储文件
-	 * 
-	 * @param path 全路径,包含根目录,含协议名
-	 */
-	public static void deleteFile(String path) {
-		if (StringUtils.isBlank(path)) {
-			return;
-		}
-		path = diposeOldPath(path);
-		// 如果是全路径直接返回
-		if (path.startsWith("http") || path.startsWith("https")) {
-			return;
-		}
-		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
-				FileStorage.class);
-		fs.deleteFile(path);
-	}
-
-	/**
-	 * 获取文件访问路径(备用域名地址)
-	 * 
-	 * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
-	 * @return 可直接访问的文件地址,如果没有配置备用地址,则返回主域名地址
-	 */
-	public static String realPathBackup(String path) {
-		if (StringUtils.isBlank(path)) {
-			return null;
-		}
-		// 兼容处理老数据文件路径
-		path = diposeOldPath(path);
-		// 如果是全路径直接返回
-		if (path.startsWith("http") || path.startsWith("https")) {
-			return path;
-		}
-		// 根据路径头获取对应的处理类
-		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
-				FileStorage.class);
-		// 返回处理类处理结果
-		return fs.realPathBackup(path);
-
-	}
-
-	public static void initYunClient() {
-		UpyunSiteManager.initClient();
-		AliyunSiteManager.initClient();
-	}
-
-	public static void initYunSite() {
-		UpyunSiteManager.initSite();
-		AliyunSiteManager.initSite();
-	}
-
-	public static FileStorageType getFileStorageType() {
-		SysPropertyCacheBean spc = CacheHelper.getSysProperty("filestorage.type");
-		if (spc == null || spc.getValue() == null) {
-			throw new StatusException("10001", "未配置文件存储服务类型");
-		}
-		String type = (String) spc.getValue();
-		try {
-			return FileStorageType.valueOf(type);
-		} catch (Exception e) {
-			throw new StatusException("10002", "配置文件存储服务类型错误");
-		}
-	}
+    // private static String fileStorageType = PropertyHolder.getString("$filestorage-type");
+
+    // private static String tempDir = PropertyHolder.getString("$filestorage-trans-tempdir");
+
+    private static String beanSuff = "FileStorage";
+
+    /**
+     * 根据当前配置存储类型保存文件到存储服务器
+     *
+     * @param siteId
+     * @param env
+     * @param in     文件流
+     * @param md5    文件MD5 可为空
+     * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+     */
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) {
+        FileStorageType fsType = getFileStorageType();
+        return saveFile(siteId, env, in, fsType, md5, false);
+    }
+
+    /**
+     * 根据当前配置存储类型保存文件到存储服务器
+     *
+     * @param siteId
+     * @param env
+     * @param file    文件
+     * @param withMd5 是否校验MD5
+     * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+     */
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, boolean withMd5) {
+        FileStorageType fsType = getFileStorageType();
+        String md5 = null;
+        if (withMd5) {
+            md5 = MD5.md5Hex(file);
+        }
+        return saveFile(siteId, env, file, fsType, md5, false);
+    }
+
+    /**
+     * 根据当前配置存储类型保存文件到存储服务器
+     *
+     * @param siteId
+     * @param env
+     * @param file   文件
+     * @param md5    文件MD5 可为空
+     * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+     */
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5) {
+        return saveFile(siteId, env, file, md5, false);
+    }
+
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN) {
+        FileStorageType fsType = getFileStorageType();
+        return saveFile(siteId, env, file, fsType, md5, refreshCDN);
+    }
+
+    /**
+     * 保存文件
+     *
+     * @param siteId
+     * @param env
+     * @param bytes
+     * @param refreshCDN 是否刷新CDN缓存
+     * @return
+     */
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, byte[] bytes, boolean refreshCDN) {
+        if (siteId == null) {
+            throw new StatusException("4001", "siteId不能为空");
+        }
+        if (bytes == null) {
+            throw new StatusException("4002", "文件不能为空");
+        }
+        if (env == null) {
+            throw new StatusException("4003", "文件上传路径不能为空");
+        }
+
+        FileStorageType fsType = getFileStorageType();
+        env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+        FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+
+        return fs.saveFile(siteId, env, bytes, refreshCDN);
+    }
+
+    /**
+     * 根据存储类型保存文件到存储服务器
+     *
+     * @param siteId
+     * @param env
+     * @param in     文件流
+     * @param fsType 存储类型
+     * @param md5    文件MD5 可为空
+     * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+     */
+    private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in,
+                                        FileStorageType fsType, String md5, boolean refreshCDN) {
+
+        if (siteId == null) {
+            throw new StatusException("1000", "siteId是空");
+        }
+        if (in == null) {
+            throw new StatusException("1001", "文件流是空");
+        }
+        if (env == null) {
+            throw new StatusException("1002", "文件上传路径信息是空");
+        }
+        env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+        FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+        return fs.saveFile(siteId, env, in, md5, refreshCDN);
+    }
+
+    /**
+     * 根据存储类型保存文件到存储服务器
+     *
+     * @param siteId
+     * @param env
+     * @param file       文件
+     * @param fsType     存储类型
+     * @param md5        文件MD5 可为空
+     * @param refreshCDN 是否刷新CDN缓存
+     * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+     */
+    private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, FileStorageType fsType,
+                                        String md5, boolean refreshCDN) {
+        if (siteId == null) {
+            throw new StatusException("2000", "siteId是空");
+        }
+        if (file == null) {
+            throw new StatusException("2001", "文件是空");
+        }
+        if (env == null) {
+            throw new StatusException("2002", "文件上传路径信息是空");
+        }
+        env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+        FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+        return fs.saveFile(siteId, env, file, md5, refreshCDN);
+    }
+
+    /**
+     * 获取文件访问路径
+     *
+     * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+     * @return 可直接访问的文件地址
+     */
+    public static String realPath(String path) {
+
+        if (StringUtils.isBlank(path)) {
+            return null;
+        }
+        // 兼容处理老数据文件路径
+        path = diposeOldPath(path);
+        // 如果是全路径直接返回
+        if (path.startsWith("http") || path.startsWith("https")) {
+            return path;
+        }
+        // 根据路径头获取对应的处理类
+        FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+                FileStorage.class);
+        // 返回处理类处理结果
+        return fs.realPath(path);
+
+    }
+
+    /**
+     * 将文件转换到指定存储服务上
+     *
+     * @param path        数据库保存的路径
+     *                    如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+     * @param transFsType 要转换的类型
+     * @return 转换之后的路径
+     */
+    /*public static String transPath(String path, FileStorageType transFsType) {
+
+        File file = null;
+        if (StringUtils.isBlank(path)) {
+            throw new StatusException("4001", "文件路径是空");
+        }
+        if (transFsType == null) {
+            throw new StatusException("4002", "转换类型是空");
+        }
+        // 兼容处理老数据文件路径
+        path = diposeOldPath(path);
+        // 如果是全路径直接返回
+        if (path.startsWith("http") || path.startsWith("https")) {
+            return path;
+        }
+
+        FileStorageType sourseType = FileStorageType.valueOf(FileStorageHelper.getHead(path));
+        // 相同的类型直接返回
+        if (sourseType.equals(transFsType)) {
+            return path;
+        }
+        // 获取对应的处理类
+        FileStorage fs = SpringContextHolder.getBean(transFsType.name().toLowerCase() + beanSuff, FileStorage.class);
+        try {
+            // 下载文件到本地
+            String httppath = fs.realPath(FileStorageHelper.getPath(path));
+            file = downFile(httppath);
+            // 保存文件到指定存储服务器,并返回路径
+            YunPathInfo ret = fs.saveFile(file, "from" + sourseType.name().toLowerCase() + "/" + FileStorageHelper.getPath(path));
+            return ret.getRelativePath();
+        } catch (IOException e) {
+            throw new StatusException("4003", "下载文件出错 " + e.getMessage());
+        } finally {
+            if (file != null) {
+                file.delete();
+            }
+        }
+
+    }*/
+
+    /*private static File downFile(String url) throws IOException {
+        String name = url.substring(url.lastIndexOf("/") + 1);
+        File file = new File(tempDir + "/" + name);
+        if (file.exists()) {
+            file.delete();
+        }
+        file.createNewFile();
+        saveUrlAs(url, file.getAbsolutePath());
+        return file;
+    }*/
+
+    /**
+     * @param path 数据库存储的路径。路径必须是包含根路径的
+     * @return 处理后的路径。补全路径头。
+     */
+    private static String diposeOldPath(String path) {
+        if (StringUtils.isBlank(path)) {
+            return null;
+        }
+        // 如果是全路径直接返回
+        if (path.startsWith("http") || path.startsWith("https")) {
+            return path;
+        }
+        // 如果是半路径,转换成又拍云类型
+        if (path.indexOf(":") == -1) {
+            if (path.startsWith("/")) {
+                path = path.substring(1);
+            }
+            return FileStorageHelper.getUpyunSiteOne(path);
+        }
+        // 无需处理
+        return path;
+    }
+
+    /**
+     * 将网络文件保存到本地
+     *
+     * @param fileUrl       网络文件URL
+     * @param localFilePath 例如D:/123.txt
+     * @throws IOException
+     */
+    private static void saveUrlAs(String fileUrl, String localFilePath) throws IOException {
+        URL url = new URL(fileUrl);
+
+        HttpURLConnection connection;
+        connection = (HttpURLConnection) url.openConnection();
+
+        try (DataInputStream dataInputStream = new DataInputStream(connection.getInputStream());
+             FileOutputStream fileOutputStream = new FileOutputStream(localFilePath);
+             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();
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 将网络文件保存到本地
+     *
+     * @param fileUrl   网络文件URL
+     * @param localFile 本地文件对象
+     * @throws IOException
+     */
+    public static void saveUrlAs(String fileUrl, File localFile) {
+        try {
+            saveUrlAs(fileUrl, localFile.getAbsolutePath());
+        } catch (IOException e) {
+            throw new StatusException("5001", "下载文件出错", e);
+        }
+    }
+
+    /**
+     * 获取指定云存储的签名
+     *
+     * @param siteId
+     * @param env
+     * @param md5
+     * @return
+     */
+    public static YunHttpRequest getSignature(FileStorageType fsType, String siteId, FileStoragePathEnvInfo env,
+                                              String md5) {
+        env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+        // 获取对应的处理类
+        FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+        return fs.getSignature(siteId, env, md5);
+    }
+
+    /**
+     * 获取完整的不带域名的路径
+     *
+     * @param prefixPath 路径前缀
+     * @param sourcePath 数据库存储的路径
+     * @return 处理之后的路径
+     */
+    public static String getIntactPath(String prefixPath, String sourcePath) {
+        if (StringUtils.isBlank(sourcePath)) {
+            return null;
+        }
+        // 如果是全路径直接返回
+        if (sourcePath.startsWith("http") || sourcePath.startsWith("https")) {
+            return sourcePath;
+        }
+        // 如果是半路径则拼接
+        if (sourcePath.indexOf(":") == -1) {
+            if (sourcePath.startsWith("/")) {
+                sourcePath = sourcePath.substring(1);
+            }
+            if (prefixPath.endsWith("/")) {
+                prefixPath = prefixPath.substring(0, prefixPath.length());
+            }
+            return prefixPath + "/" + sourcePath;
+        }
+        // 新协议无需处理
+        return sourcePath;
+    }
+
+    /**
+     * 删除云存储文件
+     *
+     * @param path 全路径,包含根目录,含协议名
+     */
+    public static void deleteFile(String path) {
+        if (StringUtils.isBlank(path)) {
+            return;
+        }
+        path = diposeOldPath(path);
+        // 如果是全路径直接返回
+        if (path.startsWith("http") || path.startsWith("https")) {
+            return;
+        }
+        FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+                FileStorage.class);
+        fs.deleteFile(path);
+    }
+
+    /**
+     * 获取文件访问路径(备用域名地址)
+     *
+     * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+     * @return 可直接访问的文件地址, 如果没有配置备用地址,则返回主域名地址
+     */
+    public static String realPathBackup(String path) {
+        if (StringUtils.isBlank(path)) {
+            return null;
+        }
+        // 兼容处理老数据文件路径
+        path = diposeOldPath(path);
+        // 如果是全路径直接返回
+        if (path.startsWith("http") || path.startsWith("https")) {
+            return path;
+        }
+        // 根据路径头获取对应的处理类
+        FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+                FileStorage.class);
+        // 返回处理类处理结果
+        return fs.realPathBackup(path);
+
+    }
+
+    public static void initYunClient() {
+        UpyunSiteManager.initClient();
+        AliyunSiteManager.initClient();
+    }
+
+    public static void initYunSite() {
+        UpyunSiteManager.initSite();
+        AliyunSiteManager.initSite();
+    }
+
+    public static FileStorageType getFileStorageType() {
+        SysPropertyCacheBean spc = CacheHelper.getSysProperty("filestorage.type");
+        if (spc == null || spc.getValue() == null) {
+            throw new StatusException("10001", "未配置文件存储服务类型");
+        }
+        String type = (String) spc.getValue();
+        try {
+            return FileStorageType.valueOf(type);
+        } catch (Exception e) {
+            throw new StatusException("10002", "配置文件存储服务类型错误");
+        }
+    }
 
 }

+ 144 - 92
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java

@@ -8,13 +8,15 @@ import com.mysql.cj.util.StringUtils;
 import org.apache.commons.lang.StringEscapeUtils;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * @Description 复合文本处理器类
@@ -25,6 +27,29 @@ import java.util.regex.Pattern;
 public class ComplexTextHandler implements RichTextHandler {
     private static Log logger = LogFactory.getLog(HtmlTextHandler.class);
 
+    private static final Map<String, String> TEXT_PARAM_MAP = new HashMap<>();
+
+    static {
+        TEXT_PARAM_MAP.put("b", "bold");
+        TEXT_PARAM_MAP.put("i", "italic");
+        TEXT_PARAM_MAP.put("strong", "bold");
+        TEXT_PARAM_MAP.put("em", "italic");
+        TEXT_PARAM_MAP.put("u", "underline");
+        TEXT_PARAM_MAP.put("sup", "sup");
+        TEXT_PARAM_MAP.put("sub", "sub");
+    }
+
+    private List<SectionBean> sections;
+
+    private SectionBean currentSection;
+
+    private Map<String, Object> currentTextParam;
+
+    public ComplexTextHandler() {
+        this.sections = new ArrayList<>();
+        this.currentTextParam = new HashMap<>();
+    }
+
     @Override
     public SectionCollectionBean handle(String richText) {
         if (logger.isDebugEnabled()) {
@@ -35,111 +60,138 @@ public class ComplexTextHandler implements RichTextHandler {
             return new SectionCollectionBean();
         }
 
-        SectionCollectionBean body = new SectionCollectionBean();
-        List<SectionBean> sections = new ArrayList<>();
-        // 得到小题题干或者答案行数
-        if (org.apache.commons.lang3.StringUtils.isBlank(richText)) {
-            return body;
+        SectionCollectionBean result = new SectionCollectionBean();
+//        List<SectionBean> sections = new ArrayList<>();
+
+        Element body = Jsoup.parseBodyFragment(org.apache.commons.lang.StringUtils.trimToEmpty(richText)).body();
+        switchSection();
+        for (Node child : body.childNodes()) {
+            parseNode(child);
         }
 
-        String[] questionRowStrings = richText.split("</p>");
-        for (int i = 0; i < questionRowStrings.length; i++) {
-            List<BlockBean> blocks = disposeQuestionBodyOrOption(questionRowStrings[i]);
-            if (blocks != null && blocks.size() > 0) {
-                SectionBean section = new SectionBean();
-                // 将小题题干拆分为Block集合
-                section.setBlocks(blocks);
-                sections.add(section);
+        result.setSections(sections);
+        return result;
+    }
+
+    private void parseNode(Node node) {
+        if (node instanceof TextNode) {
+            TextNode textNode = (TextNode) node;
+            parseText(textNode.text());
+        } else if (node instanceof Element) {
+            Element element = (Element) node;
+            onElementStart(element);
+            for (Node child : element.childNodes()) {
+                parseNode(child);
             }
+            onElementEnd(element);
         }
-        body.setSections(sections);
-        return body;
     }
 
+    private void parseText(String text) {
+        BlockBean block = new BlockBean(BlockType.text.name());
+        block.setValue(StringEscapeUtils.unescapeHtml(text));
+        block.getParam().putAll(currentTextParam);
+        addBlock(block);
+    }
 
-    private List<BlockBean> disposeQuestionBodyOrOption(String questionRow) {
-        List<BlockBean> blocks = new ArrayList<>();
-        // 去掉每行里面的<p>,<span>,</span>标签
-        questionRow = questionRow.replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("<span>", "")
-                .replaceAll("</span>", "").replaceAll("</a>", "");
-        String[] questionRowStrings = questionRow.split("<|/>|>");
-        for (int i = 0; i < questionRowStrings.length; i++) {
-            BlockBean block = new BlockBean(BlockType.text.name());
-            String rowStr = questionRowStrings[i];
-            // 判断是否有图片
-            if (rowStr.startsWith("img")) {
-                rowStr = "<" + rowStr + ">";
-                Map<String, Object> param = new HashMap<>();
-                // 需要继续做截取,取到Parma
-                block.setType("image");
-                // 获取图片的路径
-                List<String> strSrcList = getImg(rowStr, "src");
-                if (strSrcList.size() > 0) {
-                    String strSrc = strSrcList.get(0).replaceAll("src=\"", "").replaceAll("\"", "");
-                    block.setValue(strSrc);
-                }
-                // 获取图片的高度
-                List<String> strHeightList = getImg(rowStr, "height");
-                if (strHeightList.size() > 0) {
-                    String strHeight = strHeightList.get(0).replaceAll("height=\"", "").replaceAll("\"", "");
-                    param.put("height", strHeight);
-                }
-                // 获取图片的宽度
-                List<String> strWidthList = getImg(rowStr, "width");
-                if (strHeightList.size() > 0) {
-                    String strWidth = strWidthList.get(0).replaceAll("width=\"", "").replaceAll("\"", "");
-                    param.put("width", strWidth);
-                }
-                block.setParam(param);
-                blocks.add(block);
-            } else if (rowStr.startsWith("a") && rowStr.contains("id") && rowStr.contains("name")) { // 处理音频
-                rowStr = "<" + rowStr + ">";
-                block.setPlayTime(1);
-                block.setType("audio");
-                block.setValue(this.getAttrValue(rowStr, "url"));
-                blocks.add(block);
-            } else {
-                block.setType("text");
-                   if (org.apache.commons.lang3.StringUtils.isNotBlank(rowStr)) {
-                    block.setValue(StringEscapeUtils.unescapeHtml(rowStr));
-                    blocks.add(block);
-                }
-            }
+    private void parseImage(Element element) {
+        BlockBean block = new BlockBean(BlockType.image.name());
+
+        String src = element.attr("src");
+        block.setValue(src);
+
+        Map<String, Object> param = new HashMap<>();
+        String height = element.attr("height");
+        String width = element.attr("width");
+        param.put("width", height);
+        param.put("height", width);
+        block.getParam().putAll(param);
+
+        addBlock(block);
+    }
+
+    private void parseAudio(Element element) {
+        //todo 此处为题库特殊处理,待确认
+        String id = element.attr("id");
+        String name = element.attr("name");
+        if (org.apache.commons.lang.StringUtils.isNotEmpty(id) && org.apache.commons.lang.StringUtils.isNotEmpty(name)) {
+            BlockBean block = new BlockBean(BlockType.audio.name());
+            block.setPlayTime(1);
+            block.setValue(element.attr("url"));
+            addBlock(block);
         }
+    }
 
-        return blocks;
+    private void onElementStart(Element element) {
+        String name = element.nodeName().toLowerCase();
+        switch (name) {
+            case "div":
+            case "p":
+                switchSection();
+                break;
+            case "br":
+                finishSection();
+                break;
+            case "img":
+                parseImage(element);
+                break;
+            case "audio":// TODO
+            case "a":// TODO
+                parseAudio(element);
+                break;
+            default:
+                String style = TEXT_PARAM_MAP.get(name);
+                if (style != null) {
+                    currentTextParam.put(style, true);
+                }
+        }
     }
 
-    /**
-     * 获取图片里面的路径,长度,宽度
-     */
-    private List<String> getImg(String s, String str) {
-        String regex;
-        List<String> list = new ArrayList<>();
-        regex = str + "=\"(.*?)\"";
-        Pattern pa = Pattern.compile(regex, Pattern.DOTALL);
-        Matcher ma = pa.matcher(s);
-        while (ma.find()) {
-            list.add(ma.group());
+    private void onElementEnd(Element element) {
+        String name = element.nodeName().toLowerCase();
+        switch (name) {
+            case "div":
+            case "p":
+            case "span":
+                finishSection();
+                break;
+            case "img":// TODO
+            case "audio":// TODO
+            case "a":// TODO
+//                finishSection();
+                break;
+            default:
+                String style = TEXT_PARAM_MAP.get(name);
+                if (style != null) {
+                    currentTextParam.remove(style);
+                }
         }
-        return list;
     }
 
-    private String getAttrValue(String questionStr, String attrName) {
-        Pattern aPattern = Pattern.compile("a.*");
-        Matcher aMatcher = aPattern.matcher(questionStr);
-
-        if (aMatcher.find()) {
-            String idRegex = attrName + "=\".*?\"";
-            Pattern idPattern = Pattern.compile(idRegex);
-            Matcher idMatcher = idPattern.matcher(aMatcher.group());
-            if (idMatcher.find()) {
-                return idMatcher.group()
-                        .replaceAll(attrName + "=\"", "")
-                        .replaceAll("\"", "");
-            }
+    private void addBlock(BlockBean block) {
+        if (currentSection == null) {
+            switchSection();
         }
 
-        return "";
+        currentSection.getBlocks().add(block);
+    }
+
+    private void finishSection() {
+        currentSection = null;
+    }
+
+    private void switchSection() {
+        SectionBean section = new SectionBean();
+        sections.add(section);
+        currentSection = section;
+        currentTextParam.clear();
     }
+
+//    public static void main(String[] args) {
+//        String test = "<p><strong><em>杆件变形</em></strong>的基本形式有哪几种?何谓构件的强度,刚度和稳定性?</p>";
+//        test="<p><strong><em>杆件变形</em></strong><a id='a' name='1' url='http://1.mp3'></a>的基本形式有哪几种?<img src='http://www.baidu.com'/>何谓构件的强度,刚度和稳定性?</p>";
+//
+//        ComplexTextHandler handler = new ComplexTextHandler();
+//        System.out.println(JsonUtil.toJson(handler.handle(test)));
+//    }
 }

+ 35 - 8
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/helper/ExamCacheTransferHelper.java

@@ -8,12 +8,10 @@
 package cn.com.qmth.examcloud.support.helper;
 
 import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.api.commons.enums.SubmitType;
 import cn.com.qmth.examcloud.commons.util.StringUtil;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
-import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamPropertyCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.ExamSettingsCacheBean;
-import cn.com.qmth.examcloud.support.cache.bean.OrgPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.*;
 import cn.com.qmth.examcloud.support.enums.ExamProperties;
 import io.swagger.annotations.ApiOperation;
 import org.apache.commons.lang3.StringUtils;
@@ -32,11 +30,12 @@ public class ExamCacheTransferHelper {
     /**
      * 获取缓存的考试信息
      *
-     * @param examId    考试id
-     * @param studentId 学生id
+     * @param examId      考试id
+     * @param studentId   学生id
+     * @param examStageId 场次id
      * @return
      */
-    public static ExamSettingsCacheBean getCachedExam(Long examId, Long studentId) {
+    public static ExamSettingsCacheBean getCachedExam(Long examId, Long studentId, Long examStageId) {
         //默认取考试中的通用设置
         ExamSettingsCacheBean examBean = CacheHelper.getExamSettings(examId);
 
@@ -60,6 +59,15 @@ public class ExamCacheTransferHelper {
                     //初始化学生的特殊化设置
                     initStudentSpecialSettings(examId, studentId, examBean);
                     break;
+                case STAGE_BASED:
+                    //如果未选择场次,仍然按照考试的设置来处理
+                    if (null == examStageId) {
+                        break;
+                    }
+
+                    //初始化场次的特殊设置
+                    initExamStageSpecialSettings(examId, examStageId, examBean);
+                    break;
                 case COURSE_BASED:
                     //暂无此需求
                     throw new NotImplementedException();
@@ -112,6 +120,9 @@ public class ExamCacheTransferHelper {
                 case STUDENT_BASED:
                     specialExamProperty = CacheHelper.getExamStudentProperty(examId, studentId, propKey);
                     break;
+                case STAGE_BASED:
+                    specialExamProperty = examPropertyCacheBean;
+                    break;
                 case COURSE_BASED:
                     //暂无此需求
                     throw new NotImplementedException();
@@ -177,6 +188,22 @@ public class ExamCacheTransferHelper {
         return false;
     }
 
+    /**
+     * 初始化场次的特殊化设置
+     *
+     * @param examId
+     * @param examStageId
+     * @param examBean
+     */
+    private static void initExamStageSpecialSettings(Long examId, Long examStageId, ExamSettingsCacheBean examBean) {
+        ExamStageCacheBean examStageCacheBean = CacheHelper.getExamStage(examId, examStageId);
+        //此处不重新计算定点卷相关的考试时长
+        if (examStageCacheBean.getHasValue()) {
+            examBean.setBeginTime(examStageCacheBean.getStartTime());
+            examBean.setEndTime(examStageCacheBean.getEndTime());
+        }
+    }
+
     /**
      * 初始化学生的特殊化设置
      *
@@ -217,4 +244,4 @@ public class ExamCacheTransferHelper {
         }
     }
 
-}
+}

+ 10 - 13
examcloud-web/.gitignore

@@ -1,22 +1,19 @@
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.idea/
-*.iml
+*.class
 
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
-*test/
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-
+*.war
+*.ear

+ 56 - 59
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoHolder.java

@@ -1,90 +1,87 @@
 package cn.com.qmth.examcloud.web.actuator;
 
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
-
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import org.apache.commons.lang3.RandomUtils;
 import org.springframework.boot.ApplicationArguments;
 import org.springframework.boot.ApplicationRunner;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-import cn.com.qmth.examcloud.commons.util.Util;
-import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
 
 @Component
 @Order(1000)
 public class ApiStatusInfoHolder implements ApplicationRunner {
 
-	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(ApiStatusInfoHolder.class);
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(ApiStatusInfoHolder.class);
+
+    private static Map<String, ApiStatusInfo> apiStatusInfoMap = Maps.newHashMap();
 
-	private static Map<String, ApiStatusInfo> apiStatusInfoMap = Maps.newHashMap();
+    private static List<ApiStatusInfo> apiStatusInfoList = Lists.newArrayList();
 
-	private static List<ApiStatusInfo> apiStatusInfoList = Lists.newArrayList();
+    private static AtomicBoolean running = new AtomicBoolean(false);
 
-	private static AtomicBoolean running = new AtomicBoolean(false);
+    public static ApiStatusInfo getApiStatusInfo(String mapping) {
+        return apiStatusInfoMap.get(mapping);
+    }
 
-	public static ApiStatusInfo getApiStatusInfo(String mapping) {
-		return apiStatusInfoMap.get(mapping);
-	}
+    public static List<ApiStatusInfo> getApiStatusInfoList() {
+        if (running.get()) {
+            return apiStatusInfoList;
+        }
 
-	public static List<ApiStatusInfo> getApiStatusInfoList() {
-		if (running.get()) {
-			return apiStatusInfoList;
-		}
-		refresh();
-		return apiStatusInfoList;
-	}
+        refresh();
+        return apiStatusInfoList;
+    }
 
-	@Override
-	public void run(ApplicationArguments args) throws Exception {
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                while (true) {
+                    Util.sleep(5);
+                    refresh();
+                }
+            }
 
-		new Thread(new Runnable() {
-			@Override
-			public void run() {
-				while (true) {
-					Util.sleep(5);
-					refresh();
-				}
-			}
+        }).start();
+    }
 
-		}).start();
-	}
+    private static synchronized void refresh() {
+        long trace = 10000 + RandomUtils.nextLong(0, 89999);
+        // LOG.debug(trace + " [ApiStatus] refresh...");
 
-	private static synchronized void refresh() {
-		long trace = 10000 + RandomUtils.nextLong(0, 89999);
-		LOG.debug(trace + " [ApiStatus]. refresh...");
-		running.set(true);
-		try {
-			ReportorHolder.getApiDataReportor().report();
+        running.set(true);
 
-			Util.sleep(TimeUnit.MILLISECONDS, 500);
+        try {
+            ReportorHolder.getApiDataReportor().report();
 
-			ReportInfo reportInfo = ReportorHolder.getApiDataReportor().getReportInfo();
+            Util.sleep(TimeUnit.MILLISECONDS, 500);
 
-			ApiStatusInfoCollector apiStatusInfoCollector = SpringContextHolder
-					.getBean(ApiStatusInfoCollector.class);
-			apiStatusInfoList = apiStatusInfoCollector.collect(reportInfo);
+            ReportInfo reportInfo = ReportorHolder.getApiDataReportor().getReportInfo();
 
-			Map<String, ApiStatusInfo> newApiInfoMap = apiStatusInfoList.stream()
-					.collect(Collectors.toMap(ApiStatusInfo::getMapping, info -> info));
+            ApiStatusInfoCollector apiStatusInfoCollector = SpringContextHolder.getBean(ApiStatusInfoCollector.class);
+            apiStatusInfoList = apiStatusInfoCollector.collect(reportInfo);
 
-			apiStatusInfoMap = newApiInfoMap;
+            Map<String, ApiStatusInfo> newApiInfoMap = apiStatusInfoList.stream().collect(Collectors.toMap(ApiStatusInfo::getMapping, info -> info));
 
-		} catch (Exception e) {
-			LOG.debug(trace + " [ApiStatus]. fail to refresh API status", e);
-		}
+            apiStatusInfoMap = newApiInfoMap;
+        } catch (Exception e) {
+            LOG.warn(trace + " [ApiStatus] refresh fail...", e);
+        }
 
-		running.set(false);
-		LOG.debug(trace + " [ApiStatus]. OVER");
-	}
+        running.set(false);
+        // LOG.debug(trace + " [ApiStatus] refresh ok...");
+    }
 
 }

+ 1 - 1
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSiteManager.java

@@ -105,7 +105,7 @@ public class AliyunSiteManager {
 		AliyunSite aliyunSite = SITE_HOLDERS.get(siteId);
 
 		if (null == aliyunSite) {
-			throw new StatusException("20006", "aliyunSite is null");
+			throw new StatusException("20006", "aliyun.xml siteId not exist.");
 		}
 		return aliyunSite;
 	}

+ 78 - 52
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java

@@ -4,56 +4,82 @@ import java.io.File;
 import java.io.InputStream;
 
 public interface FileStorage {
-	
-	/**保存文件到存储服务
-	 * @param siteId 
-	 * @param env
-	 * @param file 文件流
-	 * @param md5 文件MD5 可为空
-	 * @return 返回路径
-	 */
-	public YunPathInfo saveFile(String siteId,FileStoragePathEnvInfo env,InputStream in,String md5);
-	
-	/**保存文件到存储服务
-	 * @param siteId 
-	 * @param env
-	 * @param file 文件
-	 * @param md5 文件MD5 可为空
-	 * @return 返回路径
-	 */
-	
-	public YunPathInfo saveFile(String siteId,FileStoragePathEnvInfo env,File file,String md5);
-	
-	/**获取可直接访问的文件地址
-	 * @param path 全路径,包含根目录,含协议名
-	 * @return 返回可直接访问的地址
-	 */
-	public String realPath(String path);
-	
-	/**获取可直接访问的文件地址(备用域名地址)
-	 * @param path 全路径,包含根目录,含协议名
-	 * @return 返回可直接访问的地址,如果没有配置备用地址,则返回主域名地址
-	 */
-	public String realPathBackup(String path);
-	
-	/**保存文件到存储服务,siteId为1,转换接口用
-	 * @param file 文件
-	 * @param path 全路径,含协议名
-	 * @return 返回路径
-	 */
-	public YunPathInfo saveFile(File file, String path);
-	
-	/**获取云存储签名
-	 * @param siteId
-	 * @param env
-	 * @param md5
-	 * @return
-	 */
-	public YunHttpRequest  getSignature(String siteId,FileStoragePathEnvInfo env,String md5);
-	
-	/**删除文件
-	 * @param path 全路径,含协议名
-	 */
-	public void deleteFile(String path) ;
-	
+
+    /**
+     * 保存文件到存储服务
+     *
+     * @param siteId
+     * @param env
+     * @param in         文件流
+     * @param md5        文件MD5 可为空
+     * @param refreshCDN 是否刷新CDN缓存
+     * @return 返回路径
+     */
+    YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN);
+
+    /**
+     * 保存文件到存储服务
+     *
+     * @param siteId
+     * @param env
+     * @param file       文件
+     * @param md5        文件MD5 可为空
+     * @param refreshCDN 是否刷新CDN缓存
+     * @return 返回路径
+     */
+    YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN);
+
+    /**
+     * 保存文件到存储服务
+     *
+     * @param siteId
+     * @param env
+     * @param bytes
+     * @param refreshCDN 是否刷新CDN缓存
+     * @return
+     */
+    YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, byte[] bytes, boolean refreshCDN);
+
+    /**
+     * 获取可直接访问的文件地址
+     *
+     * @param path 全路径,包含根目录,含协议名
+     * @return 返回可直接访问的地址
+     */
+    String realPath(String path);
+
+    /**
+     * 获取可直接访问的文件地址(备用域名地址)
+     *
+     * @param path 全路径,包含根目录,含协议名
+     * @return 返回可直接访问的地址, 如果没有配置备用地址,则返回主域名地址
+     */
+    String realPathBackup(String path);
+
+    /**
+     * 保存文件到存储服务,siteId为1,转换接口用
+     *
+     * @param file 文件
+     * @param path 全路径,含协议名
+     * @return 返回路径
+     */
+    YunPathInfo saveFile(File file, String path);
+
+    /**
+     * 获取云存储签名
+     *
+     * @param siteId
+     * @param env
+     * @param md5
+     * @return
+     */
+    YunHttpRequest getSignature(String siteId, FileStoragePathEnvInfo env, String md5);
+
+    /**
+     * 删除文件
+     *
+     * @param path 全路径,含协议名
+     */
+    void deleteFile(String path);
+
 }

+ 482 - 486
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java

@@ -1,499 +1,495 @@
 package cn.com.qmth.examcloud.web.filestorage.impl;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Base64;
-import java.util.Date;
-import java.util.Map;
-
-import org.apache.commons.codec.DecoderException;
-import org.apache.commons.codec.binary.Hex;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.time.DateUtils;
-import org.springframework.stereotype.Service;
-
-import com.aliyun.oss.OSS;
-import com.aliyun.oss.model.ObjectMetadata;
-import com.google.common.collect.Maps;
-
 import cn.com.qmth.examcloud.commons.exception.StatusException;
 import cn.com.qmth.examcloud.commons.util.DateUtil;
 import cn.com.qmth.examcloud.commons.util.FreeMarkerUtil;
 import cn.com.qmth.examcloud.web.aliyun.AliYunAccount;
 import cn.com.qmth.examcloud.web.aliyun.AliyunSite;
 import cn.com.qmth.examcloud.web.aliyun.AliyunSiteManager;
-import cn.com.qmth.examcloud.web.filestorage.FileStorage;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageHelper;
-import cn.com.qmth.examcloud.web.filestorage.FileStoragePathEnvInfo;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageType;
-import cn.com.qmth.examcloud.web.filestorage.YunHttpRequest;
-import cn.com.qmth.examcloud.web.filestorage.YunPathInfo;
+import cn.com.qmth.examcloud.web.filestorage.*;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.google.common.collect.Maps;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Map;
 
 @Service(value = "aliyunFileStorage")
 public class AliyunFileStorageImpl implements FileStorage {
-//	private ExamCloudLog log = ExamCloudLogFactory.getLog(this.getClass());
-
-	// 文件最大大小(byte)
-	private static int maxFileSize = 100 * 1024 * 1024;
-
-	@Override
-	public YunPathInfo saveFile(File file, String path) {
-		String siteId = "transPath";
-		FileStoragePathEnvInfo env = new FileStoragePathEnvInfo();
-		env.setRelativePath(path);
-		return saveFile(siteId, env, file, null);
-	}
-
-	@Override
-	public String realPath(String path) {
-		String yunId = FileStorageHelper.getYunId(path);
-		String urlpath=FileStorageHelper.getPath(path);
-		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
-		return FileStorageHelper.getUrl(ac.getDomain(), urlpath);
-	}
-
-	/**
-	 * 表单上传
-	 * 
-	 * @param siteId
-	 * @param env
-	 * @param file
-	 * @param md5
-	 * @return 不带域名的完整路径
-	 * @throws IOException
-	 */
-//	private String postObject(String siteId, FileStoragePathEnvInfo env, File file, String md5) throws IOException {
-//		AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
-//		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
-//		String ossEndpoint = ac.getOssEndpoint();
-//		String bucket = ac.getBucket();
-//		String accessKeyId = ac.getAccessKeyId();
-//		String accessKeySecret = ac.getAccessKeySecret();
-//		// 阿里云文件路径
-//		String path = FreeMarkerUtil.process(as.getPath(), env);
-//		if (path.startsWith("/")) {
-//			path = path.substring(1);
-//		}
-//
-//		String filepath = file.getAbsolutePath();
-//		String filename = path.substring(path.lastIndexOf("/") + 1);
-//		String urlStr = null; // 提交表单的URL为bucket域名
-//		if(ossEndpoint.startsWith("https://")) {
-//			urlStr = ossEndpoint.replace("https://", "https://" + bucket + "."); 
-//		}else if(ossEndpoint.startsWith("http://")) {
-//			urlStr = ossEndpoint.replace("http://", "http://" + bucket + "."); 
-//		}
-//
-//		LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
-//		// key
-//		textMap.put("key", path);
-//		// Content-Disposition
-//		textMap.put("Content-Disposition", "attachment;filename=" + filename);
-//		// OSSAccessKeyId
-//		textMap.put("OSSAccessKeyId", accessKeyId);
-//		// policy
-//		String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, "
-//				+ maxFileSize + "]]}";
-//		String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
-//		textMap.put("policy", encodePolicy);
-//		// Signature
-//		String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
-//				encodePolicy);
-//		textMap.put("Signature", signaturecom);
-//
-//		Map<String, String> fileMap = new HashMap<String, String>();
-//		fileMap.put("file", filepath);
-//
-//		String ret = formUpload(urlStr, textMap, fileMap);
-//		log.info("oss上传:" + ret);
-//		return path;
-//	}
-
-//	@SuppressWarnings("rawtypes")
-//	private static String formUpload(String urlStr, Map<String, String> textMap, Map<String, String> fileMap)
-//			throws IOException {
-//		String res = "";
-//		HttpURLConnection conn = null;
-//		String BOUNDARY = "9431149156168";
-//		try {
-//			URL url = new URL(urlStr);
-//			conn = (HttpURLConnection) url.openConnection();
-//			conn.setConnectTimeout(5000);
-//			conn.setReadTimeout(30000);
-//			conn.setDoOutput(true);
-//			conn.setDoInput(true);
-//			conn.setRequestMethod("POST");
-//			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
-//			conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
-//
-//			OutputStream out = new DataOutputStream(conn.getOutputStream());
-//			// text
-//			if (textMap != null) {
-//				StringBuffer strBuf = new StringBuffer();
-//				Iterator iter = textMap.entrySet().iterator();
-//				int i = 0;
-//				while (iter.hasNext()) {
-//					Map.Entry entry = (Map.Entry) iter.next();
-//					String inputName = (String) entry.getKey();
-//					String inputValue = (String) entry.getValue();
-//					if (inputValue == null) {
-//						continue;
-//					}
-//					if (i == 0) {
-//						strBuf.append("--").append(BOUNDARY).append("\r\n");
-//						strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
-//						strBuf.append(inputValue);
-//					} else {
-//						strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
-//						strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
-//
-//						strBuf.append(inputValue);
-//					}
-//
-//					i++;
-//				}
-//				out.write(strBuf.toString().getBytes());
-//			}
-//
-//			// file
-//			if (fileMap != null) {
-//				Iterator iter = fileMap.entrySet().iterator();
-//				while (iter.hasNext()) {
-//					Map.Entry entry = (Map.Entry) iter.next();
-//					String inputName = (String) entry.getKey();
-//					String inputValue = (String) entry.getValue();
-//					if (inputValue == null) {
-//						continue;
-//					}
-//					File file = new File(inputValue);
-//					String filename = file.getName();
-//					String contentType = new MimetypesFileTypeMap().getContentType(file);
-//					if (contentType == null || contentType.equals("")) {
-//						contentType = "application/octet-stream";
-//					}
-//
-//					StringBuffer strBuf = new StringBuffer();
-//					strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
-//					strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + filename
-//							+ "\"\r\n");
-//					strBuf.append("Content-Type: " + contentType + "\r\n\r\n");
-//
-//					out.write(strBuf.toString().getBytes());
-//
-//					DataInputStream in = new DataInputStream(new FileInputStream(file));
-//					int bytes = 0;
-//					byte[] bufferOut = new byte[1024];
-//					while ((bytes = in.read(bufferOut)) != -1) {
-//						out.write(bufferOut, 0, bytes);
-//					}
-//					in.close();
-//				}
-//				StringBuffer strBuf = new StringBuffer();
-//				out.write(strBuf.toString().getBytes());
-//			}
-//
-//			byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
-//			out.write(endData);
-//			out.flush();
-//			out.close();
-//
-//			// 读取返回数据
-//			StringBuffer strBuf = new StringBuffer();
-//			BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-//			String line = null;
-//			while ((line = reader.readLine()) != null) {
-//				strBuf.append(line).append("\n");
-//			}
-//			res = strBuf.toString();
-//			reader.close();
-//			reader = null;
-//		} finally {
-//			if (conn != null) {
-//				conn.disconnect();
-//				conn = null;
-//			}
-//		}
-//		return res;
-//	}
-
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file,String md5) {
-		InputStream in=null;
-		try {
-			in = new FileInputStream(file);
-			return saveFile(siteId, env, in, md5);
-		} catch (FileNotFoundException e) {
-			throw new StatusException("5001", "上传出错", e);
-		} finally {
-			if(in!=null) {
-				try {
-					in.close();
-				} catch (IOException e) {
-				}
-			}
-		}
-	}
-
-	@Override
-	public YunHttpRequest getSignature(String siteId, FileStoragePathEnvInfo env, String md5) {
-		AliyunSite site = AliyunSiteManager.getAliyunSite(siteId);
-		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(site.getAliyunId());
-		String ossEndpoint = ac.getOssEndpoint();
-		String bucket = ac.getBucket();
-		String accessKeyId = ac.getAccessKeyId();
-		String accessKeySecret = ac.getAccessKeySecret();
-		String urlStr = null; // 提交表单的URL为bucket域名
-		if(ossEndpoint.startsWith("https://")) {
-			urlStr = ossEndpoint.replace("https://", "https://" + bucket + "."); 
-		}else if(ossEndpoint.startsWith("http://")) {
-			urlStr = ossEndpoint.replace("http://", "http://" + bucket + "."); 
-		}
-		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
-
-		String path = FreeMarkerUtil.process(site.getPath(), env);
-		if (path.startsWith("/")) {
-			path = path.substring(1);
-		}
-
-		String accessUrl = FileStorageHelper.getUrl(ac.getDomain(), path);
-
-		Map<String, String> params = Maps.newHashMap();
-		// key
-		params.put("key", path);
-		// OSSAccessKeyId
-		params.put("OSSAccessKeyId", accessKeyId);
-		// policy
-		Date expiration = DateUtils.addSeconds(new Date(), 600);
-		String expirationStr = DateUtil.format(expiration, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
-		String policy = "{\"expiration\": \"" + expirationStr + "\",\"conditions\": [[\"content-length-range\", 0, "
-				+ maxFileSize + "]]}";
-		String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
-		params.put("policy", encodePolicy);
-		// Signature
-		String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
-				encodePolicy);
-		params.put("Signature", signaturecom);
-
-		YunHttpRequest request = new YunHttpRequest();
-		request.setAccessUrl(accessUrl);
-		request.setFormParams(params);
-		request.setFormUrl(urlStr);
-		return request;
-	}
-
-	
-//	@SuppressWarnings("unused")
-//	private  String postObjectByInputStream(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) throws IOException {
-//		AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
-//		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
-//		String ossEndpoint = ac.getOssEndpoint();
-//		String bucket = ac.getBucket();
-//		String accessKeyId = ac.getAccessKeyId();
-//		String accessKeySecret = ac.getAccessKeySecret();
-//		// 阿里云文件路径
-//		String path = FreeMarkerUtil.process(as.getPath(), env);
-//		if (path.startsWith("/")) {
-//			path = path.substring(1);
-//		}
-//
-//		String filename = path.substring(path.lastIndexOf("/") + 1);
-//		String urlStr = null; // 提交表单的URL为bucket域名
-//		if(ossEndpoint.startsWith("https://")) {
-//			urlStr = ossEndpoint.replace("https://", "https://" + bucket + "."); 
-//		}else if(ossEndpoint.startsWith("http://")) {
-//			urlStr = ossEndpoint.replace("http://", "http://" + bucket + "."); 
-//		}
-//
-//		LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
-//		// key
-//		textMap.put("key", path);
-//		// Content-Disposition
-//		textMap.put("Content-Disposition", "attachment;filename=" + filename);
-//		// OSSAccessKeyId
-//		textMap.put("OSSAccessKeyId", accessKeyId);
-//		// policy
-//		String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, "
-//				+ maxFileSize + "]]}";
-//		String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
-//		textMap.put("policy", encodePolicy);
-//		// Signature
-//		String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
-//				encodePolicy);
-//		textMap.put("Signature", signaturecom);
-//
-//
-//		String ret = formUploadByInputStream(urlStr, textMap, in,filename);
-//		log.info("oss上传:" + ret);
-//		return path;
-//	}
-
-//	@SuppressWarnings("rawtypes")
-//	private static String formUploadByInputStream(String urlStr, Map<String, String> textMap, InputStream filein,String fileName )
-//			throws IOException {
-//		String res = "";
-//		HttpURLConnection conn = null;
-//		String BOUNDARY = "9431149156168";
-//		try {
-//			URL url = new URL(urlStr);
-//			conn = (HttpURLConnection) url.openConnection();
-//			conn.setConnectTimeout(5000);
-//			conn.setReadTimeout(30000);
-//			conn.setDoOutput(true);
-//			conn.setDoInput(true);
-//			conn.setRequestMethod("POST");
-//			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
-//			conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
-//
-//			OutputStream out = new DataOutputStream(conn.getOutputStream());
-//			// text
-//			if (textMap != null) {
-//				StringBuffer strBuf = new StringBuffer();
-//				Iterator iter = textMap.entrySet().iterator();
-//				int i = 0;
-//				while (iter.hasNext()) {
-//					Map.Entry entry = (Map.Entry) iter.next();
-//					String inputName = (String) entry.getKey();
-//					String inputValue = (String) entry.getValue();
-//					if (inputValue == null) {
-//						continue;
-//					}
-//					if (i == 0) {
-//						strBuf.append("--").append(BOUNDARY).append("\r\n");
-//						strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
-//						strBuf.append(inputValue);
-//					} else {
-//						strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
-//						strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
-//
-//						strBuf.append(inputValue);
-//					}
-//
-//					i++;
-//				}
-//				out.write(strBuf.toString().getBytes());
-//			}
-//
-//			// file
-//
-//					StringBuffer strBufFile = new StringBuffer();
-//					strBufFile.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
-//					strBufFile.append("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName
-//							+ "\"\r\n");
-//					strBufFile.append("Content-Type: application/octet-stream\r\n\r\n");
-//
-//					out.write(strBufFile.toString().getBytes());
-//
-//					DataInputStream in = new DataInputStream(filein);
-//					int bytes = 0;
-//					byte[] bufferOut = new byte[1024];
-//					while ((bytes = in.read(bufferOut)) != -1) {
-//						out.write(bufferOut, 0, bytes);
-//					}
-//					in.close();
-//				StringBuffer strBufTag = new StringBuffer();
-//				out.write(strBufTag.toString().getBytes());
-//
-//			byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
-//			out.write(endData);
-//			out.flush();
-//			out.close();
-//
-//			// 读取返回数据
-//			StringBuffer strBuf = new StringBuffer();
-//			BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-//			String line = null;
-//			while ((line = reader.readLine()) != null) {
-//				strBuf.append(line).append("\n");
-//			}
-//			res = strBuf.toString();
-//			reader.close();
-//			reader = null;
-//		} finally {
-//			if (conn != null) {
-//				conn.disconnect();
-//				conn = null;
-//			}
-//		}
-//		return res;
-//	}
-
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) {
-		try {
-			String relativePath = uploadObject(siteId, env, in, md5);
-			AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
-			AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
-			String url=FileStorageHelper.getUrl(ac.getDomain(), relativePath);
-			YunPathInfo pi=new YunPathInfo(url, getTreatyPath(as.getAliyunId(), relativePath));
-			return pi;
-		} catch (Exception e) {
-			throw new StatusException("6001", "上传出错", e);
-		}
-	}
-
-	@Override
-	public String realPathBackup(String path) {
-		String yunId = FileStorageHelper.getYunId(path);
-		String urlpath=FileStorageHelper.getPath(path);
-		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
-		String bk=ac.getDomainBackup();
-		if(StringUtils.isNotBlank(bk)) {
-			return FileStorageHelper.getUrl(bk, urlpath);
-		}
-		return FileStorageHelper.getUrl(ac.getDomain(), urlpath);
-	}
-	
-	@Override
-	public void deleteFile(String path) {
-		//无删除权限
-//		String yunId=FileStorageHelper.getYunId(path);
-//		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
-//		String bucket = ac.getBucket();
-//		OSS oss=AliyunSiteManager.getAliYunClientByAliyunId(yunId);
-//		// 阿里云文件路径
-//		String urlpath = FileStorageHelper.getPath(path);
-//		if (urlpath.startsWith("/")) {
-//			urlpath = urlpath.substring(1);
-//		}
-//		oss.deleteObject(bucket, urlpath);
-		
-	}
-	private String uploadObject(String siteId, FileStoragePathEnvInfo env,InputStream in, String md5) throws IOException, DecoderException {
-		AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
-		AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
-		String bucket = ac.getBucket();
-		OSS oss=AliyunSiteManager.getAliYunClientBySiteId(siteId);
-		// 阿里云文件路径
-		String path = FreeMarkerUtil.process(as.getPath(), env);
-		path=disposePath(path);
-		if(StringUtils.isNotBlank(md5)) {
-		    md5=Base64.getEncoder().encodeToString(Hex.decodeHex(md5));
-		    ObjectMetadata meta = new ObjectMetadata();
+
+    // private ExamCloudLog log = ExamCloudLogFactory.getLog(this.getClass());
+
+    // 文件最大大小(byte)
+    private static int maxFileSize = 100 * 1024 * 1024;
+
+    @Override
+    public YunPathInfo saveFile(File file, String path) {
+        String siteId = "transPath";
+        FileStoragePathEnvInfo env = new FileStoragePathEnvInfo();
+        env.setRelativePath(path);
+        return saveFile(siteId, env, file, null, false);
+    }
+
+    @Override
+    public String realPath(String path) {
+        String yunId = FileStorageHelper.getYunId(path);
+        String urlpath = FileStorageHelper.getPath(path);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
+        return FileStorageHelper.getUrl(ac.getDomain(), urlpath);
+    }
+
+    /**
+     * 表单上传
+     *
+     * @param siteId
+     * @param env
+     * @param file
+     * @param md5
+     * @return 不带域名的完整路径
+     * @throws IOException
+     */
+    /*private String postObject(String siteId, FileStoragePathEnvInfo env, File file, String md5) throws IOException {
+        AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
+        String ossEndpoint = ac.getOssEndpoint();
+        String bucket = ac.getBucket();
+        String accessKeyId = ac.getAccessKeyId();
+        String accessKeySecret = ac.getAccessKeySecret();
+        // 阿里云文件路径
+        String path = FreeMarkerUtil.process(as.getPath(), env);
+        if (path.startsWith("/")) {
+            path = path.substring(1);
+        }
+
+        String filepath = file.getAbsolutePath();
+        String filename = path.substring(path.lastIndexOf("/") + 1);
+        String urlStr = null; // 提交表单的URL为bucket域名
+        if (ossEndpoint.startsWith("https://")) {
+            urlStr = ossEndpoint.replace("https://", "https://" + bucket + ".");
+        } else if (ossEndpoint.startsWith("http://")) {
+            urlStr = ossEndpoint.replace("http://", "http://" + bucket + ".");
+        }
+
+        LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
+        // key
+        textMap.put("key", path);
+        // Content-Disposition
+        textMap.put("Content-Disposition", "attachment;filename=" + filename);
+        // OSSAccessKeyId
+        textMap.put("OSSAccessKeyId", accessKeyId);
+        // policy
+        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, "
+                + maxFileSize + "]]}";
+        String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
+        textMap.put("policy", encodePolicy);
+        // Signature
+        String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
+                encodePolicy);
+        textMap.put("Signature", signaturecom);
+
+        Map<String, String> fileMap = new HashMap<String, String>();
+        fileMap.put("file", filepath);
+
+        String ret = formUpload(urlStr, textMap, fileMap);
+        log.info("oss上传:" + ret);
+        return path;
+    }*/
+
+    /*@SuppressWarnings("rawtypes")
+    private static String formUpload(String urlStr, Map<String, String> textMap, Map<String, String> fileMap)
+            throws IOException {
+        String res = "";
+        HttpURLConnection conn = null;
+        String BOUNDARY = "9431149156168";
+        try {
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(5000);
+            conn.setReadTimeout(30000);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
+            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+
+            OutputStream out = new DataOutputStream(conn.getOutputStream());
+            // text
+            if (textMap != null) {
+                StringBuffer strBuf = new StringBuffer();
+                Iterator iter = textMap.entrySet().iterator();
+                int i = 0;
+                while (iter.hasNext()) {
+                    Map.Entry entry = (Map.Entry) iter.next();
+                    String inputName = (String) entry.getKey();
+                    String inputValue = (String) entry.getValue();
+                    if (inputValue == null) {
+                        continue;
+                    }
+                    if (i == 0) {
+                        strBuf.append("--").append(BOUNDARY).append("\r\n");
+                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
+                        strBuf.append(inputValue);
+                    } else {
+                        strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
+                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
+
+                        strBuf.append(inputValue);
+                    }
+
+                    i++;
+                }
+                out.write(strBuf.toString().getBytes());
+            }
+
+            // file
+            if (fileMap != null) {
+                Iterator iter = fileMap.entrySet().iterator();
+                while (iter.hasNext()) {
+                    Map.Entry entry = (Map.Entry) iter.next();
+                    String inputName = (String) entry.getKey();
+                    String inputValue = (String) entry.getValue();
+                    if (inputValue == null) {
+                        continue;
+                    }
+                    File file = new File(inputValue);
+                    String filename = file.getName();
+                    String contentType = new MimetypesFileTypeMap().getContentType(file);
+                    if (contentType == null || contentType.equals("")) {
+                        contentType = "application/octet-stream";
+                    }
+
+                    StringBuffer strBuf = new StringBuffer();
+                    strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
+                    strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + filename
+                            + "\"\r\n");
+                    strBuf.append("Content-Type: " + contentType + "\r\n\r\n");
+
+                    out.write(strBuf.toString().getBytes());
+
+                    DataInputStream in = new DataInputStream(new FileInputStream(file));
+                    int bytes = 0;
+                    byte[] bufferOut = new byte[1024];
+                    while ((bytes = in.read(bufferOut)) != -1) {
+                        out.write(bufferOut, 0, bytes);
+                    }
+                    in.close();
+                }
+                StringBuffer strBuf = new StringBuffer();
+                out.write(strBuf.toString().getBytes());
+            }
+
+            byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
+            out.write(endData);
+            out.flush();
+            out.close();
+
+            // 读取返回数据
+            StringBuffer strBuf = new StringBuffer();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                strBuf.append(line).append("\n");
+            }
+            res = strBuf.toString();
+            reader.close();
+            reader = null;
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+                conn = null;
+            }
+        }
+        return res;
+    }*/
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN) {
+        try (InputStream in = new FileInputStream(file);) {
+            return saveFile(siteId, env, in, md5, refreshCDN);
+        } catch (Exception e) {
+            throw new StatusException("5001", "上传出错", e);
+        }
+    }
+
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, byte[] bytes, boolean refreshCDN) {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(bytes);) {
+            return saveFile(siteId, env, in, null, refreshCDN);
+        } catch (Exception e) {
+            throw new StatusException("5002", "上传出错", e);
+        }
+    }
+
+    @Override
+    public YunHttpRequest getSignature(String siteId, FileStoragePathEnvInfo env, String md5) {
+        AliyunSite site = AliyunSiteManager.getAliyunSite(siteId);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(site.getAliyunId());
+        String ossEndpoint = ac.getOssEndpoint();
+        String bucket = ac.getBucket();
+        String accessKeyId = ac.getAccessKeyId();
+        String accessKeySecret = ac.getAccessKeySecret();
+        String urlStr = null; // 提交表单的URL为bucket域名
+        if (ossEndpoint.startsWith("https://")) {
+            urlStr = ossEndpoint.replace("https://", "https://" + bucket + ".");
+        } else if (ossEndpoint.startsWith("http://")) {
+            urlStr = ossEndpoint.replace("http://", "http://" + bucket + ".");
+        }
+        env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+
+        String path = FreeMarkerUtil.process(site.getPath(), env);
+        if (path.startsWith("/")) {
+            path = path.substring(1);
+        }
+
+        String accessUrl = FileStorageHelper.getUrl(ac.getDomain(), path);
+
+        Map<String, String> params = Maps.newHashMap();
+        // key
+        params.put("key", path);
+        // OSSAccessKeyId
+        params.put("OSSAccessKeyId", accessKeyId);
+        // policy
+        Date expiration = DateUtils.addSeconds(new Date(), 600);
+        String expirationStr = DateUtil.format(expiration, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+        String policy = "{\"expiration\": \"" + expirationStr + "\",\"conditions\": [[\"content-length-range\", 0, "
+                + maxFileSize + "]]}";
+        String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
+        params.put("policy", encodePolicy);
+        // Signature
+        String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
+                encodePolicy);
+        params.put("Signature", signaturecom);
+
+        YunHttpRequest request = new YunHttpRequest();
+        request.setAccessUrl(accessUrl);
+        request.setFormParams(params);
+        request.setFormUrl(urlStr);
+        return request;
+    }
+
+    /*@SuppressWarnings("unused")
+    private String postObjectByInputStream(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) throws IOException {
+        AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
+        String ossEndpoint = ac.getOssEndpoint();
+        String bucket = ac.getBucket();
+        String accessKeyId = ac.getAccessKeyId();
+        String accessKeySecret = ac.getAccessKeySecret();
+        // 阿里云文件路径
+        String path = FreeMarkerUtil.process(as.getPath(), env);
+        if (path.startsWith("/")) {
+            path = path.substring(1);
+        }
+
+        String filename = path.substring(path.lastIndexOf("/") + 1);
+        String urlStr = null; // 提交表单的URL为bucket域名
+        if (ossEndpoint.startsWith("https://")) {
+            urlStr = ossEndpoint.replace("https://", "https://" + bucket + ".");
+        } else if (ossEndpoint.startsWith("http://")) {
+            urlStr = ossEndpoint.replace("http://", "http://" + bucket + ".");
+        }
+
+        LinkedHashMap<String, String> textMap = new LinkedHashMap<String, String>();
+        // key
+        textMap.put("key", path);
+        // Content-Disposition
+        textMap.put("Content-Disposition", "attachment;filename=" + filename);
+        // OSSAccessKeyId
+        textMap.put("OSSAccessKeyId", accessKeyId);
+        // policy
+        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, "
+                + maxFileSize + "]]}";
+        String encodePolicy = java.util.Base64.getEncoder().encodeToString(policy.getBytes());
+        textMap.put("policy", encodePolicy);
+        // Signature
+        String signaturecom = com.aliyun.oss.common.auth.ServiceSignature.create().computeSignature(accessKeySecret,
+                encodePolicy);
+        textMap.put("Signature", signaturecom);
+
+
+        String ret = formUploadByInputStream(urlStr, textMap, in, filename);
+        log.info("oss上传:" + ret);
+        return path;
+    }*/
+
+    /*@SuppressWarnings("rawtypes")
+    private static String formUploadByInputStream(String urlStr, Map<String, String> textMap, InputStream filein, String fileName)
+            throws IOException {
+        String res = "";
+        HttpURLConnection conn = null;
+        String BOUNDARY = "9431149156168";
+        try {
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(5000);
+            conn.setReadTimeout(30000);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
+            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+
+            OutputStream out = new DataOutputStream(conn.getOutputStream());
+            // text
+            if (textMap != null) {
+                StringBuffer strBuf = new StringBuffer();
+                Iterator iter = textMap.entrySet().iterator();
+                int i = 0;
+                while (iter.hasNext()) {
+                    Map.Entry entry = (Map.Entry) iter.next();
+                    String inputName = (String) entry.getKey();
+                    String inputValue = (String) entry.getValue();
+                    if (inputValue == null) {
+                        continue;
+                    }
+                    if (i == 0) {
+                        strBuf.append("--").append(BOUNDARY).append("\r\n");
+                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
+                        strBuf.append(inputValue);
+                    } else {
+                        strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
+                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
+
+                        strBuf.append(inputValue);
+                    }
+
+                    i++;
+                }
+                out.write(strBuf.toString().getBytes());
+            }
+
+            // file
+
+            StringBuffer strBufFile = new StringBuffer();
+            strBufFile.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
+            strBufFile.append("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName
+                    + "\"\r\n");
+            strBufFile.append("Content-Type: application/octet-stream\r\n\r\n");
+
+            out.write(strBufFile.toString().getBytes());
+
+            DataInputStream in = new DataInputStream(filein);
+            int bytes = 0;
+            byte[] bufferOut = new byte[1024];
+            while ((bytes = in.read(bufferOut)) != -1) {
+                out.write(bufferOut, 0, bytes);
+            }
+            in.close();
+            StringBuffer strBufTag = new StringBuffer();
+            out.write(strBufTag.toString().getBytes());
+
+            byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
+            out.write(endData);
+            out.flush();
+            out.close();
+
+            // 读取返回数据
+            StringBuffer strBuf = new StringBuffer();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                strBuf.append(line).append("\n");
+            }
+            res = strBuf.toString();
+            reader.close();
+            reader = null;
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+                conn = null;
+            }
+        }
+        return res;
+    }*/
+
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN) {
+        try {
+            String relativePath = uploadObject(siteId, env, in, md5);
+            AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
+            AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
+            String url = FileStorageHelper.getUrl(ac.getDomain(), relativePath);
+            YunPathInfo result = new YunPathInfo(url, getTreatyPath(as.getAliyunId(), relativePath));
+
+            if (refreshCDN) {
+                AliyunRefreshCdn.refreshCDN(ac.getAccessKeyId(), ac.getAccessKeySecret(), result.getUrl());
+            }
+
+            return result;
+        } catch (Exception e) {
+            throw new StatusException("6001", "上传出错", e);
+        }
+    }
+
+    @Override
+    public String realPathBackup(String path) {
+        String yunId = FileStorageHelper.getYunId(path);
+        String urlpath = FileStorageHelper.getPath(path);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
+        String bk = ac.getDomainBackup();
+        if (StringUtils.isNotBlank(bk)) {
+            return FileStorageHelper.getUrl(bk, urlpath);
+        }
+        return FileStorageHelper.getUrl(ac.getDomain(), urlpath);
+    }
+
+    @Override
+    public void deleteFile(String path) {
+        // 无删除权限
+        /*String yunId = FileStorageHelper.getYunId(path);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(yunId);
+        String bucket = ac.getBucket();
+        OSS oss = AliyunSiteManager.getAliYunClientByAliyunId(yunId);
+        // 阿里云文件路径
+        String urlpath = FileStorageHelper.getPath(path);
+        if (urlpath.startsWith("/")) {
+            urlpath = urlpath.substring(1);
+        }
+        oss.deleteObject(bucket, urlpath);*/
+    }
+
+    private String uploadObject(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) throws IOException, DecoderException {
+        AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
+        AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
+        String bucket = ac.getBucket();
+        OSS oss = AliyunSiteManager.getAliYunClientBySiteId(siteId);
+        // 阿里云文件路径
+        String path = FreeMarkerUtil.process(as.getPath(), env);
+        path = disposePath(path);
+        if (StringUtils.isNotBlank(md5)) {
+            md5 = Base64.getEncoder().encodeToString(Hex.decodeHex(md5));
+            ObjectMetadata meta = new ObjectMetadata();
             meta.setContentMD5(md5);
-            oss.putObject(bucket, path, in,meta);
-		}else {
-		    oss.putObject(bucket, path, in);
-		}
-
-		return path;
-	}
-	private String getTreatyPath(String yunId,String relativePath) {
-		if(relativePath.startsWith("/")) {
-			relativePath=relativePath.substring(1);
-		}
-		String path=FileStorageType.ALIYUN.name().toLowerCase()+"-"+yunId+"://"+relativePath;
-		return path;
-	}
-	private  String disposePath(String path) {
-		for(;;) {
-			if(path.startsWith("/")) {
-				path=path.substring(1);
-			}else {
-				return path;
-			}
-		}
-	}
+            oss.putObject(bucket, path, in, meta);
+        } else {
+            oss.putObject(bucket, path, in);
+        }
+
+        return path;
+    }
+
+    private String getTreatyPath(String yunId, String relativePath) {
+        if (relativePath.startsWith("/")) {
+            relativePath = relativePath.substring(1);
+        }
+        String path = FileStorageType.ALIYUN.name().toLowerCase() + "-" + yunId + "://" + relativePath;
+        return path;
+    }
+
+    private String disposePath(String path) {
+        for (; ; ) {
+            if (path.startsWith("/")) {
+                path = path.substring(1);
+            } else {
+                return path;
+            }
+        }
+    }
+
 }

+ 55 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunRefreshCdn.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2020 "https://github.com/deason" All Rights Reserved.
+ * Created by Deason on 2020-08-31 10:24:15
+ */
+
+package cn.com.qmth.examcloud.web.filestorage.impl;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.cdn.model.v20180510.RefreshObjectCachesRequest;
+import com.aliyuncs.cdn.model.v20180510.RefreshObjectCachesResponse;
+import com.aliyuncs.profile.DefaultProfile;
+
+/**
+ * 阿里云 - 刷新文件的CDN缓存
+ */
+public class AliyunRefreshCdn {
+
+    private static final ExamCloudLog log = ExamCloudLogFactory.getLog("INTERFACE_LOGGER");
+
+    /**
+     * 刷新文件的CDN缓存:
+     * 1、同一个账号 每天最多可提交2000条URL刷新和100个目录刷新
+     * 2、每次请求最多只能提交1000条URL刷新,多个URL之间需要用换行符 \n 分割
+     * 3、每秒最多50次请求
+     */
+    public static void refreshCDN(String accessKeyId, String accessKeySecret, String fileUrls) {
+        DefaultProfile profile = DefaultProfile.getProfile("oss-cn-shenzhen", accessKeyId, accessKeySecret);
+        IAcsClient client = new DefaultAcsClient(profile);
+
+        RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
+        request.setObjectPath(fileUrls);
+        request.setObjectType("file");
+
+        try {
+            RefreshObjectCachesResponse response = client.getAcsResponse(request);
+
+            log.info("refreshCDN fileUrl = " + fileUrls);
+            if (log.isDebugEnabled()) {
+                log.debug(JsonUtil.toJson(response));
+            }
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /*public static void main(String[] args) {
+        String fileUrl = "http://xxx.com/xxx/xxx.xxx";
+        refreshCDN("xxx", "xxx", fileUrl);
+    }*/
+
+}

+ 98 - 109
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java

@@ -1,127 +1,116 @@
 package cn.com.qmth.examcloud.web.filestorage.impl;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.web.filestorage.*;
+import cn.com.qmth.examcloud.web.upyun.*;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import cn.com.qmth.examcloud.commons.exception.StatusException;
-import cn.com.qmth.examcloud.web.filestorage.FileStorage;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageHelper;
-import cn.com.qmth.examcloud.web.filestorage.FileStoragePathEnvInfo;
-import cn.com.qmth.examcloud.web.filestorage.FileStorageType;
-import cn.com.qmth.examcloud.web.filestorage.YunHttpRequest;
-import cn.com.qmth.examcloud.web.filestorage.YunPathInfo;
-import cn.com.qmth.examcloud.web.upyun.UpYunClient;
-import cn.com.qmth.examcloud.web.upyun.UpYunPathInfo;
-import cn.com.qmth.examcloud.web.upyun.UpyunPathEnvironmentInfo;
-import cn.com.qmth.examcloud.web.upyun.UpyunService;
-import cn.com.qmth.examcloud.web.upyun.UpyunSite;
-import cn.com.qmth.examcloud.web.upyun.UpyunSiteManager;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
 
 @Service(value = "upyunFileStorage")
 public class UpyunFileStorageImpl implements FileStorage {
-	@Autowired
-	private UpyunService upyunService;
 
-	@Override
-	public YunPathInfo saveFile(File file, String path) {
-		String siteId="transPath";
-		FileStoragePathEnvInfo env = new FileStoragePathEnvInfo();
-		env.setRelativePath(path);
-		return saveFile(siteId, env, file, null);
-	}
+    @Autowired
+    private UpyunService upyunService;
+
+    @Override
+    public YunPathInfo saveFile(File file, String path) {
+        String siteId = "transPath";
+        FileStoragePathEnvInfo env = new FileStoragePathEnvInfo();
+        env.setRelativePath(path);
+        return saveFile(siteId, env, file, null, false);
+    }
+
+    @Override
+    public String realPath(String path) {
+        String upyunId = FileStorageHelper.getYunId(path);
+        String urlpath = FileStorageHelper.getPath(path);
+        UpYunClient c = UpyunSiteManager.getUpYunClientByUpyunId(upyunId);
+        return FileStorageHelper.getUrl(c.getDomain(), urlpath);
+    }
+
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN) {
+        try (InputStream in = new FileInputStream(file);) {
+            return saveFile(siteId, env, in, md5, refreshCDN);
+        } catch (Exception e) {
+            throw new StatusException("1001", "上传出错", e);
+        }
+    }
+
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, byte[] bytes, boolean refreshCDN) {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(bytes);) {
+            return saveFile(siteId, env, in, null, refreshCDN);
+        } catch (Exception e) {
+            throw new StatusException("1001", "上传出错", e);
+        }
+    }
 
-	@Override
-	public String realPath(String path) {
-		String upyunId=FileStorageHelper.getYunId(path);
-		String urlpath=FileStorageHelper.getPath(path);
-		UpYunClient c=UpyunSiteManager.getUpYunClientByUpyunId(upyunId);
-		return FileStorageHelper.getUrl(c.getDomain(), urlpath);
-	}
+    private UpyunPathEnvironmentInfo of(FileStoragePathEnvInfo env) {
+        UpyunPathEnvironmentInfo ret = new UpyunPathEnvironmentInfo();
+        ret.setExt1(env.getExt1());
+        ret.setExt2(env.getExt2());
+        ret.setExt3(env.getExt3());
+        ret.setExt4(env.getExt4());
+        ret.setExt5(env.getExt5());
+        ret.setFileSuffix(env.getFileSuffix());
+        ret.setRelativePath(env.getRelativePath());
+        ret.setRootOrgDomain(env.getRootOrgDomain());
+        ret.setRootOrgId(env.getRootOrgId());
+        ret.setTimeMillis(env.getTimeMillis());
+        ret.setUserId(env.getUserId());
+        return ret;
+    }
 
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file,String md5) {
-		InputStream in=null;
-		try {
-			in = new FileInputStream(file);
-			return saveFile(siteId, env, in, md5);
-		} catch (FileNotFoundException e) {
-			throw new StatusException("1001", "上传出错", e);
-		} finally {
-			if(in!=null) {
-				try {
-					in.close();
-				} catch (IOException e) {
-				}
-			}
-		}
-	}
-	
-	private UpyunPathEnvironmentInfo of(FileStoragePathEnvInfo env) {
-		UpyunPathEnvironmentInfo ret=new UpyunPathEnvironmentInfo();
-		ret.setExt1(env.getExt1());
-		ret.setExt2(env.getExt2());
-		ret.setExt3(env.getExt3());
-		ret.setExt4(env.getExt4());
-		ret.setExt5(env.getExt5());
-		ret.setFileSuffix(env.getFileSuffix());
-		ret.setRelativePath(env.getRelativePath());
-		ret.setRootOrgDomain(env.getRootOrgDomain());
-		ret.setRootOrgId(env.getRootOrgId());
-		ret.setTimeMillis(env.getTimeMillis());
-		ret.setUserId(env.getUserId());
-		return ret;
-	}
+    @Override
+    public YunHttpRequest getSignature(String siteId, FileStoragePathEnvInfo env, String md5) {
+        YunHttpRequest req = upyunService.buildUpYunHttpRequest(siteId, of(env), md5);
+        return req;
+    }
 
-	@Override
-	public YunHttpRequest getSignature(String siteId, FileStoragePathEnvInfo env, String md5) {
-		YunHttpRequest req=upyunService.buildUpYunHttpRequest(siteId, of(env), md5);
-		return req;
-	}
+    @Override
+    public void deleteFile(String path) {
+        // 无删除权限
+        // String upyunId = FileStorageHelper.getYunId(path);
+        // String urlpath = FileStorageHelper.getPath(path);
+        // upyunService.deleteByUpyunId(upyunId, urlpath);
+    }
 
-	@Override
-	public void deleteFile(String path) {
-		//无删除权限
-//		String upyunId=FileStorageHelper.getYunId(path);
-//		String urlpath=FileStorageHelper.getPath(path);
-//		upyunService.deleteByUpyunId(upyunId, urlpath);
-	}
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN) {
+        UpYunPathInfo pathInfo = upyunService.writeFile(siteId, of(env), in, md5);
+        UpyunSite site = UpyunSiteManager.getUpyunSite(siteId);
+        String relativePath = getTreatyPath(site.getUpyunId(), pathInfo.getRelativePath());
+        UpYunClient c = UpyunSiteManager.getUpYunClientByUpyunId(site.getUpyunId());
+        String url = FileStorageHelper.getUrl(c.getDomain(), pathInfo.getRelativePath());
+        YunPathInfo pi = new YunPathInfo(url, relativePath);
+        return pi;
+    }
 
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) {
-		UpYunPathInfo pathInfo =upyunService.writeFile(siteId, of(env), in, md5);
-		UpyunSite site=UpyunSiteManager.getUpyunSite(siteId);
-		String relativePath=getTreatyPath(site.getUpyunId(),pathInfo.getRelativePath());
-		UpYunClient c=UpyunSiteManager.getUpYunClientByUpyunId(site.getUpyunId());
-		String url=FileStorageHelper.getUrl(c.getDomain(), pathInfo.getRelativePath());
-		YunPathInfo pi=new YunPathInfo(url, relativePath);
-		return pi;
-	}
+    @Override
+    public String realPathBackup(String path) {
+        String upyunId = FileStorageHelper.getYunId(path);
+        String urlpath = FileStorageHelper.getPath(path);
+        UpYunClient c = UpyunSiteManager.getUpYunClientByUpyunId(upyunId);
+        String bk = c.getDomainBackup();
+        if (StringUtils.isNotBlank(bk)) {
+            return FileStorageHelper.getUrl(bk, urlpath);
+        }
+        return FileStorageHelper.getUrl(c.getDomain(), urlpath);
+    }
 
-	@Override
-	public String realPathBackup(String path) {
-		String upyunId=FileStorageHelper.getYunId(path);
-		String urlpath=FileStorageHelper.getPath(path);
-		UpYunClient c=UpyunSiteManager.getUpYunClientByUpyunId(upyunId);
-		String bk=c.getDomainBackup();
-		if(StringUtils.isNotBlank(bk)) {
-			return FileStorageHelper.getUrl(bk, urlpath);
-		}
-		return FileStorageHelper.getUrl(c.getDomain(), urlpath);
-	}
-	
-	private String getTreatyPath(String yunId,String relativePath) {
-		if(relativePath.startsWith("/")) {
-			relativePath=relativePath.substring(1);
-		}
-		String path=FileStorageType.UPYUN.name().toLowerCase()+"-"+yunId+"://"+relativePath;
-		return path;
-	}
+    private String getTreatyPath(String yunId, String relativePath) {
+        if (relativePath.startsWith("/")) {
+            relativePath = relativePath.substring(1);
+        }
+        String path = FileStorageType.UPYUN.name().toLowerCase() + "-" + yunId + "://" + relativePath;
+        return path;
+    }
 
 }

+ 3 - 1
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/jpa/JpaEntity.java

@@ -13,6 +13,7 @@ import org.springframework.data.annotation.LastModifiedDate;
 import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
 import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import org.springframework.data.jpa.repository.Modifying;
 
 /**
  * JPA 数据库实体父类
@@ -23,6 +24,7 @@ import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
  */
 @MappedSuperclass
 @EntityListeners(AuditingEntityListener.class)
+
 public abstract class JpaEntity implements JsonSerializable {
 
 	private static final long serialVersionUID = 1561273677157469875L;
@@ -59,4 +61,4 @@ public abstract class JpaEntity implements JsonSerializable {
 		this.creationTime = creationTime;
 	}
 
-}
+}

+ 4 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/ControllerSupport.java

@@ -37,6 +37,10 @@ public abstract class ControllerSupport {
      */
     protected ExamCloudLog log = ExamCloudLogFactory.getLog(this.getClass());
 
+    protected String getIp(HttpServletRequest request) {
+        return IpUtil.getRemoteIp(request);
+    }
+
     /**
      * 获取接入用户
      *

+ 66 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/IpUtil.java

@@ -0,0 +1,66 @@
+package cn.com.qmth.examcloud.web.support;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @Description 获取ip工具类
+ * @Author lideyin
+ * @Date 2020/9/2 17:55
+ * @Version 1.0
+ */
+public class IpUtil {
+
+    /**
+     * 获取过程ip(默认不包含代理ip)
+     *
+     * @param request
+     * @return
+     */
+    public static String getRemoteIp(HttpServletRequest request) {
+        return getRemoteIp(request, true);
+    }
+
+    /**
+     * excludeProxyIp
+     *
+     * @param request
+     * @param excludeProxyIp 是否排除代理ip
+     * @return
+     */
+    public static String getRemoteIp(HttpServletRequest request, boolean excludeProxyIp) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("x-real-ip");
+        }
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+
+        if (excludeProxyIp) {
+            //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+            if (ip != null && ip.length() > 15) { //"***.***.***.***".length() = 15
+                if (ip.indexOf(",") > 0) {
+                    ip = ip.substring(0, ip.indexOf(","));
+                }
+            }
+        }
+        return ip;
+    }
+
+}

+ 12 - 5
examcloud-ws-starter/.gitignore

@@ -1,12 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
-*.iml
-*.jar
 target/
 .idea/
-*test/
-logs/
-classes/
+*.iml
+
+# Log Files
+*.log
+*.class
 
 
+# Package Files #
+*.jar
+*.war
+*.ear