Browse Source

。。。

wangwei 6 years ago
commit
298a7f230e
51 changed files with 4331 additions and 0 deletions
  1. 22 0
      .gitignore
  2. 93 0
      pom.xml
  3. 16 0
      src/main/java/cn/com/qmth/examcloud/commons/web/RedisKeys.java
  4. 62 0
      src/main/java/cn/com/qmth/examcloud/commons/web/boot/BootSecurityManager.java
  5. 334 0
      src/main/java/cn/com/qmth/examcloud/commons/web/boot/ExamCloudApp.java
  6. 30 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/BaseRequest.java
  7. 44 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/BaseResponse.java
  8. 12 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/CloudService.java
  9. 13 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/ExchangeBean.java
  10. 16 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/JsonSerializable.java
  11. 14 0
      src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/OuterService.java
  12. 59 0
      src/main/java/cn/com/qmth/examcloud/commons/web/config/SystemConfig.java
  13. 44 0
      src/main/java/cn/com/qmth/examcloud/commons/web/enums/BooleanSelect.java
  14. 30 0
      src/main/java/cn/com/qmth/examcloud/commons/web/enums/DataExecutionStatus.java
  15. 136 0
      src/main/java/cn/com/qmth/examcloud/commons/web/helpers/page/PageInfo.java
  16. 95 0
      src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/EleTreeNode.java
  17. 27 0
      src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/TreeNode.java
  18. 122 0
      src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/TreeUtil.java
  19. 59 0
      src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/ZtreeNode.java
  20. 228 0
      src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/FirstInterceptor.java
  21. 21 0
      src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/Seqlock.java
  22. 124 0
      src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/SeqlockInterceptor.java
  23. 21 0
      src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/SessionSeqlock.java
  24. 58 0
      src/main/java/cn/com/qmth/examcloud/commons/web/jpa/JpaEntity.java
  25. 89 0
      src/main/java/cn/com/qmth/examcloud/commons/web/redis/RedisClient.java
  26. 75 0
      src/main/java/cn/com/qmth/examcloud/commons/web/redis/RedisClientImpl.java
  27. 70 0
      src/main/java/cn/com/qmth/examcloud/commons/web/reports/BaseReport.java
  28. 33 0
      src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportFileFilter.java
  29. 131 0
      src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportLoggerFactory.java
  30. 36 0
      src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportsCollector.java
  31. 139 0
      src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportsController.java
  32. 237 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/RequestPermissionInterceptor.java
  33. 78 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/SpringCloudInterceptor.java
  34. 64 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/Role.java
  35. 149 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/User.java
  36. 59 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/UserType.java
  37. 49 0
      src/main/java/cn/com/qmth/examcloud/commons/web/security/enums/RoleMeta.java
  38. 189 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/CloudClientSupport.java
  39. 201 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/ControllerAspect.java
  40. 333 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/ControllerSupport.java
  41. 163 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/CustomExceptionHandler.java
  42. 28 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/CustomResponseErrorHandler.java
  43. 42 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/RemoteProcedureCallTester.java
  44. 69 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/ResponseStatus.java
  45. 39 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/RuntimeController.java
  46. 110 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/ServletUtil.java
  47. 73 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/SpringContextHolder.java
  48. 94 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/StatusResponse.java
  49. 96 0
      src/main/java/cn/com/qmth/examcloud/commons/web/support/SystemController.java
  50. 3 0
      src/main/resources/app.properties
  51. 2 0
      src/main/resources/redis.properties

+ 22 - 0
.gitignore

@@ -0,0 +1,22 @@
+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
+
+.project
+.classpath
+.settings
+target/
+.idea/
+*.iml
+*test/
+# Package Files #
+*.jar
+

+ 93 - 0
pom.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-parent</artifactId>
+		<version>2019</version>
+	</parent>
+	<artifactId>examcloud-web</artifactId>
+	<version>2019-SNAPSHOT</version>
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.com.qmth.examcloud</groupId>
+			<artifactId>examcloud-commons</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-jpa</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-validator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-feign</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-autoconfigure</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-feign</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-eureka</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger2</artifactId>
+			<version>2.9.2</version>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger-ui</artifactId>
+			<version>2.9.2</version>
+		</dependency>
+	</dependencies>
+
+</project>

+ 16 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/RedisKeys.java

@@ -0,0 +1,16 @@
+package cn.com.qmth.examcloud.commons.web;
+
+/**
+ * redis keys
+ *
+ * @author WANGWEI
+ * @date 2018年6月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RedisKeys {
+	/**
+	 * session过期时长
+	 */
+	public static final String SESSION_TIMEOUT = "$_SESSION_TIMEOUT";
+
+}

+ 62 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/boot/BootSecurityManager.java

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.commons.web.boot;
+
+/**
+ * 安全处理器
+ *
+ * @author WANGWEI
+ * @date 2018年12月5日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+
+public class BootSecurityManager {
+
+	private static BootSecurityManager mgr;
+
+	private static final Object LOCK = new Object();
+
+	private String secretKey;
+
+	private String active;
+
+	/**
+	 * 构造函数
+	 */
+	private BootSecurityManager() {
+	}
+
+	/**
+	 * 获取单例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static BootSecurityManager getInstance() {
+		if (null == mgr) {
+			synchronized (LOCK) {
+				if (null == mgr) {
+					mgr = new BootSecurityManager();
+				}
+				return mgr;
+			}
+		} else {
+			return mgr;
+		}
+	}
+
+	public String getSecretKey() {
+		return secretKey;
+	}
+
+	public void setSecretKey(String secretKey) {
+		this.secretKey = secretKey;
+	}
+
+	public String getActive() {
+		return active;
+	}
+
+	public void setActive(String active) {
+		this.active = active;
+	}
+
+}

+ 334 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/boot/ExamCloudApp.java

@@ -0,0 +1,334 @@
+package cn.com.qmth.examcloud.commons.web.boot;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.base.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.base.util.HttpClientPool;
+import cn.com.qmth.examcloud.commons.base.util.HttpClientUtil;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.base.util.SecurityUtil;
+import cn.com.qmth.examcloud.commons.base.util.Util;
+
+/**
+ * 启动器<br>
+ * 
+ * 配置文件优先级:参数>配置中心>本地配置
+ *
+ * @author WANGWEI
+ * @date 2018年12月3日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamCloudApp {
+
+	/**
+	 * 启动配置中心
+	 *
+	 * @author WANGWEI
+	 * @param source
+	 * @param password
+	 * @return
+	 */
+	public static ConfigurableApplicationContext runConfigServer(Object source, String... args) {
+		Properties props = new Properties();
+		PropertiesUtil.loadFromResource("application.properties", props);
+
+		String active = null;
+		String password = null;
+		if (null != args) {
+			for (String s : args) {
+				s = s.trim();
+				if (s.startsWith("--startup.password=")) {
+					password = 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 (null != value) {
+				active = value.trim();
+			}
+		}
+		System.out.println("active=" + active);
+		if (StringUtils.isBlank(active)) {
+			throw new ExamCloudRuntimeException("active is not specified");
+		}
+
+		if (StringUtils.isBlank(password)) {
+			throw new ExamCloudRuntimeException("password is not specified");
+		}
+
+		PropertiesUtil.loadFromResource(active + "/application.properties", props);
+		PropertiesUtil.loadFromResource(active + "/application-config.properties", props);
+
+		String testPassword = props.getProperty("$encrypted.$testPassword");
+		if (StringUtils.isBlank(testPassword)) {
+			throw new ExamCloudRuntimeException(
+					"property[$encrypted.$testPassword] is not configured");
+		}
+		try {
+			SecurityUtil.decrypt(testPassword, password);
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException("password is wrong!");
+		}
+
+		SecurityUtil.decrypt(props, password);
+
+		Set<String> argSet = Sets.newLinkedHashSet();
+		for (Entry<Object, Object> p : props.entrySet()) {
+			PropertiesUtil.setProperty((String) p.getKey(), (String) p.getValue());
+			String arg = "--" + p.getKey() + "=" + p.getValue();
+			argSet.add(arg);
+		}
+
+		String[] newArgs = argSet.toArray(new String[argSet.size()]);
+
+		BootSecurityManager.getInstance().setSecretKey(password);
+		BootSecurityManager.getInstance().setActive(active);
+
+		return SpringApplication.run(source, newArgs);
+	}
+
+	/**
+	 * 启动
+	 *
+	 * @author WANGWEI
+	 * @param source
+	 * @param appSimpleName
+	 * @param args
+	 * @return
+	 */
+	public static ConfigurableApplicationContext run(Object source, String appSimpleName,
+			String... args) {
+
+		Properties props = new Properties();
+		PropertiesUtil.loadFromResource("application.properties", props);
+
+		String securityCode = null;
+		String active = null;
+		String host = null;
+		String port = null;
+		if (null != args) {
+			for (String s : args) {
+				s = s.trim();
+				if (s.startsWith("--startup.securityCode=")) {
+					securityCode = s.substring(s.indexOf("=") + 1);
+				}
+				if (s.startsWith("--spring.profiles.active=")) {
+					active = s.substring(s.indexOf("=") + 1);
+				}
+				if (s.startsWith("--config.server.host=")) {
+					host = s.substring(s.indexOf("=") + 1);
+				}
+				if (s.startsWith("--config.server.port=")) {
+					port = s.substring(s.indexOf("=") + 1);
+				}
+			}
+		}
+		if (null == active) {
+			String value = props.getProperty("spring.profiles.active");
+			if (null != value) {
+				active = value.trim();
+			}
+		}
+
+		System.out.println("active=" + active);
+		if (StringUtils.isBlank(active)) {
+			throw new ExamCloudRuntimeException("active is not specified");
+		}
+
+		if (null == host) {
+			host = props.getProperty("config.server.host");
+		}
+		System.out.println("config server host=" + host);
+
+		if (null == port) {
+			port = props.getProperty("config.server.port", "9999");
+		}
+		System.out.println("config server port=" + port);
+
+		if ("dev".equals(active)) {
+			securityCode = "SB";
+		}
+
+		if (null == securityCode) {
+			System.out.println("securityCode is null");
+			throw new ExamCloudRuntimeException("securityCode is not specified");
+		} else {
+			System.out.println("securityCode=" + securityCode);
+		}
+
+		if (StringUtils.isBlank(host)) {
+			throw new ExamCloudRuntimeException("host is not specified");
+		}
+
+		String url = "http://" + host + ":" + port + "/properties/" + appSimpleName + "/" + active;
+		Map<String, String> pairs = getProperties(url, securityCode);
+
+		SecurityUtil.decrypt(pairs, securityCode);
+
+		if (null != args) {
+			for (String arg : args) {
+				arg = arg.trim();
+				if (arg.startsWith("--")) {
+					String key = arg.substring(2, arg.indexOf("="));
+					String value = arg.substring(arg.indexOf("=") + 1);
+					pairs.put(key, value);
+				}
+			}
+		}
+
+		setSystemProperties(pairs);
+
+		Set<String> argSet = Sets.newLinkedHashSet();
+		for (Entry<String, String> p : pairs.entrySet()) {
+			PropertiesUtil.setProperty(p.getKey(), p.getValue());
+			String arg = "--" + p.getKey() + "=" + p.getValue();
+			argSet.add(arg);
+		}
+
+		BootSecurityManager.getInstance().setActive(active);
+
+		String[] newArgs = argSet.toArray(new String[argSet.size()]);
+
+		String sendStartupStatusUrl = "http://" + host + ":" + port + "/startupStatus/"
+				+ appSimpleName + "/" + active;
+		try {
+			ConfigurableApplicationContext context = SpringApplication.run(source, newArgs);
+			sendStartupStatus(sendStartupStatusUrl, securityCode, "success");
+
+			return context;
+		} catch (Exception e) {
+			sendStartupStatus(sendStartupStatusUrl, securityCode,
+					"failure\n" + Util.getStackTrace(e));
+			throw e;
+		}
+
+	}
+
+	/**
+	 * 设置系统属性
+	 *
+	 * @author WANGWEI
+	 * @param pairs
+	 */
+	private static void setSystemProperties(Map<String, String> pairs) {
+		for (Entry<String, String> p : pairs.entrySet()) {
+			String key = p.getKey();
+			String value = p.getValue();
+			if (key.endsWith("$log.level.default")) {
+				System.setProperty("logLevel", value);
+			} else if (key.endsWith("$log.rootPath")) {
+				System.setProperty("logRootPath", value);
+			}
+		}
+	}
+
+	/**
+	 * POST 表单请求
+	 *
+	 * @author WANGWEI
+	 * @param url
+	 * @return
+	 */
+	private static Map<String, String> getProperties(String url, String securityCode) {
+		Map<String, String> headers = Maps.newHashMap();
+		headers.put("securityCode", securityCode);
+		CloseableHttpResponse response = null;
+		String respBody = null;
+		try {
+			response = post(url, headers, null);
+			respBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			HttpClientUtil.close(response);
+		}
+
+		if (org.apache.http.HttpStatus.SC_OK != response.getStatusLine().getStatusCode()) {
+			System.out.println("fail to get properties from config server");
+			throw new ExamCloudRuntimeException("fail to get properties from config server");
+		}
+
+		respBody = SecurityUtil.decrypt(respBody, securityCode);
+
+		@SuppressWarnings("unchecked")
+		Map<String, String> map = JsonUtil.fromJson(respBody, Map.class);
+		return map;
+	}
+
+	/**
+	 * 发送启动状态
+	 *
+	 * @author WANGWEI
+	 * @param url
+	 * @param securityCode
+	 * @param body
+	 */
+	private static void sendStartupStatus(String url, String securityCode, String body) {
+		Map<String, String> headers = Maps.newHashMap();
+		headers.put("securityCode", securityCode);
+		CloseableHttpResponse response = null;
+		try {
+			response = post(url, headers, body);
+			EntityUtils.toString(response.getEntity(), "UTF-8");
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			HttpClientUtil.close(response);
+		}
+	}
+
+	/**
+	 * POST
+	 *
+	 * @author WANGWEI
+	 * @param url
+	 * @param headers
+	 * @param body
+	 * @return
+	 */
+	private static CloseableHttpResponse post(String url, Map<String, String> headers,
+			String body) {
+
+		CloseableHttpClient httpclient = HttpClientPool.getHttpClient();
+		HttpPost post = new HttpPost(url);
+		post.setConfig(RequestConfig.custom().setConnectTimeout(10000).build());
+		post.setConfig(RequestConfig.custom().setConnectionRequestTimeout(1000 * 60 * 2).build());
+
+		for (Map.Entry<String, String> entry : headers.entrySet()) {
+			post.addHeader(entry.getKey(), entry.getValue());
+		}
+
+		try {
+			if (null != body) {
+				post.setEntity(new StringEntity(body, "UTF-8"));
+			}
+			CloseableHttpResponse response = httpclient.execute(post);
+			return response;
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException(e);
+		}
+	}
+
+}

+ 30 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/BaseRequest.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+import cn.com.qmth.examcloud.commons.web.enums.DataExecutionStatus;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * 请求体基类
+ * 
+ * @author WANGWEI
+ *
+ */
+public abstract class BaseRequest extends ExchangeBean {
+
+	private static final long serialVersionUID = 6465330136225230063L;
+
+	/**
+	 * 数据执行状态
+	 */
+	@ApiModelProperty(value = "数据执行状态", example = "UPDATE", required = false)
+	private DataExecutionStatus des;
+
+	public DataExecutionStatus getDes() {
+		return des;
+	}
+
+	public void setDes(DataExecutionStatus des) {
+		this.des = des;
+	}
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/BaseResponse.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+import cn.com.qmth.examcloud.commons.web.enums.DataExecutionStatus;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * 响应体基类
+ * 
+ * @author WANGWEI
+ *
+ */
+public abstract class BaseResponse extends ExchangeBean {
+
+	private static final long serialVersionUID = 1755304211766414171L;
+
+	/**
+	 * 耗时(毫秒)
+	 */
+	@ApiModelProperty(value = "耗时(毫秒)", example = "500", required = true)
+	private Long cost;
+
+	/**
+	 * 数据执行状态
+	 */
+	@ApiModelProperty(value = "数据执行状态", example = "UPDATE", required = false)
+	private DataExecutionStatus des;
+
+	public Long getCost() {
+		return cost;
+	}
+
+	public void setCost(Long cost) {
+		this.cost = cost;
+	}
+
+	public DataExecutionStatus getDes() {
+		return des;
+	}
+
+	public void setDes(DataExecutionStatus des) {
+		this.des = des;
+	}
+
+}

+ 12 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/CloudService.java

@@ -0,0 +1,12 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+import java.io.Serializable;
+
+/**
+ * 云服务顶级接口
+ *
+ * @author WANGWEI
+ */
+public interface CloudService extends Serializable {
+
+}

+ 13 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/ExchangeBean.java

@@ -0,0 +1,13 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+/**
+ * bean 基类
+ * 
+ * @author WANGWEI
+ *
+ */
+public abstract class ExchangeBean implements JsonSerializable {
+
+	private static final long serialVersionUID = 3913250969569367810L;
+
+}

+ 16 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/JsonSerializable.java

@@ -0,0 +1,16 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+import java.io.Serializable;
+
+/**
+ * 可序列化为JSON<br>
+ * <p>
+ * 严重警告: 此接口为标识接口,禁止添加属性和方法. by wangwei
+ * </p>
+ * 
+ * @author WANGWEI
+ *
+ */
+public interface JsonSerializable extends Serializable {
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/cloud/api/OuterService.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.examcloud.commons.web.cloud.api;
+
+import java.io.Serializable;
+
+/**
+ * 对外服务接口
+ *
+ * @author WANGWEI
+ * @date 2018年6月29日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public interface OuterService extends Serializable {
+
+}

+ 59 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/config/SystemConfig.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.commons.web.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.commons.base.exception.StatusException;
+import cn.com.qmth.examcloud.commons.base.util.PropertiesUtil;
+
+/**
+ * 系统配置
+ *
+ * @author WANGWEI
+ * @date 2018年9月7日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class SystemConfig {
+
+	@Value("${$dir}")
+	private String dir;
+
+	@Value("${$tempDir}")
+	private String tempDir;
+
+	/**
+	 * 获取数据目录
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static String getDataDir() {
+		String dir = PropertiesUtil.getString("$dir");
+		if (StringUtils.isBlank(dir)) {
+			throw new StatusException("370", "数据目录未配置");
+		}
+		if (dir.endsWith("/")) {
+			dir = dir.substring(0, dir.length() - 1);
+		}
+		return dir;
+	}
+
+	/**
+	 * 获取临时数据目录
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static String getTempDataDir() {
+		String tempDir = PropertiesUtil.getString("$tempDir");
+		if (StringUtils.isBlank(tempDir)) {
+			throw new StatusException("370", "临时数据目录未配置");
+		}
+		if (tempDir.endsWith("/")) {
+			tempDir = tempDir.substring(0, tempDir.length() - 1);
+		}
+		return tempDir;
+	}
+}

+ 44 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/enums/BooleanSelect.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.commons.web.enums;
+
+/**
+ * 页面boolean选择
+ *
+ * @author WANGWEI
+ * @date 2018年12月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum BooleanSelect {
+
+	/**
+	 * 未指定
+	 */
+	UNDEFINED,
+
+	/**
+	 * true
+	 */
+	TRUE,
+
+	/**
+	 * false
+	 */
+	FALSE;
+
+	/**
+	 * 获取boolean
+	 *
+	 * @author WANGWEI
+	 * @param value
+	 * @return
+	 */
+	public Boolean getBoolean() {
+		if (UNDEFINED.equals(this)) {
+			return null;
+		} else if (TRUE.equals(this)) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+}

+ 30 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/enums/DataExecutionStatus.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.examcloud.commons.web.enums;
+
+/**
+ * 数据执行状态
+ *
+ * @author WANGWEI
+ * @date 2018年11月7日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum DataExecutionStatus {
+
+	/**
+	 * 创建
+	 */
+	CREATION,
+	/**
+	 * 更新
+	 */
+	UPDATE,
+
+	/**
+	 * 创建或更新
+	 */
+	CREATION_OR_UPDATE,
+	/**
+	 * 删除
+	 */
+	DELETE
+
+}

+ 136 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/helpers/page/PageInfo.java

@@ -0,0 +1,136 @@
+package cn.com.qmth.examcloud.commons.web.helpers.page;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.springframework.data.domain.Page;
+
+/**
+ * 分页
+ *
+ * @author WANGWEI
+ * @date 2018年6月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public class PageInfo<T> implements Serializable {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 当前页码
+	 */
+	private long index = 1;
+
+	/**
+	 * 每页的数量
+	 */
+	private long limit = 10;
+
+	/**
+	 * 当前页的数量
+	 */
+	private long size;
+
+	/**
+	 * 总记录数
+	 */
+	private long total;
+
+	/**
+	 * 总页数
+	 */
+	private long pages;
+
+	/**
+	 * 结果集
+	 */
+	private List<T> list;
+
+	/**
+	 * 构造函数
+	 *
+	 */
+	public PageInfo() {
+
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param page
+	 */
+	public PageInfo(Page<T> page) {
+		this.total = page.getTotalElements();
+		this.pages = page.getTotalPages();
+		this.index = page.getNumber();
+		this.size = page.getNumberOfElements();
+		this.limit = page.getSize();
+
+		this.list = page.getContent();
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param page
+	 * @param list
+	 */
+	public PageInfo(Page<?> page, List<T> list) {
+		this.total = page.getTotalElements();
+		this.pages = page.getTotalPages();
+		this.index = page.getNumber();
+		this.size = page.getNumberOfElements();
+		this.limit = page.getSize();
+
+		this.list = list;
+	}
+
+	public long getIndex() {
+		return index;
+	}
+
+	public void setIndex(long index) {
+		this.index = index;
+	}
+
+	public long getLimit() {
+		return limit;
+	}
+
+	public void setLimit(long limit) {
+		this.limit = limit;
+	}
+
+	public long getSize() {
+		return size;
+	}
+
+	public void setSize(long size) {
+		this.size = size;
+	}
+
+	public long getTotal() {
+		return total;
+	}
+
+	public void setTotal(long total) {
+		this.total = total;
+	}
+
+	public long getPages() {
+		return pages;
+	}
+
+	public void setPages(long pages) {
+		this.pages = pages;
+	}
+
+	public List<T> getList() {
+		return list;
+	}
+
+	public void setList(List<T> list) {
+		this.list = list;
+	}
+
+}

+ 95 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/EleTreeNode.java

@@ -0,0 +1,95 @@
+package cn.com.qmth.examcloud.commons.web.helpers.tree;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+
+/**
+ * element-UI tree
+ *
+ * @author WANGWEI
+ * @date 2018年6月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class EleTreeNode implements JsonSerializable {
+
+	private static final long serialVersionUID = -1250100928710219190L;
+
+	private Long id;
+
+	private String label;
+
+	private String code;
+
+	private Long parentId;
+
+	private Boolean disabled;
+
+	private List<EleTreeNode> children;
+
+	public EleTreeNode() {
+		super();
+	}
+
+	public EleTreeNode(Long id, String label) {
+		super();
+		this.id = id;
+		this.label = label;
+	}
+
+	public EleTreeNode(Long id, String label, Long parentId) {
+		super();
+		this.id = id;
+		this.label = label;
+		this.parentId = parentId;
+	}
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getLabel() {
+		return label;
+	}
+
+	public void setLabel(String label) {
+		this.label = label;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getParentId() {
+		return parentId;
+	}
+
+	public void setParentId(Long parentId) {
+		this.parentId = parentId;
+	}
+
+	public Boolean getDisabled() {
+		return disabled;
+	}
+
+	public void setDisabled(Boolean disabled) {
+		this.disabled = disabled;
+	}
+
+	public List<EleTreeNode> getChildren() {
+		return children;
+	}
+
+	public void setChildren(List<EleTreeNode> children) {
+		this.children = children;
+	}
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/TreeNode.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.commons.web.helpers.tree;
+
+/**
+ * 树节点接口
+ *
+ * @author WANGWEI
+ * @date 2018年2月7日
+ */
+public interface TreeNode {
+
+	String getNodeId();
+
+	void setNodeId(String nodeId);
+
+	String getNodeName();
+
+	void setNodeName(String nodeName);
+
+	String getNodeCode();
+
+	void setNodeCode(String nodeCode);
+
+	String getParentNodeId();
+
+	void setParentNodeId(String parentNodeId);
+
+}

+ 122 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/TreeUtil.java

@@ -0,0 +1,122 @@
+package cn.com.qmth.examcloud.commons.web.helpers.tree;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.base.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+
+/**
+ * 树工具
+ *
+ * @author WANG WEI
+ */
+public class TreeUtil {
+
+	/**
+	 * 转换
+	 * 
+	 * @author WANG WEI
+	 * @param treeNode
+	 * @param c
+	 * @return
+	 */
+	public static TreeNode convert(TreeNode treeNode, Class<? extends TreeNode> c) {
+		try {
+			TreeNode instance = c.newInstance();
+			instance.setNodeId(treeNode.getNodeId());
+			instance.setNodeName(treeNode.getNodeName());
+			instance.setParentNodeId(treeNode.getParentNodeId());
+
+			return instance;
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * 转换
+	 *
+	 * @author WANGWEI
+	 * @param treeNodeList
+	 * @param c
+	 * @return
+	 */
+	public static List<TreeNode> convert(List<? extends TreeNode> treeNodeList,
+			Class<? extends TreeNode> c) {
+		List<TreeNode> ret = new ArrayList<TreeNode>();
+		for (TreeNode n : treeNodeList) {
+			ret.add(convert(n, c));
+		}
+		return ret;
+	}
+
+	/**
+	 * 转换
+	 *
+	 * @author WANGWEI
+	 * @param root
+	 * @param treeNodeList
+	 * @param disabledCodeList
+	 * @param includeDisabledCodes
+	 * @return
+	 */
+	public static EleTreeNode convert2OneEleTreeNode(EleTreeNode root,
+			List<? extends TreeNode> treeNodeList, List<String> disabledCodeList,
+			boolean includeDisabledCodes) {
+
+		Map<String, EleTreeNode> eleTreeMap = Maps.newHashMap();
+		for (TreeNode cur : treeNodeList) {
+			Long parentId = null;
+			if (null != cur.getParentNodeId()) {
+				parentId = Long.parseLong(cur.getParentNodeId());
+			}
+			EleTreeNode eleTree = new EleTreeNode(Long.parseLong(cur.getNodeId()),
+					cur.getNodeName(), parentId);
+			eleTree.setDisabled(disabledCodeList.contains(cur.getNodeCode()));
+			eleTreeMap.put(String.valueOf(eleTree.getId()), eleTree);
+		}
+
+		List<EleTreeNode> rootChildren = root.getChildren();
+		if (null == rootChildren) {
+			rootChildren = Lists.newArrayList();
+			root.setChildren(rootChildren);
+		}
+
+		for (TreeNode cur : treeNodeList) {
+			EleTreeNode eleTree = eleTreeMap.get(cur.getNodeId());
+			if (null == eleTree || eleTree.getDisabled()) {
+				continue;
+			}
+			String parentTreeId = cur.getParentNodeId();
+
+			if (StringUtils.isBlank(parentTreeId)) {
+				rootChildren.add(eleTree);
+			} else {
+				EleTreeNode parentEleTree = eleTreeMap.get(parentTreeId);
+				if (null == parentEleTree) {
+					throw new ExamCloudRuntimeException(
+							"no parent. TreeNode:" + JsonUtil.toJson(cur));
+				}
+				if (parentEleTree.getDisabled()) {
+					continue;
+				}
+				List<EleTreeNode> children = parentEleTree.getChildren();
+				if (null == children) {
+					children = Lists.newArrayList();
+					parentEleTree.setChildren(children);
+				}
+				children.add(eleTree);
+			}
+
+		}
+		return root;
+	}
+
+}

+ 59 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/helpers/tree/ZtreeNode.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.commons.web.helpers.tree;
+
+/**
+ * ztree
+ *
+ * @author WANGWEI
+ * @date 2018年2月7日
+ */
+public class ZtreeNode implements TreeNode {
+
+	private String id;
+
+	private String name;
+
+	private String code;
+
+	private String parentId;
+
+	@Override
+	public String getNodeId() {
+		return this.id;
+	}
+
+	@Override
+	public void setNodeId(String treeId) {
+		this.id = treeId;
+	}
+
+	@Override
+	public String getNodeName() {
+		return this.name;
+	}
+
+	@Override
+	public void setNodeName(String treeName) {
+		this.name = treeName;
+	}
+
+	@Override
+	public String getParentNodeId() {
+		return this.parentId;
+	}
+
+	@Override
+	public void setParentNodeId(String parentId) {
+		this.parentId = parentId;
+	}
+
+	@Override
+	public String getNodeCode() {
+		return this.code;
+	}
+
+	@Override
+	public void setNodeCode(String code) {
+		this.code = code;
+	}
+
+}

+ 228 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/FirstInterceptor.java

@@ -0,0 +1,228 @@
+package cn.com.qmth.examcloud.commons.web.interceptor;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.MDC;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.base.helpers.FileChangeWatchdog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.logging.SLF4JImpl;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.PathUtil;
+import cn.com.qmth.examcloud.commons.base.util.StringUtil;
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.web.support.ServletUtil;
+import cn.com.qmth.examcloud.commons.web.support.StatusResponse;
+
+/**
+ * 首发拦截器
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class FirstInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(FirstInterceptor.class);
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private static final Set<String> RESOURCE_NAME_SET = Sets.newConcurrentHashSet();
+
+	/**
+	 * 拦截的请求
+	 */
+	private Set<String> badRequests;
+
+	/**
+	 * 热加载配置文件
+	 *
+	 * @author WANGWEI
+	 * @param resourceName
+	 */
+	public synchronized void configureAndWatch(String resourceName) {
+		if (RESOURCE_NAME_SET.contains(resourceName)) {
+			return;
+		}
+		String path = PathUtil.getResoucePath(resourceName);
+		loadFromFile(path);
+
+		InterceptConfFileChangeWatchdog dog = new InterceptConfFileChangeWatchdog(path);
+		dog.setDelay(30000);
+		dog.start();
+		RESOURCE_NAME_SET.add(resourceName);
+	}
+
+	/**
+	 * 观察线程
+	 *
+	 * @author WANGWEI
+	 */
+	private class InterceptConfFileChangeWatchdog extends FileChangeWatchdog {
+
+		protected InterceptConfFileChangeWatchdog(String path) {
+			super(path);
+		}
+
+		@Override
+		protected void doOnChange() {
+			try {
+				loadFromFile(path);
+			} catch (Exception e) {
+				LOG.info("Fail to load from file [" + path + "].", e);
+			}
+		}
+	}
+
+	private void loadFromFile(String path) {
+		try {
+			Set<String> newBadRequests = Sets.newHashSet();
+			List<String> lines = FileUtils.readLines(new File(path), "UTF-8");
+			for (String cur : lines) {
+				if (StringUtils.isNotBlank(cur)) {
+					newBadRequests.add(cur.trim());
+				}
+			}
+			badRequests = newBadRequests;
+			LOG.info("exclusions=" + JsonUtil.toJson(badRequests));
+		} catch (IOException e) {
+			LOG.error("fail to load config from file. filePath=" + path, e);
+		}
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+
+		String traceID = request.getHeader("TRACE_ID");
+		if (StringUtils.isBlank(traceID)) {
+			traceID = ThreadLocalUtil.next();
+		} else {
+			ThreadLocalUtil.setTraceID(traceID);
+		}
+
+		// 设置MDC
+		if (LOG instanceof SLF4JImpl) {
+			MDC.put("TRACE_ID", traceID);
+			MDC.put("KEY", "$$$$$");
+		}
+
+		String path = request.getServletPath();
+		String method = request.getMethod();
+
+		String reqPath = method + ":" + path;
+
+		LOG.debug("[preHandle]. reqPath = " + reqPath);
+
+		if (CollectionUtils.isNotEmpty(badRequests)) {
+			if (badRequests.contains(reqPath)) {
+				response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
+				ServletUtil.returnJsonWithoutLog(new StatusResponse("406", "Not Acceptable."),
+						response);
+				return false;
+			}
+		}
+
+		if (INTERFACE_LOG.isDebugEnabled()) {
+			Map<String, String> headers = Maps.newHashMap();
+			Enumeration<String> e = request.getHeaderNames();
+			while (e.hasMoreElements()) {
+				String name = (String) e.nextElement();
+				String value = request.getHeader(name);
+				if (name.startsWith("cookie")) {
+					continue;
+				}
+				headers.put(name, value);
+			}
+			INTERFACE_LOG.debug(StringUtil.join("[preHandle]. path=\"", path, "\"; method=[",
+					method, "]", "; headers=", JsonUtil.toJson(headers)));
+		}
+
+		if (handler instanceof HandlerMethod) {
+			HandlerMethod handlerMethod = (HandlerMethod) handler;
+			Object controllerObject = handlerMethod.getBean();
+			RequestMapping mappingOfClass = AnnotationUtils
+					.findAnnotation(controllerObject.getClass(), RequestMapping.class);
+			RequestMapping mappingOfMethod = handlerMethod
+					.getMethodAnnotation(RequestMapping.class);
+
+			String[] allPathOfClass = null;
+			String[] allPathOfMethod = null;
+
+			if (null != mappingOfClass) {
+				allPathOfClass = mappingOfClass.path();
+			}
+			if (null != mappingOfMethod) {
+				allPathOfMethod = mappingOfMethod.path();
+			}
+
+			String pathsOfClass = null;
+			String pathsOfMethod = null;
+
+			if (null != allPathOfClass) {
+				pathsOfClass = StringUtils.join(allPathOfClass, ",");
+			}
+			if (null != allPathOfMethod) {
+				pathsOfMethod = StringUtils.join(allPathOfMethod, ",");
+			}
+
+			String mappingPath = StringUtils.join("[", pathsOfClass, "][", pathsOfMethod, "][",
+					method, "]");
+
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("[preHandle]. mapping = " + mappingPath);
+			}
+
+			request.setAttribute("$mappingPath", mappingPath);
+			request.setAttribute("$ctrClass", controllerObject.getClass());
+
+		} else {
+			INTERFACE_LOG.error("cry.....");
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+			ModelAndView modelAndView) throws Exception {
+		LOG.debug("postHandle... ...");
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+		LOG.debug("afterCompletion... ...");
+		// 清理MDC
+		if (LOG instanceof SLF4JImpl) {
+			MDC.clear();
+		}
+	}
+
+}

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/Seqlock.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.commons.web.interceptor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 请求顺序锁(全局) 注解
+ *
+ * @author WANGWEI
+ * @date 2018年11月12日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Seqlock {
+
+}

+ 124 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/SeqlockInterceptor.java

@@ -0,0 +1,124 @@
+package cn.com.qmth.examcloud.commons.web.interceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.web.redis.RedisClient;
+import cn.com.qmth.examcloud.commons.web.security.bean.User;
+import cn.com.qmth.examcloud.commons.web.support.ServletUtil;
+import cn.com.qmth.examcloud.commons.web.support.StatusResponse;
+
+/**
+ * 顺序锁拦截器
+ *
+ * @author WANGWEI
+ * @date 2018年11月12日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class SeqlockInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(SeqlockInterceptor.class);
+
+	/**
+	 * redis client
+	 */
+	private RedisClient redisClient;
+
+	private static final String LOCK_PREFIX = "$_lock:";
+
+	/**
+	 * 构造函数
+	 *
+	 * @param redisClient
+	 * @param exclusions
+	 */
+	public SeqlockInterceptor(RedisClient redisClient) {
+		super();
+		this.redisClient = redisClient;
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		LOG.debug("preHandle... ...");
+
+		User user = null;
+		Object userAttribute = request.getAttribute("$accessUser");
+		if (null == userAttribute) {
+			return true;
+		} else {
+			user = (User) userAttribute;
+		}
+
+		if (handler instanceof HandlerMethod) {
+			HandlerMethod handlerMethod = (HandlerMethod) handler;
+			Seqlock seqlock = handlerMethod.getMethodAnnotation(Seqlock.class);
+			SessionSeqlock sessionSeqlock = handlerMethod.getMethodAnnotation(SessionSeqlock.class);
+			if (null != seqlock) {
+				String mappingPath = (String) request.getAttribute("$mappingPath");
+				String key = LOCK_PREFIX + mappingPath;
+
+				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceID(), 60 * 5)) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug("global locked");
+					}
+					return true;
+				} else {
+					response.setStatus(HttpStatus.CONFLICT.value());
+					ServletUtil.returnJson(new StatusResponse("409", "请稍后重试... ..."), response);
+					return false;
+				}
+
+			} else if (null != sessionSeqlock) {
+				String mappingPath = (String) request.getAttribute("$mappingPath");
+				String key = LOCK_PREFIX + user.getUserId() + ":" + mappingPath;
+
+				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceID(), 60 * 5)) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug("sesssion locked");
+					}
+					return true;
+				} else {
+					response.setStatus(HttpStatus.CONFLICT.value());
+					ServletUtil.returnJson(new StatusResponse("409", "请稍后重试... ..."), response);
+					return false;
+				}
+			}
+		}
+
+		return true;
+	}
+
+	@Override
+	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+			ModelAndView modelAndView) throws Exception {
+		LOG.debug("postHandle... ...");
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+		LOG.debug("afterCompletion... ...");
+
+		String mappingPath = (String) request.getAttribute("$mappingPath");
+		String key = LOCK_PREFIX + mappingPath;
+		redisClient.delete(key);
+
+		User user = null;
+		Object userAttribute = request.getAttribute("$accessUser");
+		if (null != userAttribute) {
+			user = (User) userAttribute;
+			String sessionKey = LOCK_PREFIX + user.getUserId() + ":" + mappingPath;
+			redisClient.delete(sessionKey);
+		}
+	}
+
+}

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/interceptor/SessionSeqlock.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.commons.web.interceptor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 请求顺序锁(用户) 注解
+ *
+ * @author WANGWEI
+ * @date 2018年11月12日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SessionSeqlock {
+
+}

+ 58 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/jpa/JpaEntity.java

@@ -0,0 +1,58 @@
+package cn.com.qmth.examcloud.commons.web.jpa;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+
+/**
+ * JPA 数据库实体父类
+ *
+ * @author WANGWEI
+ * @date 2018年5月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class JpaEntity implements JsonSerializable {
+
+	private static final long serialVersionUID = 1561273677157469875L;
+
+	/**
+	 * 更新时间
+	 */
+	@LastModifiedDate
+	@Column(nullable = true)
+	private Date updateTime;
+
+	/**
+	 * 创建时间
+	 */
+	@CreatedDate
+	@Column(nullable = true, updatable = false)
+	private Date creationTime;
+
+	public Date getUpdateTime() {
+		return updateTime;
+	}
+
+	public void setUpdateTime(Date updateTime) {
+		this.updateTime = updateTime;
+	}
+
+	public Date getCreationTime() {
+		return creationTime;
+	}
+
+	public void setCreationTime(Date creationTime) {
+		this.creationTime = creationTime;
+	}
+
+}

+ 89 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/redis/RedisClient.java

@@ -0,0 +1,89 @@
+package cn.com.qmth.examcloud.commons.web.redis;
+
+import java.io.Serializable;
+
+/**
+ * 类注释
+ *
+ * @author WANGWEI
+ */
+public interface RedisClient {
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 */
+	public void set(String key, Serializable value);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 * @param timeout
+	 */
+	public void set(String key, Serializable value, int timeout);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param timeout
+	 */
+	public void expire(String key, int timeout);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param c
+	 * @param timeout
+	 * @return
+	 */
+	public <T> T get(String key, Class<T> c, int timeout);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param c
+	 * @return
+	 */
+	public <T> T get(String key, Class<T> c);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 */
+	public void delete(String key);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param channel
+	 * @param message
+	 */
+	public void convertAndSend(String channel, Object message);
+
+	/**
+	 * (在key不存在时,创建并设置value 返回true; key存在时,会反回false)
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 * @param timeout
+	 * @return
+	 */
+	public Boolean setIfAbsent(String key, String value, int timeout);
+
+}

+ 75 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/redis/RedisClientImpl.java

@@ -0,0 +1,75 @@
+package cn.com.qmth.examcloud.commons.web.redis;
+
+import java.io.Serializable;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * 类注释
+ *
+ * @author WANGWEI
+ */
+public final class RedisClientImpl implements RedisClient {
+
+	private RedisTemplate<String, Object> redisTemplate;
+
+	public RedisClientImpl(RedisTemplate<String, Object> redisTemplate) {
+		super();
+		this.redisTemplate = redisTemplate;
+	}
+
+	@Override
+	public void set(String key, Serializable value) {
+		redisTemplate.opsForValue().set(key, value);
+	}
+
+	@Override
+	public void set(String key, Serializable value, int timeout) {
+		set(key, value);
+		expire(key, timeout);
+	}
+
+	@Override
+	public void expire(String key, int timeout) {
+		redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c, int timeout) {
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		expire(key, timeout);
+		return t;
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c) {
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		return t;
+	}
+
+	@Override
+	public void delete(String key) {
+		redisTemplate.opsForValue().set(key, null);
+		expire(key, 0);
+	}
+
+	@Override
+	public void convertAndSend(String channel, Object message) {
+		redisTemplate.convertAndSend(channel, message);
+	}
+
+	@Override
+	public Boolean setIfAbsent(String key, String value, int timeout) {
+		Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value);
+		if (b) {
+			expire(key, timeout);
+		}
+		return b;
+	}
+
+}

+ 70 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/reports/BaseReport.java

@@ -0,0 +1,70 @@
+package cn.com.qmth.examcloud.commons.web.reports;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+
+/**
+ * 报表数据体基类
+ *
+ * @author WANGWEI
+ * @date 2018年11月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public abstract class BaseReport implements JsonSerializable {
+
+	private static final long serialVersionUID = -6666294808887127563L;
+
+	/**
+	 * 采集时间
+	 */
+	private Date reportTime;
+
+	/**
+	 * 是否忽略采集异常
+	 */
+	private Boolean ignoreException;
+
+	/**
+	 * 采集异常
+	 */
+	private Boolean hasException;
+
+	/**
+	 * 采集主机
+	 */
+	private String reportHost;
+
+	public Date getReportTime() {
+		return reportTime;
+	}
+
+	public void setReportTime(Date reportTime) {
+		this.reportTime = reportTime;
+	}
+
+	public Boolean getIgnoreException() {
+		return ignoreException;
+	}
+
+	public void setIgnoreException(Boolean ignoreException) {
+		this.ignoreException = ignoreException;
+	}
+
+	public Boolean getHasException() {
+		return hasException;
+	}
+
+	public void setHasException(Boolean hasException) {
+		this.hasException = hasException;
+	}
+
+	public String getReportHost() {
+		return reportHost;
+	}
+
+	public void setReportHost(String reportHost) {
+		this.reportHost = reportHost;
+	}
+
+}

+ 33 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportFileFilter.java

@@ -0,0 +1,33 @@
+package cn.com.qmth.examcloud.commons.web.reports;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+/**
+ * 日志文件过滤器
+ *
+ * @author WANGWEI
+ * @date 2018年11月14日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ReportFileFilter implements FilenameFilter {
+
+	private Class<?> reportClass;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param reportClass
+	 */
+	public ReportFileFilter(Class<?> reportClass) {
+		super();
+		this.reportClass = reportClass;
+	}
+
+	@Override
+	public boolean accept(File dir, String name) {
+		String regex = reportClass.getSimpleName() + "\\.\\d{12}\\.\\d+\\.txt";
+		return name.matches(regex);
+	}
+
+}

+ 131 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportLoggerFactory.java

@@ -0,0 +1,131 @@
+package cn.com.qmth.examcloud.commons.web.reports;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
+import ch.qos.logback.core.util.FileSize;
+import ch.qos.logback.core.util.OptionHelper;
+import cn.com.qmth.examcloud.commons.web.config.SystemConfig;
+
+/**
+ * 报表日志工厂
+ *
+ * @author WANGWEI
+ * @date 2018年11月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ReportLoggerFactory {
+
+	private static final Map<String, Logger> LOGGERS_HOLDER = new HashMap<>();
+
+	private static final Object LOCK = new Object();
+
+	/**
+	 * 刷新
+	 *
+	 * @author WANGWEI
+	 */
+	public static void refresh() {
+
+		for (Entry<String, Logger> entry : LOGGERS_HOLDER.entrySet()) {
+			Logger value = entry.getValue();
+			value.info(" ");
+		}
+
+	}
+
+	/**
+	 * 获取Logger
+	 *
+	 * @author WANGWEI
+	 * @param reportClass
+	 * @return
+	 */
+	public static Logger getLogger(Class<? extends BaseReport> reportClass) {
+		String name = reportClass.getName();
+		Logger logger = LOGGERS_HOLDER.get(name);
+		if (null != logger) {
+			return logger;
+		}
+		synchronized (LOCK) {
+			logger = LOGGERS_HOLDER.get(name);
+			if (null != logger) {
+				return logger;
+			}
+			logger = build(reportClass);
+			LOGGERS_HOLDER.put(name, logger);
+		}
+		return logger;
+	}
+
+	/**
+	 * 构建
+	 *
+	 * @author WANGWEI
+	 * @param report
+	 * @return
+	 */
+	private static Logger build(Class<? extends BaseReport> reportClass) {
+		String name = reportClass.getName();
+		String simpleName = reportClass.getSimpleName();
+		LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+
+		Logger logger = context.getLogger("REP-" + name);
+		logger.setAdditive(false);
+		RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
+
+		appender.setContext(context);
+		appender.setName("REP-" + name);
+		appender.setFile(OptionHelper.substVars(
+				getReportsLogRootDirectory() + "/" + name + "/" + simpleName + ".log", context));
+		appender.setAppend(true);
+		appender.setPrudent(false);
+		SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = new SizeAndTimeBasedRollingPolicy<>();
+		String fp = OptionHelper.substVars(getReportsLogRootDirectory() + "/" + name + "/"
+				+ simpleName + ".%d{yyyyMMddHHmm}.%i.txt", context);
+
+		policy.setMaxFileSize(FileSize.valueOf("32MB"));
+		policy.setFileNamePattern(fp);
+		policy.setMaxHistory(1024);
+		policy.setTotalSizeCap(FileSize.valueOf("32GB"));
+		policy.setParent(appender);
+		policy.setContext(context);
+		policy.start();
+
+		PatternLayoutEncoder encoder = new PatternLayoutEncoder();
+		encoder.setContext(context);
+		encoder.setPattern("%m");
+		encoder.start();
+
+		appender.setRollingPolicy(policy);
+		appender.setEncoder(encoder);
+		appender.start();
+
+		logger.addAppender(appender);
+
+		return logger;
+	}
+
+	/**
+	 * 获取报告根目录
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private static String getReportsLogRootDirectory() {
+		String dataDir = SystemConfig.getDataDir();
+		if (!dataDir.endsWith("/")) {
+			dataDir += "/";
+		}
+		return dataDir + "reports";
+	}
+}

+ 36 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportsCollector.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.examcloud.commons.web.reports;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+
+/**
+ * 报表数据收集器工具
+ *
+ * @author WANGWEI
+ * @date 2018年11月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ReportsCollector {
+
+	/**
+	 * 采集数据
+	 *
+	 * @author WANGWEI
+	 * @param report
+	 */
+	public static void collect(BaseReport report) {
+		ThreadLocalUtil.set("$_HAS_COLLECTED", true);
+		String key = "$_REPORT_COLLECTIONS";
+		@SuppressWarnings("unchecked")
+		List<BaseReport> reportList = (List<BaseReport>) ThreadLocalUtil.get(key);
+		if (null == reportList) {
+			reportList = Lists.newArrayList();
+			ThreadLocalUtil.set(key, reportList);
+		}
+		reportList.add(report);
+	}
+
+}

+ 139 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/reports/ReportsController.java

@@ -0,0 +1,139 @@
+package cn.com.qmth.examcloud.commons.web.reports;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.commons.base.exception.StatusException;
+import cn.com.qmth.examcloud.commons.web.config.SystemConfig;
+import cn.com.qmth.examcloud.commons.web.redis.RedisClient;
+import cn.com.qmth.examcloud.commons.web.support.ControllerSupport;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * 报表采集和回调
+ *
+ * @author WANGWEI
+ * @date 2018年7月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController
+@RequestMapping("reports")
+public class ReportsController extends ControllerSupport {
+
+	@Value("${spring.application.name}")
+	String application;
+
+	@Autowired
+	RedisClient redisClient;
+
+	/**
+	 * 获取报告文件
+	 *
+	 * @author WANGWEI
+	 * @throws IOException
+	 */
+	@ApiOperation(value = "获取报告文件")
+	@GetMapping("getReportFile")
+	public ResponseEntity<?> getReportFile(@RequestParam(required = true) String className)
+			throws IOException {
+
+		if (StringUtils.isBlank(className)) {
+			throw new StatusException("580", "className is blank");
+		}
+		Class<?> c = null;
+		try {
+			c = Class.forName(className);
+		} catch (ClassNotFoundException e) {
+			throw new StatusException("581", "className is wrong");
+		}
+		if (!c.isAssignableFrom(BaseReport.class)) {
+			throw new StatusException("582", "className is wrong");
+		}
+
+		String dirPath = getReportsLogRootDirectory() + "/" + className;
+
+		File dir = new File(dirPath);
+
+		if ((!dir.exists()) || (!dir.isDirectory())) {
+			new ResponseEntity<>(HttpStatus.NO_CONTENT);
+		}
+
+		String[] fileNames = dir.list(new ReportFileFilter(c));
+
+		if (null == fileNames || 0 == fileNames.length) {
+			new ResponseEntity<>(HttpStatus.NO_CONTENT);
+		}
+		List<String> fileNameList = Arrays.asList(fileNames);
+		Collections.sort(fileNameList);
+
+		String fileName = fileNameList.get(0);
+		String filePath = dirPath + "/" + fileName;
+
+		File file = new File(filePath);
+		HttpHeaders headers = new HttpHeaders();
+		headers.setContentDispositionFormData("attachment", fileName);
+		headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+		return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers,
+				HttpStatus.OK);
+	}
+
+	/**
+	 * 删除报告文件
+	 *
+	 * @author WANGWEI
+	 * @throws IOException
+	 */
+	@GetMapping("delReportFile")
+	public void delReportFile(@RequestParam(required = true) String className,
+			@RequestParam(required = true) String fileName) throws IOException {
+
+		if (StringUtils.isBlank(className)) {
+			throw new StatusException("580", "className is blank");
+		}
+		Class<?> c = null;
+		try {
+			c = Class.forName(className);
+		} catch (ClassNotFoundException e) {
+			throw new StatusException("581", "className is wrong");
+		}
+		if (!c.isAssignableFrom(BaseReport.class)) {
+			throw new StatusException("582", "className is wrong");
+		}
+
+		String dirPath = getReportsLogRootDirectory() + "/" + className;
+		String filePath = dirPath + "/" + fileName;
+
+		FileUtils.forceDelete(new File(filePath));
+	}
+
+	/**
+	 * 获取报告根目录
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private String getReportsLogRootDirectory() {
+		String dataDir = SystemConfig.getDataDir();
+		if (!dataDir.endsWith("/")) {
+			dataDir += "/";
+		}
+		return dataDir + "reports";
+	}
+
+}

+ 237 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/RequestPermissionInterceptor.java

@@ -0,0 +1,237 @@
+package cn.com.qmth.examcloud.commons.web.security;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.MDC;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.PathUtil;
+import cn.com.qmth.examcloud.commons.base.util.RegExpUtil;
+import cn.com.qmth.examcloud.commons.web.RedisKeys;
+import cn.com.qmth.examcloud.commons.web.cloud.api.CloudService;
+import cn.com.qmth.examcloud.commons.web.cloud.api.OuterService;
+import cn.com.qmth.examcloud.commons.web.redis.RedisClient;
+import cn.com.qmth.examcloud.commons.web.security.bean.User;
+import cn.com.qmth.examcloud.commons.web.support.ServletUtil;
+import cn.com.qmth.examcloud.commons.web.support.StatusResponse;
+
+/**
+ * 请求鉴权
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public abstract class RequestPermissionInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory
+			.getLog(RequestPermissionInterceptor.class);
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private static final Set<String> RESOURCE_NAME_SET = Sets.newConcurrentHashSet();
+
+	/**
+	 * redis client
+	 */
+	private RedisClient redisClient;
+
+	/**
+	 * 例外请求前缀的正则表达式集合(不进行权限校验)
+	 */
+	private Set<String> exclusions;
+
+	/**
+	 * 热加载配置文件
+	 *
+	 * @author WANGWEI
+	 * @param resourceName
+	 */
+	public synchronized void configure(String resourceName) {
+		if (RESOURCE_NAME_SET.contains(resourceName)) {
+			return;
+		}
+		String path = PathUtil.getResoucePath(resourceName);
+		loadFromFile(path);
+
+		RESOURCE_NAME_SET.add(resourceName);
+	}
+
+	/**
+	 * 判断是否有请求权限
+	 *
+	 * @author WANGWEI
+	 * @param mappingPath
+	 * @param user
+	 * @return
+	 */
+	public abstract boolean hasPermission(String mappingPath, User user);
+
+	/**
+	 * 构造函数
+	 *
+	 * @param redisClient
+	 * @param exclusions
+	 */
+	public RequestPermissionInterceptor(RedisClient redisClient) {
+		super();
+		this.redisClient = redisClient;
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		LOG.debug("preHandle... ...");
+
+		Class<?> ctrClass = (Class<?>) request.getAttribute("$ctrClass");
+		if (CloudService.class.isAssignableFrom(ctrClass)
+				|| OuterService.class.isAssignableFrom(ctrClass)) {
+			return true;
+		}
+
+		String mappingPath = (String) request.getAttribute("$mappingPath");
+
+		if ("[runtime][][GET]".equals(mappingPath)) {
+			return true;
+		}
+
+		if (mappingPath.startsWith("[${server.error.path:${error.path:/error}}][]")) {
+			response.setStatus(HttpStatus.NOT_FOUND.value());
+			ServletUtil.returnJson(new StatusResponse("404", "not found."), response);
+			return false;
+		}
+
+		if (CollectionUtils.isNotEmpty(exclusions)) {
+			for (String e : exclusions) {
+				if (mappingPath.matches(e)) {
+					if (INTERFACE_LOG.isDebugEnabled()) {
+						INTERFACE_LOG.debug("No authentication. mapping=" + mappingPath);
+					}
+					return true;
+				}
+			}
+		}
+
+		String key = null;
+		String token = null;
+		String kt = request.getHeader("user_token");
+		if (StringUtils.isNotBlank(kt)) {
+			String[] arr = kt.split(":");
+			if (null != arr && 2 == arr.length) {
+				key = arr[0];
+				token = arr[1];
+			}
+		} else {
+			key = request.getHeader("key");
+			token = request.getHeader("token");
+		}
+
+		if (StringUtils.isBlank(key) && StringUtils.isBlank(token)) {
+			key = request.getParameter("$key");
+			token = request.getParameter("$token");
+		}
+
+		if (StringUtils.isBlank(key)) {
+			response.setStatus(HttpStatus.FORBIDDEN.value());
+			ServletUtil.returnJson(new StatusResponse("403", "key is blank."), response);
+			return false;
+		}
+		if (StringUtils.isBlank(token)) {
+			response.setStatus(HttpStatus.FORBIDDEN.value());
+			ServletUtil.returnJson(new StatusResponse("403", "token is blank."), response);
+			return false;
+		}
+
+		Integer sessionTimeout = redisClient.get(RedisKeys.SESSION_TIMEOUT, Integer.class);
+		User user = null;
+		if (null == sessionTimeout) {
+			user = redisClient.get(key, User.class);
+		} else {
+			user = redisClient.get(key, User.class, sessionTimeout);
+		}
+
+		if (null == user) {
+			response.setStatus(HttpStatus.FORBIDDEN.value());
+			ServletUtil.returnJson(new StatusResponse("403", "no login."), response);
+			return false;
+		} else if (!token.equals(user.getToken())) {
+			response.setStatus(HttpStatus.FORBIDDEN.value());
+			ServletUtil.returnJson(new StatusResponse("403", "token is wrong."), response);
+			return false;
+		}
+
+		MDC.put("KEY", key);
+
+		if (!hasPermission(mappingPath, user)) {
+			response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value());
+			ServletUtil.returnJson(new StatusResponse("405", "no permission."), response);
+			return false;
+		}
+
+		request.setAttribute("$accessUser", user);
+		request.setAttribute("$kt", key + ":" + token);
+
+		return true;
+	}
+
+	@Override
+	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+			ModelAndView modelAndView) throws Exception {
+		LOG.debug("postHandle... ...");
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+		LOG.debug("afterCompletion... ...");
+	}
+
+	/**
+	 * 加载配置项
+	 *
+	 * @author WANGWEI
+	 * @param path
+	 */
+	private void loadFromFile(String path) {
+		try {
+			Set<String> newExclusions = Sets.newHashSet();
+			List<String> lines = FileUtils.readLines(new File(path), "UTF-8");
+			for (String cur : lines) {
+				if (StringUtils.isNotBlank(cur)) {
+					cur = cur.trim();
+					if (cur.startsWith("regexp:")) {
+						cur = StringUtils.replace(cur, "regexp:", "", 1);
+					} else {
+						cur = RegExpUtil.escape(cur);
+					}
+					newExclusions.add(cur.trim());
+				}
+			}
+			exclusions = newExclusions;
+			LOG.info("exclusions=" + JsonUtil.toJson(exclusions));
+		} catch (IOException e) {
+			LOG.error("fail to load config from file. filePath=" + path, e);
+		}
+	}
+
+}

+ 78 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/SpringCloudInterceptor.java

@@ -0,0 +1,78 @@
+package cn.com.qmth.examcloud.commons.web.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.web.cloud.api.CloudService;
+import cn.com.qmth.examcloud.commons.web.redis.RedisClient;
+import cn.com.qmth.examcloud.commons.web.support.ServletUtil;
+import cn.com.qmth.examcloud.commons.web.support.StatusResponse;
+
+/**
+ * spring cloud 请求接入
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public final class SpringCloudInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory
+			.getLog(SpringCloudInterceptor.class);
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	/**
+	 * redis client
+	 */
+	private RedisClient redisClient;
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		LOG.debug("preHandle... ...");
+
+		Class<?> ctrClass = (Class<?>) request.getAttribute("$ctrClass");
+		if (!CloudService.class.isAssignableFrom(ctrClass)) {
+			return true;
+		}
+
+		Long timestamp = redisClient.get("$_RMI_:" + ThreadLocalUtil.getTraceID(), Long.class);
+
+		if (null == timestamp) {
+			response.setStatus(HttpStatus.REQUEST_TIMEOUT.value());
+			ServletUtil.returnJson(new StatusResponse("408", "Request Timeout"), response);
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+			ModelAndView modelAndView) throws Exception {
+		LOG.debug("postHandle... ...");
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+		LOG.debug("afterCompletion... ...");
+	}
+
+	public void setRedisClient(RedisClient redisClient) {
+		this.redisClient = redisClient;
+	}
+
+}

+ 64 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/Role.java

@@ -0,0 +1,64 @@
+package cn.com.qmth.examcloud.commons.web.security.bean;
+
+/**
+ * 角色
+ *
+ * @author WANGWEI
+ * @date 2018年5月23日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class Role {
+
+	private Long roleId;
+
+	private String roleCode;
+
+	private String roleName;
+
+	/**
+	 * 构造函数
+	 *
+	 */
+	public Role() {
+		super();
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param roleId
+	 * @param roleCode
+	 * @param roleName
+	 */
+	public Role(Long roleId, String roleCode, String roleName) {
+		super();
+		this.roleId = roleId;
+		this.roleCode = roleCode;
+		this.roleName = roleName;
+	}
+
+	public Long getRoleId() {
+		return roleId;
+	}
+
+	public void setRoleId(Long roleId) {
+		this.roleId = roleId;
+	}
+
+	public String getRoleCode() {
+		return roleCode;
+	}
+
+	public void setRoleCode(String roleCode) {
+		this.roleCode = roleCode;
+	}
+
+	public String getRoleName() {
+		return roleName;
+	}
+
+	public void setRoleName(String roleName) {
+		this.roleName = roleName;
+	}
+
+}

+ 149 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/User.java

@@ -0,0 +1,149 @@
+package cn.com.qmth.examcloud.commons.web.security.bean;
+
+import java.util.Date;
+import java.util.List;
+
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+
+/**
+ * 用户
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class User implements JsonSerializable {
+
+	private static final long serialVersionUID = 8766713125414955078L;
+
+	/**
+	 * 全局唯一用户标识符
+	 */
+	private String key;
+
+	/**
+	 * 用户类型
+	 */
+	private UserType userType;
+
+	/**
+	 * 用户ID(包含普通用户ID,学生用户ID)
+	 */
+	private Long userId;
+
+	/**
+	 * 显示名
+	 */
+	private String displayName;
+
+	/**
+	 * 顶级机构ID
+	 */
+	private Long rootOrgId;
+
+	/**
+	 * 顶级机构名称
+	 */
+	private String rootOrgName;
+
+	/**
+	 * 鉴权token创建时间
+	 */
+	private Date tokenCreationTime;
+
+	/**
+	 * 角色集合
+	 */
+	private List<Role> roleList;
+
+	/**
+	 * 客户端IP
+	 */
+	private String clientIp;
+
+	/**
+	 * 鉴权token
+	 */
+	private String token;
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public UserType getUserType() {
+		return userType;
+	}
+
+	public void setUserType(UserType userType) {
+		this.userType = userType;
+	}
+
+	public Long getUserId() {
+		return userId;
+	}
+
+	public void setUserId(Long userId) {
+		this.userId = userId;
+	}
+
+	public String getDisplayName() {
+		return displayName;
+	}
+
+	public void setDisplayName(String displayName) {
+		this.displayName = displayName;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public String getRootOrgName() {
+		return rootOrgName;
+	}
+
+	public void setRootOrgName(String rootOrgName) {
+		this.rootOrgName = rootOrgName;
+	}
+
+	public Date getTokenCreationTime() {
+		return tokenCreationTime;
+	}
+
+	public void setTokenCreationTime(Date tokenCreationTime) {
+		this.tokenCreationTime = tokenCreationTime;
+	}
+
+	public List<Role> getRoleList() {
+		return roleList;
+	}
+
+	public void setRoleList(List<Role> roleList) {
+		this.roleList = roleList;
+	}
+
+	public String getClientIp() {
+		return clientIp;
+	}
+
+	public void setClientIp(String clientIp) {
+		this.clientIp = clientIp;
+	}
+
+	public String getToken() {
+		return token;
+	}
+
+	public void setToken(String token) {
+		this.token = token;
+	}
+
+}

+ 59 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/bean/UserType.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.commons.web.security.bean;
+
+/**
+ * 用户类型
+ *
+ * @author WANGWEI
+ * @date 2018年5月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum UserType {
+
+	/**
+	 * 学生
+	 */
+	STUDENT("S", "学生"),
+
+	/**
+	 * 常规用户
+	 */
+	COMMON("C", "常规用户");
+
+	// ===========================================================================
+
+	/**
+	 * 码
+	 */
+	private String code;
+
+	/**
+	 * 描述
+	 */
+	private String desc;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param code
+	 * @param desc
+	 */
+	private UserType(String code, String desc) {
+		this.code = code;
+		this.desc = desc;
+	}
+
+	/**
+	 * @return the code
+	 */
+	public String getCode() {
+		return code;
+	}
+
+	/**
+	 * @return the desc
+	 */
+	public String getDesc() {
+		return desc;
+	}
+
+}

+ 49 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/security/enums/RoleMeta.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.commons.web.security.enums;
+
+/**
+ * Created by songyue on 17/2/24.
+ */
+public enum RoleMeta {
+
+	SUPER_ADMIN("超级管理员"),
+
+	ORG_ADMIN("机构管理员"),
+
+	LC_USER("学习中心用户"),
+
+	MARKING_ADMIN("阅卷管理员"),
+
+	MARKER("评卷员"),
+
+	QUESTION_ADMIN("题库管理员"),
+
+	OE_ADMIN("网考管理员"),
+
+	PRINT_SUPPLIER("印刷供应商"),
+
+	PRINT_SCHOOL_LEADER("印刷学校管理员"),
+
+	PRINT_SUPER_LEADER("印刷总负责人"),
+
+	PRINT_PROJECT_LEADER("项目经理"),
+
+	PRINT_LOCALE_LEADER("印刷现场负责人"),
+
+	PRINT_SALE_LEADER("销售负责人"),
+
+	PRINT_FINANCE_LEADER("财务负责人");
+
+	/**
+	 * 角色名
+	 */
+	private String name;
+
+	RoleMeta(String name) {
+		this.name = name;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+}

+ 189 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/CloudClientSupport.java

@@ -0,0 +1,189 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import cn.com.qmth.examcloud.commons.base.exception.StatusException;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.StringUtil;
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.web.cloud.api.BaseRequest;
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+import cn.com.qmth.examcloud.commons.web.redis.RedisClient;
+
+/**
+ * 云服务客户端基类
+ * 
+ * @author WANGWEI
+ *
+ */
+public abstract class CloudClientSupport {
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	protected abstract RestTemplate getRestTemplate();
+
+	protected abstract RedisClient getRedisClient();
+
+	protected abstract String getUrlPrefix();
+
+	/**
+	 * 获取响应体
+	 * 
+	 * @author WANGWEI
+	 * @param respEntity
+	 * @return
+	 */
+	private <T> T getRespBody(ResponseEntity<String> respEntity, Class<T> responseType) {
+
+		String body = respEntity.getBody();
+		if (HttpStatus.OK == respEntity.getStatusCode()) {
+			if (null == responseType) {
+				return null;
+			}
+			T t = JsonUtil.fromJson(body, responseType);
+			return t;
+		} else {
+			StatusResponse sr = JsonUtil.fromJson(body, StatusResponse.class);
+			throw new StatusException(sr.getCode(), sr.getDesc());
+		}
+	}
+
+	/**
+	 * 构建url
+	 * 
+	 * @param urlSuffix
+	 * @return
+	 */
+	private String buildUrl(String urlSuffix) {
+		String prefix = getUrlPrefix();
+		prefix = prefix.endsWith("/") ? prefix : prefix + "/";
+		urlSuffix = urlSuffix.startsWith("/") ? urlSuffix.substring(1) : urlSuffix;
+		String url = prefix + urlSuffix;
+		return url;
+	}
+
+	/**
+	 * exchange
+	 *
+	 * @author WANGWEI
+	 * @param url
+	 * @param method
+	 * @param body
+	 * @param responseType
+	 * @return
+	 */
+	protected <T> T exchange(String url, HttpMethod method, Object body, Class<T> responseType) {
+
+		long startTime = System.currentTimeMillis();
+
+		HttpHeaders requestHeaders = new HttpHeaders();
+		requestHeaders.add("TRACE_ID", ThreadLocalUtil.getTraceID());
+		requestHeaders.add("$spring_cloud_client", "_");
+		getRedisClient().set("$_RMI_:" + ThreadLocalUtil.getTraceID(), System.currentTimeMillis(),
+				10);
+
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			INTERFACE_LOG.info("[CALL-IN]. url=" + url);
+			if (body instanceof JsonSerializable) {
+				INTERFACE_LOG.info("[CALL-REQ]. request=" + JsonUtil.toJson(body));
+			}
+		}
+
+		HttpEntity<Object> requestEntity = new HttpEntity<Object>(body, requestHeaders);
+
+		T respBody = null;
+		try {
+			ResponseEntity<String> respEntity = getRestTemplate().exchange(url, method,
+					requestEntity, String.class);
+			respBody = getRespBody(respEntity, responseType);
+
+			String msg = StringUtil.join("[CALL-OK]. url=" + url,
+					" ; cost " + (System.currentTimeMillis() - startTime)) + " ms.";
+			INTERFACE_LOG.info(msg);
+			INTERFACE_LOG.info("[CALL-RESP]. response=" + respEntity.getBody());
+		} catch (StatusException e) {
+			String msg = StringUtil.join("[CALL-FAIL]. url=" + url,
+					" ; cost " + (System.currentTimeMillis() - startTime)) + " ms.";
+			INTERFACE_LOG.error(msg);
+			INTERFACE_LOG.error("[CALL-RESP]. response=" + e.toJson());
+			throw e;
+		} catch (Exception e) {
+			String msg = StringUtil.join("[CALL-FATAL]. url=" + url, " ;", e.getMessage());
+			INTERFACE_LOG.error(msg);
+			throw e;
+		}
+
+		return respBody;
+	}
+
+	/**
+	 * post请求
+	 *
+	 * @author WANGWEI
+	 * @param urlSuffix
+	 * @param body
+	 * @param responseType
+	 * @return
+	 */
+	protected <T> T post(String urlSuffix, BaseRequest body, Class<T> responseType) {
+		String url = buildUrl(urlSuffix);
+		return exchange(url, HttpMethod.POST, body, responseType);
+	}
+
+	/**
+	 * post请求
+	 * 
+	 * @param urlSuffix
+	 * @param body
+	 */
+	protected void post(String urlSuffix, BaseRequest body) {
+		String url = buildUrl(urlSuffix);
+		exchange(url, HttpMethod.POST, body, null);
+	}
+
+	/**
+	 * 文件表单提交
+	 *
+	 * @author WANGWEI
+	 * @param urlSuffix
+	 * @param params
+	 * @param file
+	 * @param responseType
+	 * @return
+	 * @throws Exception
+	 */
+	public <T> T postForm(String urlSuffix, Map<String, String> params, File file,
+			Class<T> responseType) {
+		String url = buildUrl(urlSuffix);
+
+		MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
+		FileSystemResource resource = new FileSystemResource(file);
+		param.add("file", resource);
+		param.add("fileName", file.getName());
+
+		for (Entry<String, String> entry : params.entrySet()) {
+			param.add(entry.getKey(), entry.getValue());
+		}
+
+		return exchange(url, HttpMethod.POST, param, responseType);
+	}
+
+}

+ 201 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/ControllerAspect.java

@@ -0,0 +1,201 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+import ch.qos.logback.classic.Logger;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.ObjectUtil;
+import cn.com.qmth.examcloud.commons.base.util.StringUtil;
+import cn.com.qmth.examcloud.commons.base.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.web.cloud.api.BaseResponse;
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+import cn.com.qmth.examcloud.commons.web.reports.BaseReport;
+import cn.com.qmth.examcloud.commons.web.reports.ReportLoggerFactory;
+
+/**
+ * spring mvc controller aspect.
+ *
+ * @author WANG WEI
+ *
+ */
+@Component
+@Aspect
+public class ControllerAspect {
+
+	/**
+	 * 接口日志
+	 */
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	public ControllerAspect() {
+		System.out.println("=================ControllerAspect=================");
+	}
+
+	/**
+	 * handlerMethods
+	 *
+	 * @author WANGWEI
+	 */
+	@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) "
+			+ "|| @annotation(org.springframework.web.bind.annotation.GetMapping) "
+			+ "|| @annotation(org.springframework.web.bind.annotation.PostMapping)"
+			+ "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
+			+ "|| @annotation(org.springframework.web.bind.annotation.PutMapping)")
+	public void handlerMethods() {
+
+	}
+
+	/**
+	 * 接口处理
+	 *
+	 * @author WANG WEI
+	 *
+	 * @param joinPoint
+	 * @return
+	 */
+	@Around("handlerMethods()")
+	public Object doAroundWebRequests(ProceedingJoinPoint joinPoint) {
+		long startTime = System.currentTimeMillis();
+
+		// 获取request对象
+		HttpServletRequest request = ServletUtil.getRequest();
+
+		String path = request.getServletPath();
+		String method = request.getMethod();
+
+		// 请求信息
+		String inMsg = StringUtil.join("[HTTP-IN]. path=\"", path, "\", method=[", method, "]");
+		INTERFACE_LOG.info(inMsg);
+
+		Object[] args = joinPoint.getArgs();
+
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			if (null != args) {
+				if (1 == args.length && args[0] instanceof JsonSerializable) {
+					INTERFACE_LOG.info("[HTTP-REQ]. request=" + JsonUtil.toJson(args[0]));
+				} else if (1 <= args.length) {
+					StringBuilder sb = new StringBuilder();
+					sb.append("[HTTP-REQ]. args: ");
+					for (int i = 0; i < args.length; i++) {
+						Object curArg = args[i];
+						sb.append("args[").append(i).append("]=");
+						if (null == curArg) {
+							sb.append("null");
+						} else if (curArg instanceof JsonSerializable) {
+							sb.append(JsonUtil.toJson(curArg));
+						} else if (ObjectUtil.isBaseDataType(curArg.getClass())) {
+							sb.append(curArg);
+						}
+						sb.append("; ");
+					}
+
+					INTERFACE_LOG.info(sb.toString());
+				}
+			}
+		}
+
+		Object ret = null;
+		boolean ok = false;
+		try {
+
+			// 执行
+			ret = joinPoint.proceed();
+			ok = true;
+			long cost = System.currentTimeMillis() - startTime;
+
+			String outMsg = StringUtil.join("[HTTP-OK]. path=\"", path, "\", method=[", method,
+					"] ; cost ", cost, " ms.");
+
+			INTERFACE_LOG.info(outMsg);
+
+			if (null != ret && BaseResponse.class.isAssignableFrom(ret.getClass())) {
+				BaseResponse baseResponse = (BaseResponse) ret;
+				baseResponse.setCost(cost);
+			}
+
+		} catch (Throwable e) {
+			String exceptionMsg = StringUtil.join("[HTTP-FAIL]. path=\"", path, "\", method=[",
+					method, "] ; cost ", System.currentTimeMillis() - startTime, " ms.");
+			INTERFACE_LOG.error(exceptionMsg);
+			throw new RuntimeException(e);
+		} finally {
+			Boolean hasCollected = (Boolean) ThreadLocalUtil.get("$_HAS_COLLECTED");
+			if (null != hasCollected && hasCollected) {
+				String key = "$_REPORT_COLLECTIONS";
+				@SuppressWarnings("unchecked")
+				List<BaseReport> reportList = (List<BaseReport>) ThreadLocalUtil.get(key);
+				for (BaseReport report : reportList) {
+					Boolean ignoreException = report.getIgnoreException();
+					if (null == ignoreException) {
+						ignoreException = false;
+					}
+					if (ok || ignoreException) {
+						Logger logger = ReportLoggerFactory.getLogger(report.getClass());
+						report.setReportTime(new Date());
+						report.setHasException(!ok);
+						logger.info(JsonUtil.toJson(report) + "\n");
+					}
+				}
+			}
+		}
+
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			boolean jsonSerializable = false;
+			if (null == ret) {
+				INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
+				INTERFACE_LOG.info("[HTTP-RESP]. response=void");
+			} else if (ret instanceof ResponseEntity) {
+				ResponseEntity<?> re = (ResponseEntity<?>) ret;
+				Object body = re.getBody();
+				if (null != body) {
+					if (body instanceof JsonSerializable) {
+						jsonSerializable = true;
+					} else if (body instanceof Collection) {
+						jsonSerializable = true;
+					} else if (body instanceof Map) {
+						jsonSerializable = true;
+					} else if (body instanceof String) {
+						jsonSerializable = true;
+					}
+					INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + re.getStatusCodeValue());
+					if (jsonSerializable) {
+						INTERFACE_LOG.info("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+					}
+				}
+			} else {
+				if (ret instanceof JsonSerializable) {
+					jsonSerializable = true;
+				} else if (ret instanceof Collection) {
+					jsonSerializable = true;
+				} else if (ret instanceof Map) {
+					jsonSerializable = true;
+				} else if (ret instanceof String) {
+					jsonSerializable = true;
+				}
+				INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
+				if (jsonSerializable) {
+					INTERFACE_LOG.info("[HTTP-RESP]. response=" + JsonUtil.toJson(ret));
+				}
+			}
+		}
+
+		return ret;
+	}
+
+}

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

@@ -0,0 +1,333 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import com.google.common.collect.Lists;
+
+import cn.com.qmth.examcloud.commons.base.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.base.exception.StatusException;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.ObjectUtil;
+import cn.com.qmth.examcloud.commons.web.security.bean.Role;
+import cn.com.qmth.examcloud.commons.web.security.bean.User;
+import cn.com.qmth.examcloud.commons.web.security.enums.RoleMeta;
+
+/**
+ * contorller 基类
+ * 
+ * @author wang wei
+ * @date 2018年4月4日
+ */
+public abstract class ControllerSupport {
+
+	/**
+	 * controller 统一业务日志对象
+	 */
+	protected ExamCloudLog log = ExamCloudLogFactory.getLog(this.getClass());
+
+	/**
+	 * 获取接入用户
+	 * 
+	 * @return
+	 */
+	protected User getAccessUser() {
+		User accessUser = (User) ServletUtil.getRequest().getAttribute("$accessUser");
+		if (null == accessUser) {
+			throw new StatusException("252", "请重新登陆");
+		}
+		return accessUser;
+	}
+
+	/**
+	 * 设置http响应码总是为200
+	 *
+	 * @author WANGWEI
+	 */
+	protected void setAlwaysOKResponse() {
+		ServletUtil.getRequest().setAttribute("$ALWAYS_OK", true);
+	}
+
+	/**
+	 * 获取安全接入的顶级机构(非session状态)
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected Long getSecurityRootOrgId() {
+		Long rootOrgId = (Long) getRequest().getAttribute("$rootOrgId");
+		if (null == rootOrgId) {
+			throw new StatusException("280", "安全接入的顶级机构ID为空");
+		}
+		return rootOrgId;
+	}
+
+	/**
+	 * 获取顶级机构(session状态)
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected Long getRootOrgId() {
+		Long rootOrgId = getAccessUser().getRootOrgId();
+		return rootOrgId;
+	}
+
+	/**
+	 * 判断是否超级管理员
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected boolean isSuperAdmin() {
+		User accessUser = getAccessUser();
+		List<Role> roleList = accessUser.getRoleList();
+		for (Role role : roleList) {
+			if (role.getRoleCode().equals(RoleMeta.SUPER_ADMIN.name())) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * 验证顶级机构隔离
+	 *
+	 * @author WANGWEI
+	 * @param rootOrgId
+	 */
+	protected void validateRootOrgIsolation(Long rootOrgId) {
+		if ((!isSuperAdmin()) && (!rootOrgId.equals(getRootOrgId()))) {
+			throw new StatusException("250", "非法请求");
+		}
+	}
+
+	/**
+	 * 获取接入用户的角色ID集合
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected List<Long> getAccessUserRoleIdList() {
+		List<Role> roleList = getAccessUser().getRoleList();
+		List<Long> roleIdList = Lists.newArrayList();
+		for (Role cur : roleList) {
+			roleIdList.add(cur.getRoleId());
+		}
+		return roleIdList;
+	}
+
+	/**
+	 * 获取request对象
+	 * 
+	 * @return
+	 */
+	protected HttpServletRequest getRequest() {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletRequest request = requestAttributes.getRequest();
+		return request;
+	}
+
+	/**
+	 * 获取response对象
+	 * 
+	 * @return
+	 */
+	protected HttpServletResponse getResponse() {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletResponse response = requestAttributes.getResponse();
+		return response;
+	}
+
+	/**
+	 * 文件导出
+	 *
+	 * @author WANGWEI
+	 * @param fileName
+	 * @param file
+	 */
+	protected void exportFile(String fileName, File file) {
+		OutputStream out = null;
+		InputStream in = null;
+		try {
+			in = new FileInputStream(file);
+			fileName = URLEncoder.encode(fileName, "UTF-8");
+			HttpServletResponse response = getResponse();
+			response.reset();
+			response.setHeader("Content-Disposition", "inline; filename=" + fileName);
+			response.addHeader("Content-Length", "" + file.length());
+			response.setContentType("application/octet-stream;charset=UTF-8");
+			out = new BufferedOutputStream(response.getOutputStream());
+			IOUtils.copy(in, out);
+			out.flush();
+		} catch (IOException e) {
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(out);
+			IOUtils.closeQuietly(in);
+		}
+	}
+
+	/**
+	 * 文件导出
+	 *
+	 * @author WANGWEI
+	 * @param fileName
+	 * @param bytes
+	 */
+	protected void exportFile(String fileName, byte[] bytes) {
+		OutputStream out = null;
+		try {
+			fileName = URLEncoder.encode(fileName, "UTF-8");
+			HttpServletResponse response = getResponse();
+			response.reset();
+			response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+			response.addHeader("Content-Length", "" + bytes.length);
+			response.setContentType("application/octet-stream;charset=UTF-8");
+			out = new BufferedOutputStream(response.getOutputStream());
+			out.write(bytes);
+			out.flush();
+		} catch (IOException e) {
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(out);
+		}
+	}
+
+	/**
+	 * 转换为数据库模糊查询匹配模式
+	 *
+	 * @author WANGWEI
+	 * @param column
+	 * @return
+	 */
+	protected String toSqlSearchPattern(String column) {
+		if (StringUtils.isBlank(column)) {
+			return "%";
+		} else {
+			column = column.trim().replaceAll("\\s", "%");
+			column = "%" + column + "%";
+			return column;
+		}
+	}
+
+	/**
+	 * 获取非空参数
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @return
+	 */
+	protected String getRequiredStringParam(String s) {
+		if (StringUtils.isBlank(s)) {
+			throw new StatusException("520", "参数为空");
+		} else {
+			return s.trim();
+		}
+	}
+
+	/**
+	 * 获取参数
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @return
+	 */
+	protected String getStringParam(String s) {
+		if (StringUtils.isBlank(s)) {
+			return null;
+		} else {
+			return s.trim();
+		}
+	}
+
+	/**
+	 * 参数trim.<br>
+	 * set null if blank.
+	 *
+	 * @author WANGWEI
+	 * @param bean
+	 */
+	protected void trim(Object bean) {
+		trim(bean, false);
+	}
+
+	/**
+	 * 参数trim
+	 *
+	 * @author WANGWEI
+	 * @param bean
+	 * @param nullIfBlank
+	 */
+	protected void trim(Object bean, boolean nullIfBlank) {
+		if (null == bean) {
+			return;
+		}
+		Class<? extends Object> clazz = bean.getClass();
+		if (clazz.equals(Map.class)) {
+			return;
+		} else if (clazz.equals(List.class)) {
+			return;
+		} else if (clazz.equals(Set.class)) {
+			return;
+		} else if (clazz.isEnum()) {
+			return;
+		} else if (ObjectUtil.isBaseDataType(clazz)) {
+			return;
+		}
+
+		Field[] fields = clazz.getDeclaredFields();
+		try {
+			for (int i = 0; i < fields.length; i++) {
+				Field f = fields[i];
+				f.setAccessible(true);
+				Object value = f.get(bean);
+				if (null == value) {
+					continue;
+				}
+				if (f.getType().equals(String.class)) {
+					if (null != value) {
+						String s = (String) value;
+						s = s.trim();
+						if (nullIfBlank && StringUtils.isBlank(s)) {
+							s = null;
+						}
+						f.set(bean, s);
+					}
+				} else if (f.getType().equals(Map.class)) {
+				} else if (f.getType().equals(List.class)) {
+				} else if (f.getType().equals(Set.class)) {
+				} else if (f.getType().isEnum()) {
+				} else if (ObjectUtil.isBaseDataType(f.getType())) {
+				} else {
+					trim(value);
+				}
+
+			}
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException(e);
+		}
+	}
+
+}

+ 163 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/CustomExceptionHandler.java

@@ -0,0 +1,163 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import cn.com.qmth.examcloud.commons.base.exception.StatusException;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.base.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.base.util.RegExpUtil;
+
+/**
+ * 类注释
+ *
+ * @author WANGWEI
+ * @date 2018年8月14日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@ControllerAdvice
+@ResponseBody
+public class CustomExceptionHandler {
+
+	/**
+	 * 接口日志
+	 */
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private static ExamCloudLog LOG = ExamCloudLogFactory.getLog(CustomExceptionHandler.class);
+
+	/**
+	 * 不输出堆栈的状态码
+	 */
+	private static Set<String> nonStackTraceStatusCodes = new HashSet<>();
+
+	static {
+		String str = PropertiesUtil.getString("$log.nonStackTraceStatusCodes");
+		if (StringUtils.isNotBlank(str)) {
+			List<String> list = RegExpUtil.findAll(str, "[\\w\\-]+");
+			CollectionUtils.addAll(nonStackTraceStatusCodes, list);
+			LOG.info("nonStackTraceStatusCodes: " + JsonUtil.toJson(nonStackTraceStatusCodes));
+		}
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(RuntimeException.class)
+	public ResponseEntity<StatusResponse> handleException(RuntimeException e,
+			HttpServletRequest request) {
+
+		Throwable cause = e.getCause();
+		StatusResponse body = null;
+
+		if (null != cause && cause instanceof StatusException) {
+			StatusException se = (StatusException) cause;
+			body = new StatusResponse(se.getCode(), se.getDesc());
+		} else if (null != cause && cause instanceof DataIntegrityViolationException) {
+			body = new StatusResponse(ResponseStatus.DATA_INTEGRITY_VIOLATION.getCode(),
+					ResponseStatus.DATA_INTEGRITY_VIOLATION.getDesc());
+		} else if (null != cause && cause instanceof IllegalStateException) {
+			IllegalStateException se = (IllegalStateException) cause;
+			body = new StatusResponse(ResponseStatus.FATAL_ERROR.getCode(), se.getMessage());
+		} else {
+			body = new StatusResponse(ResponseStatus.FATAL_ERROR.getCode(),
+					ResponseStatus.FATAL_ERROR.getDesc());
+			cause = e;
+		}
+
+		boolean alwaysOK = alwaysOK(request);
+		if (alwaysOK) {
+			INTERFACE_LOG.error("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
+			if (nonStackTraceStatusCodes.contains(body.getCode())) {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+			} else {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body), cause);
+			}
+			return new ResponseEntity<StatusResponse>(body, HttpStatus.OK);
+		} else {
+			INTERFACE_LOG.error("[HTTP-RESP]. StatusCode=" + HttpStatus.INTERNAL_SERVER_ERROR);
+			if (nonStackTraceStatusCodes.contains(body.getCode())) {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+			} else {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body), cause);
+			}
+			return new ResponseEntity<StatusResponse>(body, HttpStatus.INTERNAL_SERVER_ERROR);
+		}
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(Exception.class)
+	public ResponseEntity<StatusResponse> handleException(Exception e, HttpServletRequest request) {
+
+		StatusResponse body = new StatusResponse(
+				cn.com.qmth.examcloud.commons.web.support.ResponseStatus.FATAL_ERROR.getCode(),
+				cn.com.qmth.examcloud.commons.web.support.ResponseStatus.FATAL_ERROR.getDesc());
+
+		boolean alwaysOK = alwaysOK(request);
+		if (alwaysOK) {
+			INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
+			if (nonStackTraceStatusCodes.contains(body.getCode())) {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+			} else {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body), e);
+			}
+			return new ResponseEntity<StatusResponse>(body, HttpStatus.OK);
+		} else {
+			INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.INTERNAL_SERVER_ERROR);
+			if (nonStackTraceStatusCodes.contains(body.getCode())) {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+			} else {
+				INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body), e);
+			}
+			return new ResponseEntity<StatusResponse>(body, HttpStatus.INTERNAL_SERVER_ERROR);
+		}
+	}
+
+	/**
+	 * 是否总是响应200
+	 *
+	 * @author WANGWEI
+	 * @param request
+	 * @return
+	 */
+	private boolean alwaysOK(HttpServletRequest request) {
+		boolean alwaysOK = false;
+		Object attribute = request.getAttribute("$ALWAYS_OK");
+		if (null != attribute) {
+			try {
+				alwaysOK = (boolean) attribute;
+			} catch (Exception ex) {
+				// ignore
+			}
+		}
+		return alwaysOK;
+	}
+
+}

+ 28 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/CustomResponseErrorHandler.java

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.io.IOException;
+
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.web.client.ResponseErrorHandler;
+
+/**
+ * 定制版<br>
+ * 不抛出异常,也不处理错误
+ *
+ * @author WANGWEI
+ * @date 2018年5月26日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class CustomResponseErrorHandler implements ResponseErrorHandler {
+
+	@Override
+	public boolean hasError(ClientHttpResponse response) throws IOException {
+		return false;
+	}
+
+	@Override
+	public void handleError(ClientHttpResponse response) throws IOException {
+
+	}
+
+}

+ 42 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/RemoteProcedureCallTester.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+
+/**
+ * 远程调用测试
+ *
+ * @author WANGWEI
+ * @date 2018年11月29日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class RemoteProcedureCallTester {
+
+	protected ExamCloudLog log = ExamCloudLogFactory.getLog(RemoteProcedureCallTester.class);
+
+	@Autowired
+	RestTemplate restTemplate;
+
+	public void testRestTemplate(String... appNames) {
+		for (String appName : appNames) {
+			try {
+				log.info("[test RestTemplate].  appName=" + appName);
+				ResponseEntity<String> entity = restTemplate.getForEntity("http://" + appName,
+						String.class);
+				log.info("[test RestTemplate].  statusCode=" + entity.getStatusCodeValue());
+			} catch (Exception e) {
+				log.error("[test RestTemplate]. \n" + "WARN:\n"
+						+ "*********************************************************************\n"
+						+ e.getMessage() + "\n"
+						+ "*********************************************************************\n");
+			}
+		}
+	}
+
+}

+ 69 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/ResponseStatus.java

@@ -0,0 +1,69 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+/**
+ * 全局响应状态
+ *
+ * @author WANGWEI
+ * @date 2018年5月26日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum ResponseStatus {
+
+	/**
+	 * 成功
+	 */
+	OK("200", "成功"),
+
+	/**
+	 * 系统异常
+	 */
+	SERVER_ERROR("500", "系统异常"),
+
+	/**
+	 * 违反数据完整性约束
+	 */
+	DATA_INTEGRITY_VIOLATION("520", "违反数据完整性约束"),
+
+	/**
+	 * 致命错误
+	 */
+	FATAL_ERROR("555", "系统繁忙");
+
+	// ===========================================================================
+
+	/**
+	 * 码
+	 */
+	private String code;
+
+	/**
+	 * 描述
+	 */
+	private String desc;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param code
+	 * @param desc
+	 */
+	private ResponseStatus(String code, String desc) {
+		this.code = code;
+		this.desc = desc;
+	}
+
+	/**
+	 * @return the code
+	 */
+	public String getCode() {
+		return code;
+	}
+
+	/**
+	 * @return the desc
+	 */
+	public String getDesc() {
+		return desc;
+	}
+
+}

+ 39 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/RuntimeController.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.google.common.collect.Maps;
+
+/**
+ * 运行时系统信息
+ *
+ * @author WANGWEI
+ * @date 2018年7月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController
+@RequestMapping("runtime")
+public class RuntimeController extends ControllerSupport {
+
+	@Value("${spring.application.name}")
+	private String application;
+
+	@GetMapping
+	public void getInfo() {
+		Map<String, Object> infos = Maps.newLinkedHashMap();
+		infos.put("应用名称", application.toUpperCase());
+
+		infos.put("CPU个数", Runtime.getRuntime().availableProcessors());
+		infos.put("虚拟机内存总量", Runtime.getRuntime().totalMemory());
+		infos.put("虚拟机空闲内存量", Runtime.getRuntime().freeMemory());
+		infos.put("虚拟机使用最大内存量", Runtime.getRuntime().maxMemory());
+
+		ServletUtil.returnJson(infos, getResponse());
+	}
+
+}

+ 110 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/ServletUtil.java

@@ -0,0 +1,110 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.base.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.base.util.JsonUtil;
+
+/**
+ * servlet 工具
+ *
+ * @author WANGWEI
+ * @date 2018年5月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ServletUtil {
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	/**
+	 * 获取request对象
+	 * 
+	 * @return
+	 */
+	public static HttpServletRequest getRequest() {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletRequest request = requestAttributes.getRequest();
+		return request;
+	}
+
+	/**
+	 * 获取response对象
+	 * 
+	 * @return
+	 */
+	public static HttpServletResponse getResponse() {
+		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
+				.getRequestAttributes();
+		HttpServletResponse response = requestAttributes.getResponse();
+		return response;
+	}
+
+	/**
+	 * 输出响应流
+	 *
+	 * @author WANGWEI
+	 * @param respEntity
+	 * @param response
+	 */
+	public static void returnJson(Object body, HttpServletResponse response) {
+
+		response.setContentType("application/json;charset=utf-8");
+		PrintWriter writer = null;
+		try {
+			writer = response.getWriter();
+			String json = "{}";
+			if (null != body) {
+				json = JsonUtil.toJson(body);
+			}
+			writer.write(json);
+
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("[HTTP-RESP]. Response=" + json);
+			}
+		} catch (IOException e) {
+			INTERFACE_LOG.error("fail to write json", e);
+		} finally {
+			IOUtils.closeQuietly(writer);
+		}
+	}
+
+	/**
+	 * 输出响应流(无日志)
+	 *
+	 * @author WANGWEI
+	 * @param respEntity
+	 * @param response
+	 */
+	public static void returnJsonWithoutLog(Object body, HttpServletResponse response) {
+
+		response.setContentType("application/json;charset=utf-8");
+		PrintWriter writer = null;
+		try {
+			writer = response.getWriter();
+			String json = "{}";
+			if (null != body) {
+				json = JsonUtil.toJson(body);
+			}
+			writer.write(json);
+		} catch (IOException e) {
+			// ignore
+		} finally {
+			IOUtils.closeQuietly(writer);
+		}
+	}
+
+}

+ 73 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/SpringContextHolder.java

@@ -0,0 +1,73 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.ResolvableType;
+import org.springframework.stereotype.Component;
+
+/**
+ * spring context holder.
+ *
+ * @author WANGWEI
+ * @date 2018年7月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class SpringContextHolder implements ApplicationContextAware {
+
+	private static ApplicationContext ctx = null;
+
+	@Override
+	public void setApplicationContext(ApplicationContext ctx) {
+		SpringContextHolder.ctx = ctx;
+	}
+
+	public static Object getBean(String name) {
+		return ctx.getBean(name);
+	}
+
+	public static <T> T getBean(String name, Class<T> requiredType) {
+		return ctx.getBean(name, requiredType);
+	}
+
+	public static <T> T getBean(Class<T> requiredType) {
+		return ctx.getBean(requiredType);
+	}
+
+	public static Object getBean(String name, Object... args) {
+		return ctx.getBean(name, args);
+	}
+
+	public static <T> T getBean(Class<T> requiredType, Object... args) {
+		return ctx.getBean(requiredType, args);
+	}
+
+	public static boolean containsBean(String name) {
+		return ctx.containsBean(name);
+	}
+
+	public static boolean isSingleton(String name) {
+		return ctx.isSingleton(name);
+	}
+
+	public static boolean isPrototype(String name) {
+		return ctx.isPrototype(name);
+	}
+
+	public static boolean isTypeMatch(String name, ResolvableType typeToMatch) {
+		return ctx.isTypeMatch(name, typeToMatch);
+	}
+
+	public static boolean isTypeMatch(String name, Class<?> typeToMatch) {
+		return ctx.isTypeMatch(name, typeToMatch);
+	}
+
+	public static Class<?> getType(String name) {
+		return ctx.getType(name);
+	}
+
+	public static String[] getAliases(String name) {
+		return ctx.getAliases(name);
+	}
+
+}

+ 94 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/StatusResponse.java

@@ -0,0 +1,94 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import cn.com.qmth.examcloud.commons.web.cloud.api.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * 状态响应实体
+ *
+ * @author WANGWEI
+ * @date 2018年5月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class StatusResponse implements JsonSerializable {
+
+	private static final long serialVersionUID = 8393074113722405560L;
+
+	@ApiModelProperty(value = "响应编码", example = "B-001001", required = true)
+	private String code;
+
+	@ApiModelProperty(value = "响应描述", example = "密码错误", required = true)
+	private String desc;
+
+	@ApiModelProperty(value = "响应信息.当http响应码非200时,此节点内容为空", example = "", required = false)
+	private Object content;
+
+	/**
+	 * 构造函数
+	 *
+	 */
+	public StatusResponse() {
+		super();
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param responseStatus
+	 * @param content
+	 */
+	public StatusResponse(ResponseStatus responseStatus, Object content) {
+		super();
+		this.code = responseStatus.getCode();
+		this.desc = responseStatus.getDesc();
+		this.content = content;
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param responseStatus
+	 */
+	public StatusResponse(ResponseStatus responseStatus) {
+		super();
+		this.code = responseStatus.getCode();
+		this.desc = responseStatus.getDesc();
+	}
+
+	/**
+	 * 构造函数
+	 *
+	 * @param code
+	 * @param desc
+	 */
+	public StatusResponse(String code, String desc) {
+		super();
+		this.code = code;
+		this.desc = desc;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getDesc() {
+		return desc;
+	}
+
+	public void setDesc(String desc) {
+		this.desc = desc;
+	}
+
+	public Object getContent() {
+		return content;
+	}
+
+	public void setContent(Object content) {
+		this.content = content;
+	}
+
+}

+ 96 - 0
src/main/java/cn/com/qmth/examcloud/commons/web/support/SystemController.java

@@ -0,0 +1,96 @@
+package cn.com.qmth.examcloud.commons.web.support;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.compress.utils.IOUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.commons.base.util.DateUtil;
+import cn.com.qmth.examcloud.commons.base.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.web.interceptor.Seqlock;
+import cn.com.qmth.examcloud.commons.web.interceptor.SessionSeqlock;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * 测试状态.勿修改或删除
+ *
+ * @author WANGWEI
+ * @date 2018年8月23日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController("System")
+@RequestMapping
+public class SystemController extends ControllerSupport {
+
+	@Value("${$test}")
+	private String test;
+
+	@ApiOperation(value = "测试")
+	@GetMapping()
+	public String get() {
+		log.debug("@Value   $test=" + test);
+		log.debug("PropertiesUtil    $test=" + PropertiesUtil.getString("$test"));
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "测试")
+	@PostMapping()
+	public String post() {
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "测试")
+	@PutMapping()
+	public String put() {
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "测试")
+	@DeleteMapping()
+	public String delete() {
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "测试")
+	@PutMapping("uploadFile")
+	public String uploadFile(HttpServletRequest req, HttpServletResponse resp) {
+		ServletInputStream in = null;
+		try {
+			in = req.getInputStream();
+			IOUtils.copy(in, System.out);
+		} catch (IOException e) {
+			e.printStackTrace();
+		} finally {
+			IOUtils.closeQuietly(in);
+		}
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "@Seqlock 测试")
+	@GetMapping("seqlock")
+	@Seqlock
+	public String seqlock() {
+		cn.com.qmth.examcloud.commons.base.util.Util.sleep(TimeUnit.SECONDS, 1);
+		return DateUtil.getNowISO();
+	}
+
+	@ApiOperation(value = "@SessionSeqlock 测试")
+	@GetMapping("sessionSeqlock")
+	@SessionSeqlock
+	public String sessionSeqlock() {
+		cn.com.qmth.examcloud.commons.base.util.Util.sleep(TimeUnit.SECONDS, 1);
+		return DateUtil.getNowISO();
+	}
+
+}

+ 3 - 0
src/main/resources/app.properties

@@ -0,0 +1,3 @@
+app.code=ecs_core,ecs_exam_work,ecs_marking,ecs_oe,ecs_question,ecs_analysis
+app.name='\u57fa\u7840\u4fe1\u606f','\u8003\u52a1','\u9605\u5377','\u7f51\u8003','\u9898\u5e93','\u6210\u7ee9\u5206\u6790'
+app.port=8000,8002,8004,8003,8008,8007

+ 2 - 0
src/main/resources/redis.properties

@@ -0,0 +1,2 @@
+redis.server.ip=127.0.0.1
+redis.port=6379