Răsfoiți Sursa

Merge branch 'branch_other' 4

deason 5 ani în urmă
părinte
comite
9fcbe81828
100 a modificat fișierele cu 10685 adăugiri și 0 ștergeri
  1. 113 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusEndpoint.java
  2. 185 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfo.java
  3. 144 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoCollector.java
  4. 90 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoHolder.java
  5. 286 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/DataReportor.java
  6. 125 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/HistogramInfo.java
  7. 75 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/MeterInfo.java
  8. 14 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/MetricNames.java
  9. 26 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/MetricRegistryHolder.java
  10. 48 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ReportInfo.java
  11. 22 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/ReportorHolder.java
  12. 175 0
      src/main/java/cn/com/qmth/examcloud/web/actuator/TimerInfo.java
  13. 74 0
      src/main/java/cn/com/qmth/examcloud/web/aliyun/AliYunAccount.java
  14. 64 0
      src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSite.java
  15. 136 0
      src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSiteManager.java
  16. 398 0
      src/main/java/cn/com/qmth/examcloud/web/baidu/BaiduClient.java
  17. 322 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/AppBootstrap.java
  18. 122 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/BootstrapSecurityUtil.java
  19. 131 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/PropertyHolder.java
  20. 136 0
      src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java
  21. 24 0
      src/main/java/cn/com/qmth/examcloud/web/cache/DefaultFullObjectCacheWatcher.java
  22. 29 0
      src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCache.java
  23. 28 0
      src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCacheWatcher.java
  24. 115 0
      src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectRedisCache.java
  25. 41 0
      src/main/java/cn/com/qmth/examcloud/web/cache/HashCache.java
  26. 89 0
      src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java
  27. 134 0
      src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheProcessor.java
  28. 109 0
      src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheTrigger.java
  29. 47 0
      src/main/java/cn/com/qmth/examcloud/web/cache/ObjectCache.java
  30. 133 0
      src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheProcessor.java
  31. 90 0
      src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheTrigger.java
  32. 42 0
      src/main/java/cn/com/qmth/examcloud/web/cache/RandomCacheBean.java
  33. 50 0
      src/main/java/cn/com/qmth/examcloud/web/cache/RandomCacheVersionHelper.java
  34. 86 0
      src/main/java/cn/com/qmth/examcloud/web/cache/RandomObjectRedisCache.java
  35. 47 0
      src/main/java/cn/com/qmth/examcloud/web/cache/RefreshCacheReq.java
  36. 76 0
      src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java
  37. 37 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelf.java
  38. 44 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelfHolder.java
  39. 145 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientConfiguration.java
  40. 322 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientSupport.java
  41. 65 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/CloudServiceRedirector.java
  42. 28 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/CustomFileSystemResource.java
  43. 67 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/ExamCloudDiscoveryClient.java
  44. 56 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/RibbonClientsConfiguration.java
  45. 43 0
      src/main/java/cn/com/qmth/examcloud/web/config/LogProperties.java
  46. 98 0
      src/main/java/cn/com/qmth/examcloud/web/config/SystemProperties.java
  47. 44 0
      src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceAutoConfigure.java
  48. 21 0
      src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceBuilder.java
  49. 98 0
      src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceWrapper.java
  50. 72 0
      src/main/java/cn/com/qmth/examcloud/web/druid/RemoveDruidAdConfig.java
  51. 196 0
      src/main/java/cn/com/qmth/examcloud/web/druid/properties/DruidStatProperties.java
  52. 108 0
      src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidFilterConfiguration.java
  53. 33 0
      src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidSpringAopConfiguration.java
  54. 41 0
      src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidStatViewServletConfiguration.java
  55. 48 0
      src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidWebStatFilterConfiguration.java
  56. 62 0
      src/main/java/cn/com/qmth/examcloud/web/enums/HttpServletRequestAttribute.java
  57. 24 0
      src/main/java/cn/com/qmth/examcloud/web/exception/ApiFlowLimitedException.java
  58. 22 0
      src/main/java/cn/com/qmth/examcloud/web/exception/SequenceLockException.java
  59. 425 0
      src/main/java/cn/com/qmth/examcloud/web/facepp/FaceppClient.java
  60. 59 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java
  61. 79 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageHelper.java
  62. 160 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStoragePathEnvInfo.java
  63. 66 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageSite.java
  64. 17 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageType.java
  65. 45 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/YunHttpRequest.java
  66. 47 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/YunPathInfo.java
  67. 499 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java
  68. 127 0
      src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java
  69. 72 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/GlobalHelper.java
  70. 136 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/SequenceLockHelper.java
  71. 95 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/tree/EleTreeNode.java
  72. 27 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/tree/TreeNode.java
  73. 120 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/tree/TreeUtil.java
  74. 59 0
      src/main/java/cn/com/qmth/examcloud/web/helpers/tree/ZtreeNode.java
  75. 309 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiFlowLimitedInterceptor.java
  76. 117 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiStatisticInterceptor.java
  77. 136 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/FirstInterceptor.java
  78. 21 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/GlobalSequenceLock.java
  79. 139 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/SeqlockInterceptor.java
  80. 22 0
      src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSequenceLock.java
  81. 70 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/DataIntegrityViolationTransverter.java
  82. 62 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/JpaEntity.java
  83. 49 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/UniqueRule.java
  84. 32 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdAndStatusJpaEntity.java
  85. 32 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdJpaEntity.java
  86. 46 0
      src/main/java/cn/com/qmth/examcloud/web/jpa/WithStatusJpaEntity.java
  87. 84 0
      src/main/java/cn/com/qmth/examcloud/web/mongodb/MongodbDetector.java
  88. 83 0
      src/main/java/cn/com/qmth/examcloud/web/redis/CustomRedisConfiguration.java
  89. 153 0
      src/main/java/cn/com/qmth/examcloud/web/redis/RedisClient.java
  90. 185 0
      src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java
  91. 154 0
      src/main/java/cn/com/qmth/examcloud/web/security/RequestPermissionInterceptor.java
  92. 46 0
      src/main/java/cn/com/qmth/examcloud/web/security/ResourceManager.java
  93. 189 0
      src/main/java/cn/com/qmth/examcloud/web/security/RpcInterceptor.java
  94. 22 0
      src/main/java/cn/com/qmth/examcloud/web/support/ApiId.java
  95. 121 0
      src/main/java/cn/com/qmth/examcloud/web/support/ApiInfo.java
  96. 177 0
      src/main/java/cn/com/qmth/examcloud/web/support/ApiInfoHolder.java
  97. 28 0
      src/main/java/cn/com/qmth/examcloud/web/support/ClasspathHelper.java
  98. 235 0
      src/main/java/cn/com/qmth/examcloud/web/support/ControllerAspect.java
  99. 350 0
      src/main/java/cn/com/qmth/examcloud/web/support/ControllerSupport.java
  100. 290 0
      src/main/java/cn/com/qmth/examcloud/web/support/CustomExceptionHandler.java

+ 113 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusEndpoint.java

@@ -0,0 +1,113 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
+import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
+import org.springframework.boot.actuate.endpoint.annotation.Selector;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+import cn.com.qmth.examcloud.commons.util.FreeMarkerUtil;
+import cn.com.qmth.examcloud.commons.util.ResourceLoader;
+
+/**
+ * 接口状态
+ *
+ * @author WANGWEI
+ * @date 2019年7月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Configuration
+@Endpoint(id = "api-status")
+public class ApiStatusEndpoint {
+
+	private static AtomicBoolean running = new AtomicBoolean(false);
+
+	@Autowired
+	ApiStatusInfoCollector apiStatusInfoCollector;
+
+	/**
+	 * url: /actuator/api-status
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	@ReadOperation(produces = {MediaType.TEXT_HTML_VALUE})
+	public String getByDefault() {
+		return get("");
+	}
+
+	/**
+	 * url: /actuator/api-status/ignored?order=XX
+	 *
+	 * @author WANGWEI
+	 * @param order
+	 * @return
+	 */
+	@ReadOperation(produces = {MediaType.TEXT_HTML_VALUE})
+	public String get(@Selector String order) {
+
+		if (!running.compareAndSet(false, true)) {
+			return DateUtil.chinaNow() + " | 请求限制";
+		}
+
+		String ret = null;
+		try {
+			List<ApiStatusInfo> list = ApiStatusInfoHolder.getApiStatusInfoList();
+
+			Collections.sort(list, new Comparator<ApiStatusInfo>() {
+				@Override
+				public int compare(ApiStatusInfo o1, ApiStatusInfo o2) {
+
+					try {
+						Field field = o1.getClass().getDeclaredField(order);
+						field.setAccessible(true);
+						Object value1 = field.get(o1);
+						Object value2 = field.get(o2);
+
+						if (null == value1) {
+							return -1;
+						} else if (null == value2) {
+							return 1;
+						} else if (value1 instanceof Long) {
+							return (int) (((Long) value2) - ((Long) value1));
+						} else if (value1 instanceof Double) {
+							return (int) (((Double) value2) - ((Double) value1));
+						} else if (value1 instanceof String) {
+							return ((String) value1).compareToIgnoreCase((String) value2);
+						} else {
+							return 0;
+						}
+					} catch (Exception e) {
+						return 0;
+					}
+				}
+			});
+
+			Map<String, Object> map = Maps.newHashMap();
+			map.put("data", list);
+			map.put("dateTime", new Date());
+
+			String html = ResourceLoader.getResource("api-status.html");
+
+			ret = FreeMarkerUtil.process(html, map);
+
+		} finally {
+			running.compareAndSet(true, false);
+		}
+
+		return ret;
+	}
+
+}

+ 185 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfo.java

@@ -0,0 +1,185 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+public class ApiStatusInfo {
+
+	private String mapping;
+
+	private String description;
+
+	private Long count;
+
+	private double meanRate;
+
+	private double oneMinuteRate;
+
+	private double fiveMinuteRate;
+
+	private double fifteenMinuteRate;
+
+	private double min;
+
+	private double max;
+
+	private double mean;
+
+	private double p50;
+
+	private double p75;
+
+	private double p95;
+
+	private double p98;
+
+	private double p99;
+
+	private Long errorCount;
+
+	private double errorMeanRate;
+
+	private double errorPercent;
+
+	public String getMapping() {
+		return mapping;
+	}
+
+	public void setMapping(String mapping) {
+		this.mapping = mapping;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public Long getCount() {
+		return count;
+	}
+
+	public void setCount(Long count) {
+		this.count = count;
+	}
+
+	public double getMeanRate() {
+		return meanRate;
+	}
+
+	public void setMeanRate(double meanRate) {
+		this.meanRate = meanRate;
+	}
+
+	public double getOneMinuteRate() {
+		return oneMinuteRate;
+	}
+
+	public void setOneMinuteRate(double oneMinuteRate) {
+		this.oneMinuteRate = oneMinuteRate;
+	}
+
+	public double getFiveMinuteRate() {
+		return fiveMinuteRate;
+	}
+
+	public void setFiveMinuteRate(double fiveMinuteRate) {
+		this.fiveMinuteRate = fiveMinuteRate;
+	}
+
+	public double getFifteenMinuteRate() {
+		return fifteenMinuteRate;
+	}
+
+	public void setFifteenMinuteRate(double fifteenMinuteRate) {
+		this.fifteenMinuteRate = fifteenMinuteRate;
+	}
+
+	public double getMin() {
+		return min;
+	}
+
+	public void setMin(double min) {
+		this.min = min;
+	}
+
+	public double getMax() {
+		return max;
+	}
+
+	public void setMax(double max) {
+		this.max = max;
+	}
+
+	public double getMean() {
+		return mean;
+	}
+
+	public void setMean(double mean) {
+		this.mean = mean;
+	}
+
+	public double getP50() {
+		return p50;
+	}
+
+	public void setP50(double p50) {
+		this.p50 = p50;
+	}
+
+	public double getP75() {
+		return p75;
+	}
+
+	public void setP75(double p75) {
+		this.p75 = p75;
+	}
+
+	public double getP95() {
+		return p95;
+	}
+
+	public void setP95(double p95) {
+		this.p95 = p95;
+	}
+
+	public double getP98() {
+		return p98;
+	}
+
+	public void setP98(double p98) {
+		this.p98 = p98;
+	}
+
+	public double getP99() {
+		return p99;
+	}
+
+	public void setP99(double p99) {
+		this.p99 = p99;
+	}
+
+	public Long getErrorCount() {
+		return errorCount;
+	}
+
+	public void setErrorCount(Long errorCount) {
+		this.errorCount = errorCount;
+	}
+
+	public double getErrorMeanRate() {
+		return errorMeanRate;
+	}
+
+	public void setErrorMeanRate(double errorMeanRate) {
+		this.errorMeanRate = errorMeanRate;
+	}
+
+	public double getErrorPercent() {
+		return errorPercent;
+	}
+
+	public void setErrorPercent(double errorPercent) {
+		this.errorPercent = errorPercent;
+	}
+
+}

+ 144 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoCollector.java

@@ -0,0 +1,144 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.util.Calculator;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ApiInfoHolder;
+
+@Component
+public class ApiStatusInfoCollector {
+
+	private static String algorithm;
+
+	{
+		// 耗时直方图算法:U:uniform ; B:biased
+		algorithm = PropertyHolder.getString("$API.statistic.histograms.algorithm", "B");
+		if (!(algorithm.equals("B") || algorithm.equals("U"))) {
+			throw new RuntimeException("histograms algorithm must be 'U' or 'B'");
+		}
+	}
+
+	public synchronized List<ApiStatusInfo> collect(ReportInfo reportInfo) {
+
+		List<HistogramInfo> histogramInfoList = reportInfo.getHistogramInfoList();
+		List<MeterInfo> meterInfoList = reportInfo.getMeterInfoList();
+		List<TimerInfo> timerInfoList = reportInfo.getTimerInfoList();
+
+		Map<String, MeterInfo> apiErrMeterInfoMap = Maps.newHashMap();
+		for (MeterInfo meterInfo : meterInfoList) {
+			String key = meterInfo.getKey();
+			if (key.startsWith(MetricNames.API_ERROR_METER.name())) {
+				String mapping = key.substring(MetricNames.API_ERROR_METER.name().length() + 1);
+				apiErrMeterInfoMap.put(mapping, meterInfo);
+			}
+		}
+
+		Map<String, HistogramInfo> apiHistogramInfoMap = Maps.newHashMap();
+		if (algorithm.equals("U")) {
+			for (HistogramInfo histogramInfo : histogramInfoList) {
+				String key = histogramInfo.getKey();
+				if (key.startsWith(MetricNames.API_HISTOGRAM.name())) {
+					String mapping = key.substring(MetricNames.API_HISTOGRAM.name().length() + 1);
+					apiHistogramInfoMap.put(mapping, histogramInfo);
+				}
+			}
+		}
+
+		Map<String, TimerInfo> apiTimerInfoMap = Maps.newHashMap();
+		if (algorithm.equals("B")) {
+			for (TimerInfo timerInfo : timerInfoList) {
+				String key = timerInfo.getKey();
+				if (key.startsWith(MetricNames.API_TIMER.name())) {
+					String mapping = key.substring(MetricNames.API_TIMER.name().length() + 1);
+					apiTimerInfoMap.put(mapping, timerInfo);
+				}
+			}
+		}
+
+		List<ApiStatusInfo> list = Lists.newArrayList();
+
+		for (MeterInfo cur : meterInfoList) {
+			String key = cur.getKey();
+			if (key.startsWith(MetricNames.API_METER.name())) {
+
+				String mapping = key.substring(MetricNames.API_METER.name().length() + 1);
+
+				ApiStatusInfo apiStatusInfo = new ApiStatusInfo();
+				list.add(apiStatusInfo);
+
+				apiStatusInfo.setMapping(mapping);
+
+				ApiInfo apiInfo = ApiInfoHolder.getApiInfo(mapping);
+				if (null != apiInfo) {
+					apiStatusInfo.setDescription(apiInfo.getDescription());
+				}
+
+				apiStatusInfo.setCount(cur.getCount());
+				apiStatusInfo.setMeanRate(cur.getMeanRate());
+				apiStatusInfo.setOneMinuteRate(cur.getOneMinuteRate());
+				apiStatusInfo.setFiveMinuteRate(cur.getFiveMinuteRate());
+				apiStatusInfo.setFifteenMinuteRate(cur.getFifteenMinuteRate());
+
+				if (algorithm.equals("U")) {
+					HistogramInfo histogramInfo = apiHistogramInfoMap.get(mapping);
+
+					apiStatusInfo.setMin(histogramInfo.getMin());
+					apiStatusInfo.setMax(histogramInfo.getMax());
+					apiStatusInfo.setMean(histogramInfo.getMean());
+
+					apiStatusInfo.setP50(histogramInfo.getP50());
+					apiStatusInfo.setP75(histogramInfo.getP75());
+					apiStatusInfo.setP95(histogramInfo.getP95());
+					apiStatusInfo.setP98(histogramInfo.getP98());
+					apiStatusInfo.setP99(histogramInfo.getP99());
+				}
+
+				if (algorithm.equals("B")) {
+					TimerInfo timerInfo = apiTimerInfoMap.get(mapping);
+
+					apiStatusInfo.setMin(timerInfo.getMin());
+					apiStatusInfo.setMax(timerInfo.getMax());
+					apiStatusInfo.setMean(timerInfo.getMean());
+
+					apiStatusInfo.setP50(timerInfo.getP50());
+					apiStatusInfo.setP75(timerInfo.getP75());
+					apiStatusInfo.setP95(timerInfo.getP95());
+					apiStatusInfo.setP98(timerInfo.getP98());
+					apiStatusInfo.setP99(timerInfo.getP99());
+				}
+
+				MeterInfo apiErrMeterInfo = apiErrMeterInfoMap.get(mapping);
+				if (null == apiErrMeterInfo) {
+					apiStatusInfo.setErrorCount(0L);
+					apiStatusInfo.setErrorMeanRate(0L);
+					apiStatusInfo.setErrorPercent(0D);
+				} else {
+					apiStatusInfo.setErrorCount(apiErrMeterInfo.getCount());
+					apiStatusInfo.setErrorMeanRate(apiErrMeterInfo.getMeanRate());
+					apiStatusInfo.setErrorPercent(
+							getPercent(apiErrMeterInfo.getCount(), cur.getCount()));
+				}
+
+			}
+		}
+
+		return list;
+	}
+
+	private Double getPercent(double v1, double v2) {
+		if (0 == v1 || 0 == v2) {
+			return 0d;
+		}
+		double ret = Calculator.divide(v1, v2, 3);
+		return ret;
+	}
+
+}

+ 90 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoHolder.java

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

+ 286 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/DataReportor.java

@@ -0,0 +1,286 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricAttribute;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+import com.google.common.collect.Lists;
+
+/**
+ * 数据报告
+ *
+ * @author WANGWEI
+ * @date 2019年7月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class DataReportor extends ScheduledReporter {
+
+	public static Builder forRegistry(MetricRegistry registry) {
+		return new Builder(registry);
+	}
+
+	public static class Builder {
+		private final MetricRegistry registry;
+
+		private Clock clock;
+
+		private TimeUnit rateUnit;
+
+		private TimeUnit durationUnit;
+
+		private MetricFilter filter;
+
+		private ScheduledExecutorService executor;
+
+		private boolean shutdownExecutorOnStop;
+
+		private Set<MetricAttribute> disabledMetricAttributes;
+
+		private Builder(MetricRegistry registry) {
+			this.registry = registry;
+			this.clock = Clock.defaultClock();
+			this.rateUnit = TimeUnit.SECONDS;
+			this.durationUnit = TimeUnit.MILLISECONDS;
+			this.filter = MetricFilter.ALL;
+			this.executor = null;
+			this.shutdownExecutorOnStop = true;
+			disabledMetricAttributes = Collections.emptySet();
+		}
+
+		public Builder shutdownExecutorOnStop(boolean shutdownExecutorOnStop) {
+			this.shutdownExecutorOnStop = shutdownExecutorOnStop;
+			return this;
+		}
+
+		public Builder scheduleOn(ScheduledExecutorService executor) {
+			this.executor = executor;
+			return this;
+		}
+
+		public Builder withClock(Clock clock) {
+			this.clock = clock;
+			return this;
+		}
+
+		public Builder convertRatesTo(TimeUnit rateUnit) {
+			this.rateUnit = rateUnit;
+			return this;
+		}
+
+		public Builder convertDurationsTo(TimeUnit durationUnit) {
+			this.durationUnit = durationUnit;
+			return this;
+		}
+
+		public Builder filter(MetricFilter filter) {
+			this.filter = filter;
+			return this;
+		}
+
+		public Builder disabledMetricAttributes(Set<MetricAttribute> disabledMetricAttributes) {
+			this.disabledMetricAttributes = disabledMetricAttributes;
+			return this;
+		}
+
+		public DataReportor build() {
+			return new DataReportor(registry, clock, rateUnit, durationUnit, filter, executor,
+					shutdownExecutorOnStop, disabledMetricAttributes);
+		}
+	}
+
+	private final Clock clock;
+
+	private ReportInfo reportInfo;
+
+	private DataReportor(MetricRegistry registry, Clock clock, TimeUnit rateUnit,
+			TimeUnit durationUnit, MetricFilter filter, ScheduledExecutorService executor,
+			boolean shutdownExecutorOnStop, Set<MetricAttribute> disabledMetricAttributes) {
+		super(registry, "data-reporter", filter, rateUnit, durationUnit, executor,
+				shutdownExecutorOnStop, disabledMetricAttributes);
+		this.clock = clock;
+	}
+
+	@Override
+	@SuppressWarnings("rawtypes")
+	public void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
+			SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters,
+			SortedMap<String, Timer> timers) {
+
+		ReportInfo info = new ReportInfo();
+		info.setDateTime(new Date(clock.getTime()));
+
+		final List<HistogramInfo> histogramInfoList = Lists.newArrayList();
+		final List<TimerInfo> timerInfoList = Lists.newArrayList();
+		final List<MeterInfo> meterInfoList = Lists.newArrayList();
+		info.setHistogramInfoList(histogramInfoList);
+		info.setMeterInfoList(meterInfoList);
+		info.setTimerInfoList(timerInfoList);
+
+		if (!histograms.isEmpty()) {
+			for (Map.Entry<String, Histogram> entry : histograms.entrySet()) {
+				HistogramInfo histogramInfo = getHistogramInfo(entry.getKey(), entry.getValue());
+				histogramInfoList.add(histogramInfo);
+			}
+		}
+
+		if (!meters.isEmpty()) {
+			for (Map.Entry<String, Meter> entry : meters.entrySet()) {
+				MeterInfo meterInfo = getMeterInfo(entry.getKey(), entry.getValue());
+				meterInfoList.add(meterInfo);
+			}
+		}
+
+		if (!timers.isEmpty()) {
+			for (Map.Entry<String, Timer> entry : timers.entrySet()) {
+				TimerInfo timerInfo = getTimerInfo(entry.getKey(), entry.getValue());
+				timerInfoList.add(timerInfo);
+			}
+		}
+
+		reportInfo = info;
+	}
+
+	private MeterInfo getMeterInfo(String key, Meter meter) {
+
+		MeterInfo info = new MeterInfo();
+		info.setKey(key);
+		info.setRateUnit(getRateUnit());
+
+		if (isEnabled(MetricAttribute.COUNT)) {
+			info.setCount(meter.getCount());
+		}
+		if (isEnabled(MetricAttribute.MEAN_RATE)) {
+			info.setMeanRate(convertRate(meter.getMeanRate()));
+		}
+
+		if (isEnabled(MetricAttribute.M1_RATE)) {
+			info.setOneMinuteRate(convertRate(meter.getOneMinuteRate()));
+		}
+		if (isEnabled(MetricAttribute.M5_RATE)) {
+			info.setFiveMinuteRate(convertRate(meter.getFiveMinuteRate()));
+		}
+		if (isEnabled(MetricAttribute.M15_RATE)) {
+			info.setFifteenMinuteRate(convertRate(meter.getFifteenMinuteRate()));
+		}
+
+		return info;
+	}
+
+	private HistogramInfo getHistogramInfo(String key, Histogram histogram) {
+		Snapshot snapshot = histogram.getSnapshot();
+		HistogramInfo info = new HistogramInfo();
+		info.setKey(key);
+
+		if (isEnabled(MetricAttribute.MIN)) {
+			info.setMin(snapshot.getMin());
+		}
+		if (isEnabled(MetricAttribute.MAX)) {
+			info.setMax(snapshot.getMax());
+		}
+		if (isEnabled(MetricAttribute.MEAN)) {
+			info.setMean(snapshot.getMean());
+		}
+
+		if (isEnabled(MetricAttribute.P50)) {
+			info.setP50(snapshot.getMedian());
+		}
+		if (isEnabled(MetricAttribute.P75)) {
+			info.setP75(snapshot.get75thPercentile());
+		}
+		if (isEnabled(MetricAttribute.P95)) {
+			info.setP95(snapshot.get95thPercentile());
+		}
+		if (isEnabled(MetricAttribute.P98)) {
+			info.setP98(snapshot.get98thPercentile());
+		}
+		if (isEnabled(MetricAttribute.P99)) {
+			info.setP99(snapshot.get99thPercentile());
+		}
+		if (isEnabled(MetricAttribute.P999)) {
+			info.setP999(snapshot.get999thPercentile());
+		}
+
+		return info;
+	}
+
+	private TimerInfo getTimerInfo(String key, Timer timer) {
+		final Snapshot snapshot = timer.getSnapshot();
+		TimerInfo info = new TimerInfo();
+		info.setKey(key);
+		info.setRateUnit(getRateUnit());
+		info.setDurationUnit(getDurationUnit());
+
+		if (isEnabled(MetricAttribute.COUNT)) {
+			info.setCount(timer.getCount());
+		}
+		if (isEnabled(MetricAttribute.MEAN_RATE)) {
+			info.setMeanRate(convertRate(timer.getMeanRate()));
+		}
+
+		if (isEnabled(MetricAttribute.M1_RATE)) {
+			info.setOneMinuteRate(convertRate(timer.getOneMinuteRate()));
+		}
+		if (isEnabled(MetricAttribute.M5_RATE)) {
+			info.setFiveMinuteRate(convertRate(timer.getFiveMinuteRate()));
+		}
+		if (isEnabled(MetricAttribute.M15_RATE)) {
+			info.setFifteenMinuteRate(convertRate(timer.getFifteenMinuteRate()));
+		}
+
+		if (isEnabled(MetricAttribute.MIN)) {
+			info.setMin(convertDuration(snapshot.getMin()));
+		}
+		if (isEnabled(MetricAttribute.MAX)) {
+			info.setMax(convertDuration(snapshot.getMax()));
+		}
+		if (isEnabled(MetricAttribute.MEAN)) {
+			info.setMean(convertDuration(snapshot.getMean()));
+		}
+
+		if (isEnabled(MetricAttribute.P50)) {
+			info.setP50(convertDuration(snapshot.getMedian()));
+		}
+		if (isEnabled(MetricAttribute.P75)) {
+			info.setP75(convertDuration(snapshot.get75thPercentile()));
+		}
+		if (isEnabled(MetricAttribute.P95)) {
+			info.setP95(convertDuration(snapshot.get95thPercentile()));
+		}
+		if (isEnabled(MetricAttribute.P98)) {
+			info.setP98(convertDuration(snapshot.get98thPercentile()));
+		}
+		if (isEnabled(MetricAttribute.P99)) {
+			info.setP99(convertDuration(snapshot.get99thPercentile()));
+		}
+		if (isEnabled(MetricAttribute.P999)) {
+			info.setP999(convertDuration(snapshot.get999thPercentile()));
+		}
+
+		return info;
+	}
+
+	private boolean isEnabled(MetricAttribute type) {
+		return !getDisabledMetricAttributes().contains(type);
+	}
+
+	public ReportInfo getReportInfo() {
+		return reportInfo;
+	}
+
+}

+ 125 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/HistogramInfo.java

@@ -0,0 +1,125 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+public class HistogramInfo {
+
+	private String key;
+
+	private Long count;
+
+	private double min;
+
+	private double max;
+
+	private double mean;
+
+	private String durationUnit;
+
+	private double p50;
+
+	private double p75;
+
+	private double p95;
+
+	private double p98;
+
+	private double p99;
+
+	private double p999;
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public Long getCount() {
+		return count;
+	}
+
+	public void setCount(Long count) {
+		this.count = count;
+	}
+
+	public double getMin() {
+		return min;
+	}
+
+	public void setMin(double min) {
+		this.min = min;
+	}
+
+	public double getMax() {
+		return max;
+	}
+
+	public void setMax(double max) {
+		this.max = max;
+	}
+
+	public double getMean() {
+		return mean;
+	}
+
+	public void setMean(double mean) {
+		this.mean = mean;
+	}
+
+	public String getDurationUnit() {
+		return durationUnit;
+	}
+
+	public void setDurationUnit(String durationUnit) {
+		this.durationUnit = durationUnit;
+	}
+
+	public double getP50() {
+		return p50;
+	}
+
+	public void setP50(double p50) {
+		this.p50 = p50;
+	}
+
+	public double getP75() {
+		return p75;
+	}
+
+	public void setP75(double p75) {
+		this.p75 = p75;
+	}
+
+	public double getP95() {
+		return p95;
+	}
+
+	public void setP95(double p95) {
+		this.p95 = p95;
+	}
+
+	public double getP98() {
+		return p98;
+	}
+
+	public void setP98(double p98) {
+		this.p98 = p98;
+	}
+
+	public double getP99() {
+		return p99;
+	}
+
+	public void setP99(double p99) {
+		this.p99 = p99;
+	}
+
+	public double getP999() {
+		return p999;
+	}
+
+	public void setP999(double p999) {
+		this.p999 = p999;
+	}
+
+}

+ 75 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/MeterInfo.java

@@ -0,0 +1,75 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+public class MeterInfo {
+
+	private String key;
+
+	private Long count;
+
+	private double meanRate;
+
+	private double oneMinuteRate;
+
+	private double fiveMinuteRate;
+
+	private double fifteenMinuteRate;
+
+	private String rateUnit;
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public Long getCount() {
+		return count;
+	}
+
+	public void setCount(Long count) {
+		this.count = count;
+	}
+
+	public double getMeanRate() {
+		return meanRate;
+	}
+
+	public void setMeanRate(double meanRate) {
+		this.meanRate = meanRate;
+	}
+
+	public double getOneMinuteRate() {
+		return oneMinuteRate;
+	}
+
+	public void setOneMinuteRate(double oneMinuteRate) {
+		this.oneMinuteRate = oneMinuteRate;
+	}
+
+	public double getFiveMinuteRate() {
+		return fiveMinuteRate;
+	}
+
+	public void setFiveMinuteRate(double fiveMinuteRate) {
+		this.fiveMinuteRate = fiveMinuteRate;
+	}
+
+	public double getFifteenMinuteRate() {
+		return fifteenMinuteRate;
+	}
+
+	public void setFifteenMinuteRate(double fifteenMinuteRate) {
+		this.fifteenMinuteRate = fifteenMinuteRate;
+	}
+
+	public String getRateUnit() {
+		return rateUnit;
+	}
+
+	public void setRateUnit(String rateUnit) {
+		this.rateUnit = rateUnit;
+	}
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/MetricNames.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+/**
+ * 名称元素
+ *
+ * @author WANGWEI
+ * @date 2019年7月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum MetricNames {
+
+	API_TIMER, API_HISTOGRAM, API_METER, API_ERROR_METER
+
+}

+ 26 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/MetricRegistryHolder.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import com.codahale.metrics.MetricRegistry;
+
+/**
+ * MetricRegistryHolder
+ *
+ * @author WANGWEI
+ * @date 2019年7月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class MetricRegistryHolder {
+
+	private static MetricRegistry defaultMetricRegistry = new MetricRegistry();
+
+	/**
+	 * 获取默认
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static MetricRegistry getDefalut() {
+		return defaultMetricRegistry;
+	}
+
+}

+ 48 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ReportInfo.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import java.util.Date;
+import java.util.List;
+
+public class ReportInfo {
+
+	private List<HistogramInfo> histogramInfoList;
+
+	private List<MeterInfo> meterInfoList;
+
+	private List<TimerInfo> timerInfoList;
+
+	private Date dateTime;
+
+	public List<HistogramInfo> getHistogramInfoList() {
+		return histogramInfoList;
+	}
+
+	public void setHistogramInfoList(List<HistogramInfo> histogramInfoList) {
+		this.histogramInfoList = histogramInfoList;
+	}
+
+	public List<MeterInfo> getMeterInfoList() {
+		return meterInfoList;
+	}
+
+	public void setMeterInfoList(List<MeterInfo> meterInfoList) {
+		this.meterInfoList = meterInfoList;
+	}
+
+	public List<TimerInfo> getTimerInfoList() {
+		return timerInfoList;
+	}
+
+	public void setTimerInfoList(List<TimerInfo> timerInfoList) {
+		this.timerInfoList = timerInfoList;
+	}
+
+	public Date getDateTime() {
+		return dateTime;
+	}
+
+	public void setDateTime(Date dateTime) {
+		this.dateTime = dateTime;
+	}
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/ReportorHolder.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Reportor Holder
+ *
+ * @author WANGWEI
+ * @date 2019年7月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ReportorHolder {
+
+	private static DataReportor apiReporter = DataReportor
+			.forRegistry(MetricRegistryHolder.getDefalut()).convertRatesTo(TimeUnit.SECONDS)
+			.convertDurationsTo(TimeUnit.MILLISECONDS).build();
+
+	public static DataReportor getApiDataReportor() {
+		return apiReporter;
+	}
+
+}

+ 175 - 0
src/main/java/cn/com/qmth/examcloud/web/actuator/TimerInfo.java

@@ -0,0 +1,175 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+public class TimerInfo {
+
+	private String key;
+
+	private Long count;
+
+	private double meanRate;
+
+	private double oneMinuteRate;
+
+	private double fiveMinuteRate;
+
+	private double fifteenMinuteRate;
+
+	private String rateUnit;
+
+	private double min;
+
+	private double max;
+
+	private double mean;
+
+	private String durationUnit;
+
+	private double p50;
+
+	private double p75;
+
+	private double p95;
+
+	private double p98;
+
+	private double p99;
+
+	private double p999;
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public Long getCount() {
+		return count;
+	}
+
+	public void setCount(Long count) {
+		this.count = count;
+	}
+
+	public double getMeanRate() {
+		return meanRate;
+	}
+
+	public void setMeanRate(double meanRate) {
+		this.meanRate = meanRate;
+	}
+
+	public double getOneMinuteRate() {
+		return oneMinuteRate;
+	}
+
+	public void setOneMinuteRate(double oneMinuteRate) {
+		this.oneMinuteRate = oneMinuteRate;
+	}
+
+	public double getFiveMinuteRate() {
+		return fiveMinuteRate;
+	}
+
+	public void setFiveMinuteRate(double fiveMinuteRate) {
+		this.fiveMinuteRate = fiveMinuteRate;
+	}
+
+	public double getFifteenMinuteRate() {
+		return fifteenMinuteRate;
+	}
+
+	public void setFifteenMinuteRate(double fifteenMinuteRate) {
+		this.fifteenMinuteRate = fifteenMinuteRate;
+	}
+
+	public String getRateUnit() {
+		return rateUnit;
+	}
+
+	public void setRateUnit(String rateUnit) {
+		this.rateUnit = rateUnit;
+	}
+
+	public double getMin() {
+		return min;
+	}
+
+	public void setMin(double min) {
+		this.min = min;
+	}
+
+	public double getMax() {
+		return max;
+	}
+
+	public void setMax(double max) {
+		this.max = max;
+	}
+
+	public double getMean() {
+		return mean;
+	}
+
+	public void setMean(double mean) {
+		this.mean = mean;
+	}
+
+	public String getDurationUnit() {
+		return durationUnit;
+	}
+
+	public void setDurationUnit(String durationUnit) {
+		this.durationUnit = durationUnit;
+	}
+
+	public double getP50() {
+		return p50;
+	}
+
+	public void setP50(double p50) {
+		this.p50 = p50;
+	}
+
+	public double getP75() {
+		return p75;
+	}
+
+	public void setP75(double p75) {
+		this.p75 = p75;
+	}
+
+	public double getP95() {
+		return p95;
+	}
+
+	public void setP95(double p95) {
+		this.p95 = p95;
+	}
+
+	public double getP98() {
+		return p98;
+	}
+
+	public void setP98(double p98) {
+		this.p98 = p98;
+	}
+
+	public double getP99() {
+		return p99;
+	}
+
+	public void setP99(double p99) {
+		this.p99 = p99;
+	}
+
+	public double getP999() {
+		return p999;
+	}
+
+	public void setP999(double p999) {
+		this.p999 = p999;
+	}
+
+}

+ 74 - 0
src/main/java/cn/com/qmth/examcloud/web/aliyun/AliYunAccount.java

@@ -0,0 +1,74 @@
+package cn.com.qmth.examcloud.web.aliyun;
+
+/**
+ * 阿里云存储账号信息
+ */
+public class AliYunAccount {
+
+	private String bucket;
+	private String ossEndpoint;
+	private String accessKeyId;
+	private String accessKeySecret;
+	private String domain;
+	private String domainBackup;
+
+	public String getBucket() {
+		return bucket;
+	}
+
+	public void setBucket(String bucket) {
+		this.bucket = bucket;
+	}
+
+	public String getOssEndpoint() {
+		return ossEndpoint;
+	}
+
+	public void setOssEndpoint(String ossEndpoint) {
+		this.ossEndpoint = ossEndpoint;
+	}
+
+	public String getAccessKeyId() {
+		return accessKeyId;
+	}
+
+	public void setAccessKeyId(String accessKeyId) {
+		this.accessKeyId = accessKeyId;
+	}
+
+	public String getAccessKeySecret() {
+		return accessKeySecret;
+	}
+
+	public void setAccessKeySecret(String accessKeySecret) {
+		this.accessKeySecret = accessKeySecret;
+	}
+
+	public String getDomain() {
+		return domain;
+	}
+
+	public void setDomain(String domain) {
+		this.domain = domain;
+	}
+
+	public String getDomainBackup() {
+		return domainBackup;
+	}
+
+	public void setDomainBackup(String domainBackup) {
+		this.domainBackup = domainBackup;
+	}
+
+	public AliYunAccount(String bucket, String ossEndpoint, String accessKeyId, String accessKeySecret, String domain,
+			String domainBackup) {
+		super();
+		this.bucket = bucket;
+		this.ossEndpoint = ossEndpoint;
+		this.accessKeyId = accessKeyId;
+		this.accessKeySecret = accessKeySecret;
+		this.domain = domain;
+		this.domainBackup = domainBackup;
+	}
+
+}

+ 64 - 0
src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSite.java

@@ -0,0 +1,64 @@
+package cn.com.qmth.examcloud.web.aliyun;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 类注释
+ *
+ */
+public class AliyunSite implements JsonSerializable {
+
+	private static final long serialVersionUID = -1754474062438702321L;
+
+	private String id;
+
+	private String name;
+
+	private String aliyunId;
+
+	private String maxSize;
+
+	private String path;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getMaxSize() {
+		return maxSize;
+	}
+
+	public void setMaxSize(String maxSize) {
+		this.maxSize = maxSize;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+	public String getAliyunId() {
+		return aliyunId;
+	}
+
+	public void setAliyunId(String aliyunId) {
+		this.aliyunId = aliyunId;
+	}
+
+	
+}

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

@@ -0,0 +1,136 @@
+package cn.com.qmth.examcloud.web.aliyun;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.thoughtworks.xstream.XStream;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.XStreamBuilder;
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * aliyun site manager
+ *
+ */
+public class AliyunSiteManager {
+
+	private static final Map<String, AliyunSite> SITE_HOLDERS = new ConcurrentHashMap<>();
+	
+	private static final Map<String, OSS> CLIENT_HOLDERS = new ConcurrentHashMap<>();
+	 
+	private static final Map<String, AliYunAccount> ACCOUNT_HOLDERS = new ConcurrentHashMap<>();
+
+	public static void initSite() {
+		String resoucePath = PathUtil.getResoucePath("aliyun.xml");
+		File file = new File(resoucePath);
+
+		XStream xStream = XStreamBuilder.newInstance().build();
+		xStream.allowTypes(new Class[] { AliyunSite.class, List.class });
+		xStream.alias("sites", List.class);
+		xStream.alias("site", AliyunSite.class);
+
+		List<AliyunSite> list = null;
+		try {
+			@SuppressWarnings("unchecked")
+			List<AliyunSite> obj = (List<AliyunSite>) xStream.fromXML(file);
+			list = obj;
+		} catch (Exception e) {
+			throw new StatusException("20001", "aliyun.xml is wrong", e);
+		}
+
+		for (AliyunSite aliyunSite : list) {
+
+			SITE_HOLDERS.put(aliyunSite.getId(), aliyunSite);
+		}
+
+	}
+	public static void initClient() {
+		String aliyunNum = PropertyHolder.getString("$aliyun.site.num");
+		if (StringUtils.isBlank(aliyunNum)) {
+			throw new StatusException("20021", "aliyunNum is not configured.");
+		}
+		int num=Integer.parseInt(aliyunNum);
+		for(int i=1;i<=num;i++) {
+			String aliyunId=i+"";
+			String bucket = PropertyHolder.getString("$aliyun.site." + aliyunId + ".bucket");
+			String ossEndpoint = PropertyHolder.getString("$aliyun.site." + aliyunId + ".ossEndpoint");
+
+			String accessKeyId = PropertyHolder.getString("$aliyun.site." + aliyunId + ".accessKeyId");
+			String accessKeySecret = PropertyHolder.getString("$aliyun.site." + aliyunId + ".accessKeySecret");
+			String domain = PropertyHolder.getString("$aliyun.site." + aliyunId + ".domain");
+			String domainBackup = PropertyHolder.getString("$aliyun.site." + aliyunId + ".domain.backup");
+
+			if (StringUtils.isBlank(bucket)) {
+				throw new StatusException("20001", "bucket is not configured. aliyunId=" + aliyunId);
+			}
+			if (StringUtils.isBlank(ossEndpoint)) {
+				throw new StatusException("20002", "ossEndpoint is not configured. aliyunId=" + aliyunId);
+			}
+			if (StringUtils.isBlank(accessKeyId)) {
+				throw new StatusException("20003", "accessKeyId is not configured. aliyunId=" + aliyunId);
+			}
+			if (StringUtils.isBlank(accessKeySecret)) {
+				throw new StatusException("20004", "accessKeySecret is not configured. aliyunId=" + aliyunId);
+			}
+			if (StringUtils.isBlank(domain)) {
+				throw new StatusException("20005", "domain is not configured. aliyunId=" + aliyunId);
+			}
+			if (null == ACCOUNT_HOLDERS.get(aliyunId)) {
+				AliYunAccount ac = new AliYunAccount(bucket, ossEndpoint, accessKeyId, accessKeySecret, domain,
+						domainBackup);
+				ACCOUNT_HOLDERS.put(aliyunId, ac);
+			}
+			if (null == CLIENT_HOLDERS.get(aliyunId)) {
+				OSS ossClient = new OSSClientBuilder().build(ossEndpoint, accessKeyId, accessKeySecret);
+				CLIENT_HOLDERS.put(aliyunId, ossClient);
+			}
+		}
+	}
+	/**
+	 * 方法注释
+	 *
+	 * @author
+	 * @param siteId
+	 * @return
+	 */
+	public static AliyunSite getAliyunSite(String siteId) {
+		AliyunSite aliyunSite = SITE_HOLDERS.get(siteId);
+
+		if (null == aliyunSite) {
+			throw new StatusException("20006", "aliyunSite is null");
+		}
+		return aliyunSite;
+	}
+
+	public static AliYunAccount getAliYunAccountByAliyunId(String aliyunId) {
+		AliYunAccount ac = ACCOUNT_HOLDERS.get(aliyunId);
+
+		if (null == ac) {
+			throw new StatusException("20007", "AliYunAccount is null");
+		}
+		return ac;
+	}
+	
+	public static OSS getAliYunClientByAliyunId(String aliyunId) {
+		OSS oss = CLIENT_HOLDERS.get(aliyunId);
+
+		if (null == oss) {
+			throw new StatusException("20008", "aliYunClient is null");
+		}
+		return oss;
+	}
+
+	public static OSS getAliYunClientBySiteId(String siteId) {
+		AliyunSite site = getAliyunSite(siteId);
+		return getAliYunClientByAliyunId(site.getAliyunId());
+	}
+
+}

+ 398 - 0
src/main/java/cn/com/qmth/examcloud/web/baidu/BaiduClient.java

@@ -0,0 +1,398 @@
+package cn.com.qmth.examcloud.web.baidu;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * baidu 客户端
+ *
+ * @author WANGWEI
+ * @date 2019年9月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class BaiduClient {
+
+	protected static ExamCloudLog log = ExamCloudLogFactory.getLog(BaiduClient.class);
+
+	private static CloseableHttpClient httpclient;
+
+	private static RequestConfig requestConfig;
+
+	private static BaiduClient baiduClient;
+
+	private static String apiKey;
+
+	private static String secretKey;
+
+	private BaiduClient() {
+	}
+
+	/**
+	 * 获取单例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static BaiduClient getClient() {
+		if (null == baiduClient) {
+			synchronized (BaiduClient.class) {
+				if (null == baiduClient) {
+					baiduClient = new BaiduClient();
+				}
+			}
+		}
+
+		return baiduClient;
+	}
+
+	static {
+		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(60,
+				TimeUnit.SECONDS);
+		cm.setValidateAfterInactivity(1000);
+		cm.setMaxTotal(8000);
+		cm.setDefaultMaxPerRoute(200);
+
+		requestConfig = RequestConfig.custom().setConnectionRequestTimeout(500)
+				.setSocketTimeout(10000).setConnectTimeout(10000)
+				.setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
+
+		httpclient = HttpClients.custom().setConnectionManager(cm).disableAutomaticRetries()
+				.setDefaultRequestConfig(requestConfig).build();
+
+		apiKey = PropertyHolder.getString("$baidu.apiKey");
+		secretKey = PropertyHolder.getString("$baidu.secretKey");
+
+		if (StringUtils.isBlank(apiKey)) {
+			log.error("'facepp.apiKey' is not configured");
+		}
+		if (StringUtils.isBlank(secretKey)) {
+			log.error("'facepp.secretKey' is not configured");
+		}
+	}
+
+	/**
+	 * 调用鉴权接口获取的token
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public String getAccessToken() {
+
+		String url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id="
+				+ apiKey + "&client_secret=" + secretKey;
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(BaiduClient.requestConfig);
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		long s = System.currentTimeMillis();
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Baidu AI FAIL]. fail to get access token.", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Baidu AI]. cost " + (System.currentTimeMillis() - s) + " ms.");
+		}
+
+		int statusCode = responseHolder.getStatusCode();
+		if (HttpStatus.SC_OK != statusCode) {
+			throw new StatusException("920", "[Baidu AI]. fail to get access token");
+		}
+
+		JSONObject respBody = responseHolder.getRespBody();
+		String accessToken = respBody.getString("access_token");
+
+		return accessToken;
+	}
+
+	/**
+	 * 活体检测
+	 *
+	 * @author WANGWEI
+	 * @param imageUrl
+	 * @return
+	 */
+	public JsonHttpResponseHolder verifyFaceLiveness(String imageUrl) {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Baidu AI]. imageUrl=" + imageUrl);
+		}
+
+		String accessToken = getAccessToken();
+		String url = "https://aip.baidubce.com/rest/2.0/face/v3/faceverify?access_token="
+				+ accessToken;
+
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(BaiduClient.requestConfig);
+		httpPost.setHeader("Content-Type", "application/json");
+
+		Map<String, String> params = Maps.newHashMap();
+
+		params.put("image", imageUrl);
+		params.put("image_type", "URL");
+
+		List<Map<String, String>> list = Lists.newArrayList();
+		list.add(params);
+
+		httpPost.setEntity(new StringEntity(JsonUtil.toJson(list), "UTF-8"));
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		long s = System.currentTimeMillis();
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Baidu AI FAIL].", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Baidu AI]. imageUrl=" + imageUrl + "; cost "
+					+ (System.currentTimeMillis() - s) + " ms.");
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 活体检测
+	 *
+	 * @author WANGWEI
+	 * @param imageUrl
+	 * @return
+	 */
+	public JsonHttpResponseHolder verifyFaceLivenessUseBase64(String base64) {
+
+		String accessToken = getAccessToken();
+		String url = "https://aip.baidubce.com/rest/2.0/face/v3/faceverify?access_token="
+				+ accessToken;
+
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(BaiduClient.requestConfig);
+		httpPost.setHeader("Content-Type", "application/json");
+
+		Map<String, String> params = Maps.newHashMap();
+
+		params.put("image", base64);
+		params.put("image_type", "BASE64");
+
+		List<Map<String, String>> list = Lists.newArrayList();
+		list.add(params);
+
+		httpPost.setEntity(new StringEntity(JsonUtil.toJson(list), "UTF-8"));
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Baidu AI Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Baidu AI FAIL].", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 百度活体检测<br>
+	 * 优先使用主地址调用baidu活体检测接口<br>
+	 * 主地址无效时,使用备用地址调用baidu活体检测接口<br>
+	 * 备用地址也无效时,使用备用地址下载数据,使用下载数据的base64加密串调用baidu活体检测接口<br>
+	 * 
+	 *
+	 * @author WANGWEI
+	 * @param imageUrl
+	 *            主地址
+	 * @param backupImageUrl
+	 *            备用地址
+	 * @return
+	 * @throws StatusException
+	 *             code为901,902,903表示图片地址无效
+	 */
+	public JsonHttpResponseHolder verifyFaceLiveness(String imageUrl, String backupImageUrl)
+			throws StatusException {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++]. imageUrl=" + imageUrl + "; backupImageUrl=" + backupImageUrl);
+		}
+
+		JsonHttpResponseHolder responseHolder = null;
+
+		boolean exceptionWhenUsingImageUrl = false;
+		boolean exceptionWhenUsingBackupImageUrl = false;
+
+		try {
+			responseHolder = verifyFaceLiveness(imageUrl);
+		} catch (ExamCloudRuntimeException e) {
+			exceptionWhenUsingImageUrl = true;
+		}
+
+		if (exceptionWhenUsingImageUrl) {
+			try {
+				responseHolder = verifyFaceLiveness(backupImageUrl);
+			} catch (ExamCloudRuntimeException e) {
+				exceptionWhenUsingBackupImageUrl = true;
+			}
+		} else {
+			long errCode = responseHolder.getRespBody().getLong("error_code");
+
+			if (0 == errCode) {
+				return responseHolder;
+			}
+
+			if (retry(errCode)) {
+				try {
+					responseHolder = verifyFaceLiveness(backupImageUrl);
+				} catch (ExamCloudRuntimeException e) {
+					exceptionWhenUsingBackupImageUrl = true;
+				}
+			}
+
+		}
+
+		long errCode = responseHolder.getRespBody().getLong("error_code");
+
+		if (0 == errCode) {
+			return responseHolder;
+		}
+
+		if (exceptionWhenUsingBackupImageUrl || retry(errCode)) {
+			HttpGet get = new HttpGet(backupImageUrl);
+			get.setConfig(BaiduClient.requestConfig);
+			CloseableHttpResponse response = null;
+			String imageBase64 = null;
+			long s = System.currentTimeMillis();
+			try {
+				response = httpclient.execute(get);
+
+				if (HttpStatus.SC_OK != response.getStatusLine().getStatusCode()) {
+					throw new StatusException("901",
+							"fail to download image file. url=" + backupImageUrl);
+				}
+
+				byte[] byteArray = EntityUtils.toByteArray(response.getEntity());
+				if (100 > byteArray.length) {
+					throw new StatusException("902", "invalid image size. url=" + backupImageUrl);
+				}
+
+				imageBase64 = Base64.encodeBase64String(byteArray);
+
+			} catch (StatusException e) {
+				log.error("fail to download image file. url=" + backupImageUrl, e);
+				throw e;
+			} catch (Exception e) {
+				log.error("fail to download image file. url=" + backupImageUrl, e);
+				throw new StatusException("903", "fail to download file. url=" + backupImageUrl, e);
+			} finally {
+				IOUtils.closeQuietly(response);
+			}
+
+			if (log.isDebugEnabled()) {
+				log.debug("download image file successfully; url=" + backupImageUrl + "; cost "
+						+ (System.currentTimeMillis() - s) + " ms.");
+			}
+
+			responseHolder = verifyFaceLivenessUseBase64(imageBase64);
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 是否重试
+	 *
+	 * @author WANGWEI
+	 * @param errCode
+	 * @return
+	 */
+	private boolean retry(long errCode) {
+		return 222204 == errCode || 222013 == errCode;
+	}
+
+}

+ 322 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/AppBootstrap.java

@@ -0,0 +1,322 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.http.HttpStatus;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.HttpMethod;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.OKHttpUtil;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.cloud.AppSelf;
+import cn.com.qmth.examcloud.web.cloud.AppSelfHolder;
+import okhttp3.Response;
+
+/**
+ * 系统启动器
+ *
+ * @author WANGWEI
+ * @date 2019年3月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class AppBootstrap {
+
+	private static ExamCloudLog log = ExamCloudLogFactory.getLog(AppBootstrap.class);
+
+	private static Map<String, String> properties;
+
+	private static String active;
+
+	private static String startupCode;
+
+	private static Long appId;
+
+	private static String secretKey;
+
+	private static String appCode;
+
+	private static String configCenterHost;
+
+	private static String configCenterPort;
+
+	/**
+	 * 启动方法
+	 *
+	 * @author WANGWEI
+	 * @param primarySource
+	 * @param args
+	 * @return
+	 */
+	public static synchronized ConfigurableApplicationContext run(Class<?> primarySource,
+			String... args) {
+
+		ThreadContext.put("TRACE_ID", Thread.currentThread().getName());
+		log.info("Starting...");
+
+		checkBootstrapParams(args);
+
+		sendStartupRequest2ConfigCenter();
+
+		if (null != args) {
+			for (String arg : args) {
+				arg = arg.trim();
+				if (arg.startsWith("--")) {
+					String key = arg.substring(2, arg.indexOf("=")).trim();
+					String value = arg.substring(arg.indexOf("=") + 1).trim();
+					properties.put(key, value);
+				}
+			}
+		}
+
+		// 网卡选择
+		String preferredNetworks = properties.get("examcloud.inet.preferredNetworks");
+		if (StringUtils.isNotBlank(preferredNetworks)) {
+			System.setProperty("spring.cloud.inetutils.preferred-networks", preferredNetworks);
+			properties.put("spring.cloud.inetutils.preferred-networks", preferredNetworks);
+		}
+
+		properties.put("spring.profiles.active", active);
+
+		Set<String> argSet = Sets.newLinkedHashSet();
+		for (Entry<String, String> p : properties.entrySet()) {
+			String key = p.getKey();
+			String value = p.getValue();
+			if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
+				continue;
+			}
+			key = key.trim();
+			value = value.trim();
+			PropertyHolder.setProperty(key, value);
+			PropertiesUtil.setProperty(key, value);
+			String arg = "--" + key + "=" + value;
+			argSet.add(arg);
+		}
+
+		AppSelfHolder.set(buildAppSelf());
+
+		String[] newArgs = argSet.toArray(new String[argSet.size()]);
+		ConfigurableApplicationContext context = null;
+		try {
+			context = SpringApplication.run(primarySource, newArgs);
+			noticeConfigCenter("success");
+		} catch (Exception e) {
+			String stackTrace = Util.getStackTrace(e);
+			noticeConfigCenter("failure:\n" + stackTrace);
+			log.error("fail to run spring app.", e);
+			System.exit(-1);
+		}
+
+		return context;
+	}
+
+	/**
+	 * 创建 {@link AppSelf} 实例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private static AppSelf buildAppSelf() {
+		return new AppSelf() {
+
+			@Override
+			public Long getAppId() {
+				return appId;
+			}
+
+			@Override
+			public String getAppCode() {
+				return appCode;
+			}
+
+			@Override
+			public String getSecretKey() {
+				return secretKey;
+			}
+		};
+	}
+
+	/**
+	 * 通知配置中心
+	 *
+	 * @author WANGWEI
+	 * @param message
+	 */
+	private static void noticeConfigCenter(String message) {
+		String url = "http://" + configCenterHost + ":" + configCenterPort + "/configCenter/notice";
+
+		Map<String, String> req = Maps.newHashMap();
+		req.put("active", active);
+		req.put("appCode", appCode);
+		req.put("startupCode", startupCode);
+		req.put("message", message);
+
+		Response resp = null;
+		try {
+			Map<String, String> headers = Maps.newHashMap();
+			headers.put("Trace-Id", startupCode);
+			resp = OKHttpUtil.call(HttpMethod.POST, url.toString(), headers, req);
+		} catch (Exception e) {
+			log.error("fail to notice config center.", e);
+			System.exit(-1);
+		} finally {
+			IOUtils.closeQuietly(resp);
+		}
+	}
+
+	/**
+	 * 向配置中心发送启动请求
+	 *
+	 * @author WANGWEI
+	 */
+	private static void sendStartupRequest2ConfigCenter() {
+		StringBuilder url = new StringBuilder(
+				"http://" + configCenterHost + ":" + configCenterPort + "/configCenter/startup");
+
+		Map<String, String> req = Maps.newHashMap();
+		req.put("active", active);
+		req.put("appCode", appCode);
+		req.put("startupCode", startupCode);
+
+		Response resp = null;
+		try {
+			Map<String, String> headers = Maps.newHashMap();
+			headers.put("Trace-Id", startupCode);
+			resp = OKHttpUtil.call(HttpMethod.POST, url.toString(), headers, req);
+			String body = resp.body().string();
+			if (resp.code() != HttpStatus.OK.value()) {
+				throw new ExamCloudRuntimeException("fail to get configuration. " + body);
+			}
+
+			String encryptedAppId = resp.header("App-Id");
+			String encryptedSecretKey = resp.header("Secret-Key");
+
+			appId = StringUtil.toLong(BootstrapSecurityUtil.decrypt(encryptedAppId, startupCode));
+			secretKey = BootstrapSecurityUtil.decrypt(encryptedSecretKey, startupCode);
+
+			String decryptedBody = BootstrapSecurityUtil.decrypt(body, startupCode);
+			@SuppressWarnings("unchecked")
+			Map<String, String> map = JsonUtil.fromJson(decryptedBody, Map.class);
+			properties = map;
+
+		} catch (Exception e) {
+			log.error("fail to send startup request to config center.", e);
+			System.exit(-1);
+		} finally {
+			IOUtils.closeQuietly(resp);
+		}
+
+	}
+
+	/**
+	 * 检查启动参数
+	 *
+	 * @author WANGWEI
+	 * @param args
+	 * @return
+	 */
+	private static void checkBootstrapParams(String... args) {
+
+		Properties props = new Properties();
+		PropertiesUtil.loadFromResource("application.properties", props);
+
+		if (null != args) {
+			for (String s : args) {
+				s = s.trim();
+				if (s.startsWith("--spring.profiles.active=")) {
+					active = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--examcloud.startup.startupCode=")) {
+					startupCode = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--examcloud.startup.configCenterHost=")) {
+					configCenterHost = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--examcloud.startup.configCenterPort=")) {
+					configCenterPort = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--examcloud.startup.appCode=")) {
+					appCode = s.substring(s.indexOf("=") + 1);
+				}
+			}
+		}
+
+		// active
+		if (null == active) {
+			String value = props.getProperty("spring.profiles.active");
+			if (StringUtils.isNotBlank(value)) {
+				active = value.trim();
+			}
+		}
+		log.info("active=" + active);
+		if (StringUtils.isBlank(active)) {
+			log.error("property[spring.profiles.active] is not specified");
+			System.exit(-1);
+		}
+
+		// startupCode
+		if (null == startupCode) {
+			String value = props.getProperty("examcloud.startup.startupCode");
+			if (StringUtils.isNotBlank(value)) {
+				startupCode = value.trim();
+			}
+		}
+		log.info("startupCode=" + startupCode);
+		if (StringUtils.isBlank(startupCode)) {
+			log.error("property[examcloud.startup.startupCode] is not specified");
+			System.exit(-1);
+		}
+
+		// appCode
+		if (null == appCode) {
+			String value = props.getProperty("examcloud.startup.appCode");
+			if (StringUtils.isNotBlank(value)) {
+				appCode = value.trim();
+			}
+		}
+		log.info("appCode=" + appCode);
+		if (StringUtils.isBlank(appCode)) {
+			log.error("property[examcloud.startup.appCode] is not specified");
+			System.exit(-1);
+		}
+
+		// configCenterHost
+		if (null == configCenterHost) {
+			String value = props.getProperty("examcloud.startup.configCenterHost");
+			if (StringUtils.isNotBlank(value)) {
+				configCenterHost = value.trim();
+			}
+		}
+		log.info("configCenterHost=" + configCenterHost);
+		if (StringUtils.isBlank(configCenterHost)) {
+			log.error("property[examcloud.startup.configCenterHost] is not specified");
+			System.exit(-1);
+		}
+
+		// configCenterPort
+		if (null == configCenterPort) {
+			String value = props.getProperty("examcloud.startup.configCenterPort");
+			if (StringUtils.isNotBlank(value)) {
+				configCenterPort = value.trim();
+			}
+		}
+		log.info("configCenterPort=" + configCenterPort);
+		if (null == configCenterPort) {
+			log.error("property[examcloud.startup.configCenterPort] is not specified");
+			System.exit(-1);
+		}
+
+	}
+
+}

+ 122 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/BootstrapSecurityUtil.java

@@ -0,0 +1,122 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.AES;
+
+/**
+ * 安全工具
+ *
+ * @author WANGWEI
+ * @date 2018年12月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class BootstrapSecurityUtil {
+
+	private static final String RANDOM = "9451utb@91&vrmio!90iu!89F2QN1V ALAII!K33I*DFA^sA";
+
+	private static final String ENCRYPTED_HEAD = "$$.";
+
+	/**
+	 * 加密
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @param secretKey
+	 */
+	public static String encrypt(String s, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		String encrypted = aes.encrypt(s);
+		return encrypted;
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @param secretKey
+	 */
+	public static String decrypt(String s, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		String decrypted = aes.decrypt(s);
+		return decrypted;
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param props
+	 * @param secretKey
+	 */
+	public static void decrypt(Properties props, String secretKey) {
+		Map<String, String> map = Maps.newHashMap();
+		Set<String> set = Sets.newHashSet();
+		for (Entry<Object, Object> entry : props.entrySet()) {
+			Object key = entry.getKey();
+			Object value = entry.getValue();
+			if (key instanceof String && value instanceof String) {
+				if (((String) key).startsWith(ENCRYPTED_HEAD)) {
+					AES aes = new AES(secretKey + RANDOM);
+					String decryptedValue = null;
+					try {
+						decryptedValue = aes.decrypt((String) value);
+					} catch (Exception e) {
+						throw new ExamCloudRuntimeException(
+								"fail to decrypt value of '" + key + "'");
+					}
+					String newKey = ((String) key).substring(ENCRYPTED_HEAD.length());
+					map.put(newKey, decryptedValue);
+					set.add((String) key);
+				}
+			}
+		}
+
+		for (String s : set) {
+			props.remove(s);
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			props.put(entry.getKey(), entry.getValue());
+		}
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param props
+	 * @param secretKey
+	 */
+	public static void decrypt(Map<String, String> props, String secretKey) {
+		Map<String, String> map = Maps.newHashMap();
+		Set<String> set = Sets.newHashSet();
+		for (Entry<String, String> entry : props.entrySet()) {
+			String key = entry.getKey();
+			String value = entry.getValue();
+			if (key.startsWith(ENCRYPTED_HEAD)) {
+				AES aes = new AES(secretKey + RANDOM);
+				String decrypted = aes.decrypt((String) value);
+				map.put(key.substring(ENCRYPTED_HEAD.length() + 1), decrypted);
+				set.add(key);
+			}
+		}
+
+		for (String s : set) {
+			props.remove(s);
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			props.put(entry.getKey(), entry.getValue());
+		}
+	}
+
+}

+ 131 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/PropertyHolder.java

@@ -0,0 +1,131 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Properties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+
+/**
+ * 云配置
+ *
+ * @author WANGWEI
+ * @date 2019年3月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class PropertyHolder {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(PropertyHolder.class);
+
+	private static final Properties PROPS = new Properties();
+
+	/**
+	 * 从资源文件加载配置
+	 *
+	 * @author WANGWEI
+	 * @param resourceName
+	 */
+	public static void loadFromResource(String resourceName) {
+		PropertiesUtil.loadFromResource(resourceName, PROPS);
+	}
+
+	/**
+	 * 设置属性
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 */
+	public static void setProperty(String key, String value) {
+		PROPS.setProperty(key, value);
+	}
+
+	/**
+	 * @param key
+	 * @return
+	 */
+	public static String getString(String key) {
+		String value = PROPS.getProperty(key);
+		if (StringUtils.isNotBlank(value)) {
+			return value.trim();
+		} else {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug("No  property key named [" + key + "] is configured.");
+			}
+			return null;
+		}
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static String getString(String key, String defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			return value;
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static int getInt(String key, int defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			try {
+				return Integer.parseInt(value);
+			} catch (NumberFormatException e) {
+				PROPS.setProperty(key, String.valueOf(defaultValue));
+				return defaultValue;
+			}
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static long getLong(String key, long defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			try {
+				return Long.parseLong(value);
+			} catch (NumberFormatException e) {
+				return defaultValue;
+			}
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * 获取boolean
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param defaultVale
+	 * @return
+	 */
+	public static boolean getBoolean(String key, boolean defaultVale) {
+		String value = getString(key);
+		if (null == value) {
+			return defaultVale;
+		}
+		if (value.equals("true")) {
+			return true;
+		} else if (value.equals("false")) {
+			return false;
+		} else {
+			return defaultVale;
+		}
+	}
+
+}

+ 136 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java

@@ -0,0 +1,136 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.Map;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.api.commons.CloudService;
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * cache
+ *
+ * @author WANGWEI
+ * @date 2018年8月23日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController
+@RequestMapping("cache")
+public class CacheCloudServiceProvider implements CloudService {
+
+	private static final long serialVersionUID = -5326807830421467943L;
+
+	protected static final ExamCloudLog LOG = ExamCloudLogFactory
+			.getLog(CacheCloudServiceProvider.class);
+
+	private static Map<String, ObjectCache<?>> map = Maps.newConcurrentMap();
+	
+	private static Map<String, HashCache<?>> hashCacheMap = Maps.newConcurrentMap();
+
+	@PostMapping("refresh")
+	public String refresh(@RequestBody RefreshCacheReq req) {
+
+		String className = req.getClassName();
+		String[] keys = req.getKeys();
+		BasicDataType[] typeArray = req.getTypeArray();
+
+		Object[] expectedKeys = new Object[keys.length];
+
+		for (int i = 0; i < keys.length; i++) {
+			String key = keys[i];
+			BasicDataType type = typeArray[i];
+			if (type.equals(BasicDataType.LONG)) {
+				expectedKeys[i] = Long.parseLong(key);
+			} else if (type.equals(BasicDataType.STRING)) {
+				expectedKeys[i] = key;
+			} else if (type.equals(BasicDataType.INTEGER)) {
+				expectedKeys[i] = Integer.parseInt(key);
+			} else {
+				throw new ExamCloudRuntimeException("key type is not supported");
+			}
+		}
+
+		ObjectCache<?> objectCache = map.get(className);
+		if (null == objectCache) {
+
+			try {
+				Class<?> c = Class.forName(className);
+				objectCache = (ObjectCache<?>) SpringContextHolder.getBean(c);
+				map.put(className, objectCache);
+			} catch (ClassNotFoundException e) {
+				throw new StatusException("008001", "class not found", e);
+			}
+		}
+		objectCache.refresh(expectedKeys);
+		Object object = objectCache.get(expectedKeys);
+		return JsonUtil.toJson(object);
+	}
+	
+	@PostMapping("refreshHash")
+    public String refreshHash(@RequestBody RefreshHashCacheReq req) {
+
+        String className = req.getClassName();
+        String[] keys = req.getKeys();
+        String[] subkeys = req.getSubKeys();
+        BasicDataType[] typeArray = req.getTypeArray();
+        BasicDataType[] subTypeArray = req.getSubTypeArray();
+
+        Object[] expectedKeys = new Object[keys.length];
+
+        for (int i = 0; i < keys.length; i++) {
+            String key = keys[i];
+            BasicDataType type = typeArray[i];
+            if (type.equals(BasicDataType.LONG)) {
+                expectedKeys[i] = Long.parseLong(key);
+            } else if (type.equals(BasicDataType.STRING)) {
+                expectedKeys[i] = key;
+            } else if (type.equals(BasicDataType.INTEGER)) {
+                expectedKeys[i] = Integer.parseInt(key);
+            } else {
+                throw new ExamCloudRuntimeException("key type is not supported");
+            }
+        }
+        
+        Object[] expectedSubKeys = new Object[subkeys.length];
+        for (int i = 0; i < subkeys.length; i++) {
+            String key = subkeys[i];
+            BasicDataType type = subTypeArray[i];
+            if (type.equals(BasicDataType.LONG)) {
+                expectedSubKeys[i] = Long.parseLong(key);
+            } else if (type.equals(BasicDataType.STRING)) {
+                expectedSubKeys[i] = key;
+            } else if (type.equals(BasicDataType.INTEGER)) {
+                expectedSubKeys[i] = Integer.parseInt(key);
+            } else {
+                throw new ExamCloudRuntimeException("subkey type is not supported");
+            }
+        }
+
+        HashCache<?> hashCache = hashCacheMap.get(className);
+        if (null == hashCache) {
+
+            try {
+                Class<?> c = Class.forName(className);
+                hashCache = (HashCache<?>) SpringContextHolder.getBean(c);
+                hashCacheMap.put(className, hashCache);
+            } catch (ClassNotFoundException e) {
+                throw new StatusException("008002", "class not found", e);
+            }
+        }
+        hashCache.refresh(expectedKeys,expectedSubKeys);
+        Object object = hashCache.get(expectedKeys,expectedSubKeys);
+        return JsonUtil.toJson(object);
+    }
+
+}

+ 24 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/DefaultFullObjectCacheWatcher.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * 全量缓存观察器
+ *
+ * @author WANGWEI
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class DefaultFullObjectCacheWatcher implements FullObjectCacheWatcher {
+
+	private Boolean allLoaded = false;
+
+	@Override
+	public boolean allLoaded() {
+		return allLoaded;
+	}
+
+	@Override
+	public void setAllLoaded(boolean allLoaded) {
+		this.allLoaded = allLoaded;
+	}
+
+}

+ 29 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCache.java

@@ -0,0 +1,29 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * 全量对象缓存
+ *
+ * @author WANGWEI
+ * @date 2019年3月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public interface FullObjectCache<T> extends ObjectCache<T> {
+
+	/**
+	 * 从数据源(数据库,配置文件等)加载所有缓存项
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	long loadAllFromResource();
+
+	/**
+	 * 获取全量缓存控制器
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	FullObjectCacheWatcher getFullObjectCacheWatcher();
+
+}

+ 28 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCacheWatcher.java

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * 全量缓存观察器
+ *
+ * @author WANGWEI
+ * @date 2019年3月1日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public interface FullObjectCacheWatcher {
+
+	/**
+	 * 缓存是否全部加载
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public boolean allLoaded();
+
+	/**
+	 * 设置缓存是否全部加载
+	 *
+	 * @author WANGWEI
+	 * @param allLoaded
+	 */
+	public void setAllLoaded(boolean allLoaded);
+
+}

+ 115 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectRedisCache.java

@@ -0,0 +1,115 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * 全量reids缓存
+ *
+ * @author WANGWEI
+ * @date 2019年3月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public abstract class FullObjectRedisCache<T> implements FullObjectCache<T> {
+
+	protected abstract String getKeyPrefix();
+
+	private RedisClient redisClient;
+
+	private FullObjectCacheWatcher fullObjectCacheWatcher = new DefaultFullObjectCacheWatcher();
+
+	private RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	protected String buildKey(Object... keys) {
+		String key = getKeyPrefix() + StringUtils.join(Arrays.asList(keys), '_');
+		return key;
+	}
+
+	private T getFromCache(String key) {
+		Object object = getRedisClient().get(key, Object.class);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		return t;
+	}
+
+	@Override
+	public T get(Object... keys) {
+		String key = buildKey(keys);
+		T t = getFromCache(key);
+		if (null == t) {
+			if (getFullObjectCacheWatcher().allLoaded()) {
+				return t;
+			} else {
+				refresh(keys);
+				t = getFromCache(key);
+				return t;
+			}
+		} else {
+			return t;
+		}
+	}
+
+	@Override
+	public void remove(Object... keys) {
+		String key = buildKey(keys);
+		getRedisClient().delete(key);
+	}
+
+	@Override
+	public void refresh(Object... keys) {
+		String key = buildKey(keys);
+		T t = null;
+		try {
+			t = loadFromResource(keys);
+		} catch (StatusException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException("fail to load data. key=" + key, e);
+		}
+		if (null != t) {
+			getRedisClient().set(key, t);
+		} else {
+			getRedisClient().delete(key);
+		}
+	}
+
+	protected abstract List<Object[]> getKeys(final ObjectHolder<Long> beginIndex,
+			final ObjectHolder<Boolean> empty);
+
+	@Override
+	public long loadAllFromResource() {
+		final ObjectHolder<Long> beginIndex = new ObjectHolder<Long>(null);
+		final ObjectHolder<Boolean> empty = new ObjectHolder<Boolean>(false);
+		while (true) {
+			List<Object[]> keysList = getKeys(beginIndex, empty);
+			if (empty.get()) {
+				break;
+			}
+			for (Object[] keys : keysList) {
+				this.refresh(keys);
+			}
+		}
+		getFullObjectCacheWatcher().setAllLoaded(true);
+		return 0;
+	}
+
+	@Override
+	public FullObjectCacheWatcher getFullObjectCacheWatcher() {
+		return fullObjectCacheWatcher;
+	}
+
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/HashCache.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * Hash缓存
+ *
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public interface HashCache<T> {
+
+	/**
+	 * 从缓存中获取
+	 *
+	 * @param keys
+	 * @return
+	 */
+	T get(Object[] keys,Object[] subkeys);
+
+	/**
+	 * 删除缓存
+	 *
+	 * @param keys 大key
+	 */
+	void remove(Object... keys);
+
+	/**
+	 * 刷新缓存
+	 *
+	 * @param keys
+	 */
+	void refresh(Object[] keys,Object[] subkeys);
+
+	/**
+	 * 从数据源(数据库,配置文件等)加载单个缓存项
+	 *
+	 * @param keys
+	 * @return
+	 */
+	T loadFromResource(Object[] keys,Object[] subkeys);
+
+}

+ 89 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java

@@ -0,0 +1,89 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * Hash redis缓存
+ *
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public abstract class HashRedisCache<T extends RandomCacheBean> implements HashCache<T> {
+
+    private RedisClient redisClient;
+
+    private RedisClient getRedisClient() {
+        if (null == redisClient) {
+            redisClient = SpringContextHolder.getBean(RedisClient.class);
+        }
+        return redisClient;
+    }
+
+    protected abstract String getKeyPrefix();
+
+    protected abstract int getTimeout();
+
+    protected String buildKey(Object... keys) {
+        String key = getKeyPrefix() + StringUtils.join(Arrays.asList(keys), '_');
+        return key;
+    }
+    
+    protected String buildSubKey(Object... keys) {
+        String key = StringUtils.join(Arrays.asList(keys), '_');
+        return key;
+    }
+
+    private T getFromCache(String key, String subkey) {
+        Object object = getRedisClient().get(key,subkey, Object.class);
+        @SuppressWarnings("unchecked")
+        T t = (T) object;
+        return t;
+    }
+    @Override
+    public T get(Object[] keys,Object[] subkeys) {
+        String key = buildKey(keys);
+        String subkey = buildSubKey(subkeys);
+        T t = getFromCache(key,subkey);
+
+        if (null == t) {
+            refresh(keys,subkeys);
+        }
+        t = getFromCache(key,subkey);
+        return t;
+    }
+
+    @Override
+    public void remove(Object... keys) {
+        String key = buildKey(keys);
+        getRedisClient().delete(key);
+    }
+
+    @Override
+    public void refresh(Object[] keys,Object[] subkeys) {
+        String key = buildKey(keys);
+        String subkey = buildSubKey(subkeys);
+        T t = null;
+        try {
+            t = loadFromResource(keys,subkeys);
+        } catch (StatusException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ExamCloudRuntimeException("fail to load data. key=" + key+" subkey="+subkey, e);
+        }
+        if (null != t) {
+            int timeout = getTimeout();
+            if (timeout < 60) {
+                timeout = 60;
+            }
+            getRedisClient().set(key,subkey, t, timeout);
+        } else {
+            getRedisClient().delete(key,subkey);
+        }
+    }
+}

+ 134 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheProcessor.java

@@ -0,0 +1,134 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis Hash缓存处理器
+ *
+ * @date 2019年5月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class HashRedisCacheProcessor {
+
+	private static RedisClient redisClient;
+
+	private static HashRedisCacheTrigger hashRedisCacheTrigger;
+
+	private static RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	private static HashRedisCacheTrigger getHashRedisCacheTrigger() {
+		if (null == hashRedisCacheTrigger) {
+			hashRedisCacheTrigger = SpringContextHolder.getBean(HashRedisCacheTrigger.class);
+		}
+		return hashRedisCacheTrigger;
+	}
+
+	/**
+	 * 获取缓存对象
+	 *
+	 * @author 
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys,Object[] subpropKeys, Class<T> c) {
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+		String hashKey=StringUtils.join(Arrays.asList(subpropKeys), '_');
+		T t = getRedisClient().get(key,hashKey, c);
+		return t;
+	}
+
+	/**
+	 * 取缓存对象(不存在时远程或本地加载)<br>
+	 * 缓存失效时,只允许一个线程加载缓存,防止缓存击穿<br>
+	 * 缓存加载时长不得超过10秒,否则所有取缓存线程无等待抛出异常,只到缓存被正确加载<br>
+	 * 
+	 *
+	 * @author 
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @param appName
+	 * @param className
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys,Object[] subpropKeys, Class<? extends RandomCacheBean> c,
+			String appName, String className) {
+
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+		String subKey=StringUtils.join(Arrays.asList(subpropKeys), '_');
+
+		RandomCacheBean t = getRedisClient().get(key,subKey, c);
+
+		if (null == t) {
+
+			int count = 0;
+
+			String cacheLock = "$_CACHE_LOCK:" + key+"_"+subKey;
+			String cacheException = "$_CACHE_EXCEPTION:" + key+"_"+subKey;
+
+			while (true) {
+				count++;
+
+				t = getRedisClient().get(key,subKey, c);
+
+				if (null != t) {
+					break;
+				}
+
+				Boolean locked = getRedisClient().setIfAbsent(cacheLock,
+						ThreadLocalUtil.getTraceId(), 30);
+
+				if (locked) {
+					try {
+					    getHashRedisCacheTrigger().fire(appName, className, propKeys,subpropKeys);
+						getRedisClient().delete(cacheException);
+						t = getRedisClient().get(key,subKey, c);
+						break;
+					} catch (Exception e) {
+						getRedisClient().set(cacheException, true, 60);
+						throw e;
+					} finally {
+						getRedisClient().delete(cacheLock);
+					}
+				} else {
+
+					if (null != getRedisClient().get(cacheException, Boolean.class)) {
+						throw new ExamCloudRuntimeException(
+								"exception happened when loading cache. key=" + key+" subKey="+subKey);
+					}
+
+					// 10秒内未加载完缓存,抛出异常
+					if (200 < count) {
+						throw new ExamCloudRuntimeException(
+								"fail to load cache in 10 seconds. key=" + key+" subKey="+subKey);
+					}
+
+					Util.sleep(TimeUnit.MILLISECONDS, 50);
+
+				}
+			}
+
+		}
+
+		@SuppressWarnings("unchecked")
+		T ret = (T) t;
+		return ret;
+	}
+
+}

+ 109 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheTrigger.java

@@ -0,0 +1,109 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.cloud.CloudClientSupport;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis缓存触发器
+ *
+ * @author 
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class HashRedisCacheTrigger extends CloudClientSupport {
+
+	private static Map<String, Boolean> hasClassMap = Maps.newConcurrentMap();
+
+	private static Map<String, HashCache<?>> hashCacheMap = Maps.newConcurrentMap();
+
+	/**
+	 * 开火
+	 *
+	 * @author 
+	 * @param appName
+	 * @param keys
+	 * @return
+	 */
+	public void fire(String appName, String className, Object[] keys,Object[] subkeys) {
+
+		Boolean has = hasClassMap.get(className);
+		HashCache<?> hashCache = null;
+
+		if (null != has) {
+			if (has) {
+				hashCache = hashCacheMap.get(className);
+				hashCache.refresh(keys,subkeys);
+				return;
+			}
+		} else {
+			try {
+				Class<?> c = Class.forName(className);
+				hashCache = (HashCache<?>) SpringContextHolder.getBean(c);
+				hashCache.refresh(keys,subkeys);
+				hashCacheMap.put(className, hashCache);
+				hasClassMap.put(className, true);
+				return;
+			} catch (ClassNotFoundException e) {
+				hasClassMap.put(className, false);
+			}
+		}
+
+		String[] keyArray = new String[keys.length];
+		BasicDataType[] typeArray = new BasicDataType[keys.length];
+
+		for (int i = 0; i < keys.length; i++) {
+			keyArray[i] = String.valueOf(keys[i]);
+			Class<? extends Object> c = keys[i].getClass();
+			if (c.equals(Long.class)) {
+				typeArray[i] = BasicDataType.LONG;
+			} else if (c.equals(String.class)) {
+				typeArray[i] = BasicDataType.STRING;
+			} else if (c.equals(Integer.class)) {
+				typeArray[i] = BasicDataType.INTEGER;
+			} else {
+				throw new ExamCloudRuntimeException("key type is not supported");
+			}
+		}
+		
+		String[] subKeyArray = new String[subkeys.length];
+        BasicDataType[] subTypeArray = new BasicDataType[subkeys.length];
+
+        for (int i = 0; i < subkeys.length; i++) {
+            subKeyArray[i] = String.valueOf(subkeys[i]);
+            Class<? extends Object> c = subkeys[i].getClass();
+            if (c.equals(Long.class)) {
+                subTypeArray[i] = BasicDataType.LONG;
+            } else if (c.equals(String.class)) {
+                subTypeArray[i] = BasicDataType.STRING;
+            } else if (c.equals(Integer.class)) {
+                subTypeArray[i] = BasicDataType.INTEGER;
+            } else {
+                throw new ExamCloudRuntimeException("subkey type is not supported");
+            }
+        }
+
+		RefreshHashCacheReq req = new RefreshHashCacheReq();
+		req.setClassName(className);
+		req.setKeys(keyArray);
+		req.setTypeArray(typeArray);
+		req.setSubKeys(subKeyArray);
+		req.setSubTypeArray(subTypeArray);
+		post(appName, "refreshHash", req);
+
+	}
+
+	@Override
+	protected String getRequestMappingPrefix() {
+		return "cache";
+	}
+
+}

+ 47 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/ObjectCache.java

@@ -0,0 +1,47 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * 对象缓存
+ *
+ * @author WANGWEI
+ * @date 2019年3月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public interface ObjectCache<T> {
+
+	/**
+	 * 从缓存中获取
+	 *
+	 * @author WANGWEI
+	 * @param keys
+	 * @return
+	 */
+	T get(Object... keys);
+
+	/**
+	 * 删除缓存
+	 *
+	 * @author WANGWEI
+	 * @param keys
+	 */
+	void remove(Object... keys);
+
+	/**
+	 * 刷新缓存
+	 *
+	 * @author WANGWEI
+	 * @param keys
+	 */
+	void refresh(Object... keys);
+
+	/**
+	 * 从数据源(数据库,配置文件等)加载单个缓存项
+	 *
+	 * @author WANGWEI
+	 * @param keys
+	 * @return
+	 */
+	T loadFromResource(Object... keys);
+
+}

+ 133 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheProcessor.java

@@ -0,0 +1,133 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis 缓存处理器
+ *
+ * @author WANGWEI
+ * @date 2019年5月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ObjectRedisCacheProcessor {
+
+	private static RedisClient redisClient;
+
+	private static ObjectRedisCacheTrigger objectRedisCacheTrigger;
+
+	private static RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	private static ObjectRedisCacheTrigger getObjectRedisCacheTrigger() {
+		if (null == objectRedisCacheTrigger) {
+			objectRedisCacheTrigger = SpringContextHolder.getBean(ObjectRedisCacheTrigger.class);
+		}
+		return objectRedisCacheTrigger;
+	}
+
+	/**
+	 * 获取缓存对象
+	 *
+	 * @author WANGWEI
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys, Class<T> c) {
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+		T t = getRedisClient().get(key, c);
+		return t;
+	}
+
+	/**
+	 * 取缓存对象(不存在时远程或本地加载)<br>
+	 * 缓存失效时,只允许一个线程加载缓存,防止缓存击穿<br>
+	 * 缓存加载时长不得超过10秒,否则所有取缓存线程无等待抛出异常,只到缓存被正确加载<br>
+	 * 
+	 *
+	 * @author WANGWEI
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @param appName
+	 * @param className
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys, Class<? extends RandomCacheBean> c,
+			String appName, String className) {
+
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+
+		RandomCacheBean t = getRedisClient().get(key, c);
+
+		if (null == t) {
+
+			int count = 0;
+
+			String cacheLock = "$_CACHE_LOCK:" + key;
+			String cacheException = "$_CACHE_EXCEPTION:" + key;
+
+			while (true) {
+				count++;
+
+				t = getRedisClient().get(key, c);
+
+				if (null != t) {
+					break;
+				}
+
+				Boolean locked = getRedisClient().setIfAbsent(cacheLock,
+						ThreadLocalUtil.getTraceId(), 30);
+
+				if (locked) {
+					try {
+						getObjectRedisCacheTrigger().fire(appName, className, propKeys);
+						getRedisClient().delete(cacheException);
+						t = getRedisClient().get(key, c);
+						break;
+					} catch (Exception e) {
+						getRedisClient().set(cacheException, true, 60);
+						throw e;
+					} finally {
+						getRedisClient().delete(cacheLock);
+					}
+				} else {
+
+					if (null != getRedisClient().get(cacheException, Boolean.class)) {
+						throw new ExamCloudRuntimeException(
+								"exception happened when loading cache. key=" + key);
+					}
+
+					// 10秒内未加载完缓存,抛出异常
+					if (200 < count) {
+						throw new ExamCloudRuntimeException(
+								"fail to load cache in 10 seconds. key=" + key);
+					}
+
+					Util.sleep(TimeUnit.MILLISECONDS, 50);
+
+				}
+			}
+
+		}
+
+		@SuppressWarnings("unchecked")
+		T ret = (T) t;
+		return ret;
+	}
+
+}

+ 90 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheTrigger.java

@@ -0,0 +1,90 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.cloud.CloudClientSupport;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis缓存触发器
+ *
+ * @author WANGWEI
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class ObjectRedisCacheTrigger extends CloudClientSupport {
+
+	private static Map<String, Boolean> hasClassMap = Maps.newConcurrentMap();
+
+	private static Map<String, ObjectCache<?>> objectCacheMap = Maps.newConcurrentMap();
+
+	/**
+	 * 开火
+	 *
+	 * @author WANGWEI
+	 * @param appName
+	 * @param keys
+	 * @return
+	 */
+	public void fire(String appName, String className, Object... keys) {
+
+		Boolean has = hasClassMap.get(className);
+		ObjectCache<?> objectCache = null;
+
+		if (null != has) {
+			if (has) {
+				objectCache = objectCacheMap.get(className);
+				objectCache.refresh(keys);
+				return;
+			}
+		} else {
+			try {
+				Class<?> c = Class.forName(className);
+				objectCache = (ObjectCache<?>) SpringContextHolder.getBean(c);
+				objectCache.refresh(keys);
+				objectCacheMap.put(className, objectCache);
+				hasClassMap.put(className, true);
+				return;
+			} catch (ClassNotFoundException e) {
+				hasClassMap.put(className, false);
+			}
+		}
+
+		String[] keyArray = new String[keys.length];
+		BasicDataType[] typeArray = new BasicDataType[keys.length];
+
+		for (int i = 0; i < keys.length; i++) {
+			keyArray[i] = String.valueOf(keys[i]);
+			Class<? extends Object> c = keys[i].getClass();
+			if (c.equals(Long.class)) {
+				typeArray[i] = BasicDataType.LONG;
+			} else if (c.equals(String.class)) {
+				typeArray[i] = BasicDataType.STRING;
+			} else if (c.equals(Integer.class)) {
+				typeArray[i] = BasicDataType.INTEGER;
+			} else {
+				throw new ExamCloudRuntimeException("key type is not supported");
+			}
+		}
+
+		RefreshCacheReq req = new RefreshCacheReq();
+		req.setClassName(className);
+		req.setKeys(keyArray);
+		req.setTypeArray(typeArray);
+		post(appName, "refresh", req);
+
+	}
+
+	@Override
+	protected String getRequestMappingPrefix() {
+		return "cache";
+	}
+
+}

+ 42 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/RandomCacheBean.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.io.Serializable;
+
+/**
+ * 随机缓存基类
+ *
+ * @author WANGWEI
+ * @date 2019年9月5日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public abstract class RandomCacheBean implements Serializable {
+
+	private static final long serialVersionUID = 5409197052989051020L;
+
+	/**
+	 * 版本
+	 */
+	private String version;
+
+	/**
+	 * 缓存是否有值(防缓存穿透)
+	 */
+	private Boolean hasValue = true;
+
+	public String getVersion() {
+		return version;
+	}
+
+	public void setVersion(String version) {
+		this.version = version;
+	}
+
+	public Boolean getHasValue() {
+		return hasValue;
+	}
+
+	public void setHasValue(Boolean hasValue) {
+		this.hasValue = hasValue;
+	}
+
+}

+ 50 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/RandomCacheVersionHelper.java

@@ -0,0 +1,50 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * 随机缓存版本控制
+ *
+ * @author WANGWEI
+ * @date 2019年10月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RandomCacheVersionHelper {
+
+	private static RedisClient redisClient;
+
+	private static RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	/**
+	 * 设置版本
+	 *
+	 * @author WANGWEI
+	 * @param c
+	 * @param version
+	 * @param timeout
+	 */
+	public static void setVersion(Class<? extends RandomCacheBean> c, String version, int timeout) {
+		String key = "$_V_" + c.getCanonicalName();
+		getRedisClient().set(key, version, timeout);
+	}
+
+	/**
+	 * 获取版本
+	 *
+	 * @author WANGWEI
+	 * @param c
+	 * @param version
+	 * @return
+	 */
+	public static String getVersion(Class<? extends RandomCacheBean> c, String version) {
+		String key = "$_V_" + c.getCanonicalName();
+		return getRedisClient().get(key, String.class);
+	}
+
+}

+ 86 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/RandomObjectRedisCache.java

@@ -0,0 +1,86 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * 随机redis缓存
+ *
+ * @author WANGWEI
+ * @date 2019年4月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public abstract class RandomObjectRedisCache<T extends RandomCacheBean> implements ObjectCache<T> {
+
+	private RedisClient redisClient;
+
+	private RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	protected abstract String getKeyPrefix();
+
+	protected abstract int getTimeout();
+
+	protected String buildKey(Object... keys) {
+		String key = getKeyPrefix() + StringUtils.join(Arrays.asList(keys), '_');
+		return key;
+	}
+
+	private T getFromCache(String key) {
+		Object object = getRedisClient().get(key, Object.class);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		return t;
+	}
+
+	@Override
+	public T get(Object... keys) {
+		String key = buildKey(keys);
+		T t = getFromCache(key);
+
+		if (null == t) {
+			refresh(keys);
+		}
+		t = getFromCache(key);
+		return t;
+	}
+
+	@Override
+	public void remove(Object... keys) {
+		String key = buildKey(keys);
+		getRedisClient().delete(key);
+	}
+
+	@Override
+	public void refresh(Object... keys) {
+		String key = buildKey(keys);
+		T t = null;
+		try {
+			t = loadFromResource(keys);
+		} catch (StatusException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new ExamCloudRuntimeException("fail to load data. key=" + key, e);
+		}
+		if (null != t) {
+			int timeout = getTimeout();
+			if (timeout < 60) {
+				timeout = 60;
+			}
+			getRedisClient().set(key, t, timeout);
+		} else {
+			getRedisClient().delete(key);
+		}
+	}
+
+}

+ 47 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/RefreshCacheReq.java

@@ -0,0 +1,47 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.api.commons.exchange.BaseRequest;
+
+/**
+ * 刷新缓存请求
+ *
+ * @author WANGWEI
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RefreshCacheReq extends BaseRequest {
+
+	private static final long serialVersionUID = -6516842403328041136L;
+
+	private String className;
+
+	private String[] keys;
+
+	private BasicDataType[] typeArray;
+
+	public String getClassName() {
+		return className;
+	}
+
+	public void setClassName(String className) {
+		this.className = className;
+	}
+
+	public String[] getKeys() {
+		return keys;
+	}
+
+	public void setKeys(String[] keys) {
+		this.keys = keys;
+	}
+
+	public BasicDataType[] getTypeArray() {
+		return typeArray;
+	}
+
+	public void setTypeArray(BasicDataType[] typeArray) {
+		this.typeArray = typeArray;
+	}
+
+}

+ 76 - 0
src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java

@@ -0,0 +1,76 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.api.commons.exchange.BaseRequest;
+
+/**
+ * 刷新Hash缓存请求
+ *
+ * @author 
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RefreshHashCacheReq extends BaseRequest {
+
+
+	/**
+     * 
+     */
+    private static final long serialVersionUID = 3251806982238101789L;
+
+    private String className;
+
+	private String[] keys;
+
+	private BasicDataType[] typeArray;
+	
+	private String[] subKeys;
+
+    private BasicDataType[] subTypeArray;
+
+	public String getClassName() {
+		return className;
+	}
+
+	public void setClassName(String className) {
+		this.className = className;
+	}
+
+	public String[] getKeys() {
+		return keys;
+	}
+
+	public void setKeys(String[] keys) {
+		this.keys = keys;
+	}
+
+	public BasicDataType[] getTypeArray() {
+		return typeArray;
+	}
+
+	public void setTypeArray(BasicDataType[] typeArray) {
+		this.typeArray = typeArray;
+	}
+
+    
+    public String[] getSubKeys() {
+        return subKeys;
+    }
+
+    
+    public void setSubKeys(String[] subKeys) {
+        this.subKeys = subKeys;
+    }
+
+    
+    public BasicDataType[] getSubTypeArray() {
+        return subTypeArray;
+    }
+
+    
+    public void setSubTypeArray(BasicDataType[] subTypeArray) {
+        this.subTypeArray = subTypeArray;
+    }
+
+
+}

+ 37 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelf.java

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+/**
+ * APP self
+ *
+ * @author WANGWEI
+ * @date 2019年2月18日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface AppSelf {
+
+	/**
+	 * 获取当前APP ID
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	Long getAppId();
+
+	/**
+	 * 获取当前APP识别码<br>
+	 * (通常为1到3位大写字母和数字组合)
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	String getAppCode();
+
+	/**
+	 * 获取当前APP 密钥
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	String getSecretKey();
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelfHolder.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * AppSelf holder
+ *
+ * @author WANGWEI
+ * @date 2019年2月21日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class AppSelfHolder {
+
+	private static AppSelf appSelf;
+
+	/**
+	 * 获取
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static AppSelf get() {
+		if (null == appSelf) {
+			appSelf = SpringContextHolder.getBean(AppSelf.class);
+			if (null == appSelf) {
+				throw new ExamCloudRuntimeException("no AppSelf");
+			}
+		}
+
+		return appSelf;
+	}
+
+	/**
+	 * 设置
+	 *
+	 * @author WANGWEI
+	 * @param appSelf
+	 */
+	public static void set(AppSelf appSelf) {
+		AppSelfHolder.appSelf = appSelf;
+	}
+
+}

+ 145 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientConfiguration.java

@@ -0,0 +1,145 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.cloud.netflix.ribbon.RibbonClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.DefaultResponseErrorHandler;
+import org.springframework.web.client.RestTemplate;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * cloud 配置
+ *
+ * @author WANGWEI
+ * @date 2019年1月28日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+@RibbonClients(defaultConfiguration = RibbonClientsConfiguration.class)
+public class CloudClientConfiguration {
+
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(CloudClientConfiguration.class);
+
+	@Bean
+	@LoadBalanced
+	@Autowired
+	public RestTemplate buildRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
+
+		RestTemplate restTemplate = new RestTemplate();
+		restTemplate.setRequestFactory(clientHttpRequestFactory);
+
+		restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
+			@Override
+			public boolean hasError(ClientHttpResponse response) throws IOException {
+				return false;
+			}
+		});
+
+		return restTemplate;
+	}
+
+	@Bean
+	public ClientHttpRequestFactory buildClientHttpRequestFactory() {
+
+		String httpRequestFactory = PropertyHolder.getString("examcloud.rpc.httpRequestFactory",
+				"OkHttp3");
+
+		if (httpRequestFactory.equals("Simple")) {
+			return buildSimpleClientHttpRequestFactory();
+		} else if (httpRequestFactory.equals("HttpComponents")) {
+			return buildHttpComponentsClientHttpRequestFactory();
+		}
+		if (httpRequestFactory.equals("OkHttp3")) {
+			return buildOkHttp3ClientHttpRequestFactory();
+		} else {
+			DEBUG_LOG.info(
+					"value of property[examcloud.rpc.httpRequestFactory] is wrong. will use default value [OkHttp3] .");
+			return buildOkHttp3ClientHttpRequestFactory();
+		}
+	}
+
+	/**
+	 * Simple
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildSimpleClientHttpRequestFactory() {
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+
+		SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		return clientHttpRequestFactory;
+	}
+
+	/**
+	 * OkHttp3
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildOkHttp3ClientHttpRequestFactory() {
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+		int writeTimeout = PropertyHolder.getInt("examcloud.rpc.writeTimeout", 60000);
+
+		OkHttp3ClientHttpRequestFactory clientHttpRequestFactory = new OkHttp3ClientHttpRequestFactory();
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		clientHttpRequestFactory.setWriteTimeout(writeTimeout);
+		return clientHttpRequestFactory;
+	}
+
+	/**
+	 * HttpComponents
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildHttpComponentsClientHttpRequestFactory() {
+		Boolean poolEnable = PropertyHolder.getBoolean("examcloud.rpc.pool.enable", true);
+
+		int maxTotal = PropertyHolder.getInt("examcloud.rpc.pool.maxTotal", 200);
+		long timeToLive = PropertyHolder.getLong("examcloud.rpc.pool.timeToLive", 60);
+		int defaultMaxPerRoute = PropertyHolder.getInt("examcloud.rpc.pool.defaultMaxPerRoute", 10);
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+
+		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+
+		if (poolEnable) {
+			PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
+					timeToLive, TimeUnit.SECONDS);
+			cm.setValidateAfterInactivity(1000);
+			cm.setMaxTotal(maxTotal);
+			cm.setDefaultMaxPerRoute(defaultMaxPerRoute);
+
+			httpClientBuilder.setConnectionManager(cm);
+		}
+
+		HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
+		clientHttpRequestFactory.setHttpClient(httpClientBuilder.build());
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		return clientHttpRequestFactory;
+	}
+
+}

+ 322 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientSupport.java

@@ -0,0 +1,322 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.commons.collections.CollectionUtils;
+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.MediaType;
+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.api.commons.exchange.BaseRequest;
+import cn.com.qmth.examcloud.api.commons.exchange.FormFilePart;
+import cn.com.qmth.examcloud.api.commons.exchange.FormRequest;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.ByteUtil;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.SHA256;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.config.LogProperties;
+import cn.com.qmth.examcloud.web.exception.ApiFlowLimitedException;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * 云服务客户端基类
+ *
+ * @author WANGWEI
+ * @date 2019年1月25日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public abstract class CloudClientSupport {
+
+	/**
+	 * 接口日志
+	 */
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private RestTemplate restTemplate;
+
+	private LogProperties logProperties;
+
+	private static String[] excludeFields = new String[]{"password", ".*Password"};
+
+	private LogProperties getLogProperties() {
+		if (null == logProperties) {
+			logProperties = SpringContextHolder.getBean(LogProperties.class);
+		}
+		return logProperties;
+	}
+
+	/**
+	 * 获取请求映射前缀
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected abstract String getRequestMappingPrefix();
+
+	/**
+	 * 获取 RestTemplate
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private RestTemplate getRestTemplate() {
+		if (null == restTemplate) {
+			restTemplate = SpringContextHolder.getBean(RestTemplate.class);
+		}
+		return restTemplate;
+	}
+
+	/**
+	 * 构建url
+	 *
+	 * @author WANGWEI
+	 * @param appName
+	 * @param requestMapping
+	 * @return
+	 */
+	private String buildUrl(String appName, String requestMapping) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("http://");
+		String redirection = CloudServiceRedirector.getRedirection(appName);
+		if (null != redirection) {
+			sb.append(redirection);
+		} else {
+			sb.append(appName);
+		}
+		String rmp = getRequestMappingPrefix();
+		if (rmp.startsWith("/")) {
+			sb.append(rmp);
+		} else {
+			sb.append("/").append(rmp);
+		}
+		if ('/' == sb.charAt(sb.length() - 1) && requestMapping.endsWith("/")) {
+			sb.deleteCharAt(sb.length() - 1).append(requestMapping);
+		} else if ('/' != sb.charAt(sb.length() - 1) && !requestMapping.endsWith("/")) {
+			sb.append("/").append(requestMapping);
+		} else {
+			sb.append(requestMapping);
+		}
+
+		return sb.toString();
+	}
+
+	/**
+	 * 获取响应体
+	 * 
+	 * @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 if (HttpStatus.INTERNAL_SERVER_ERROR == respEntity.getStatusCode()) {
+			StatusResponse sr = JsonUtil.fromJson(body, StatusResponse.class);
+			throw new StatusException(sr.getCode(), sr.getDesc());
+		} else if (HttpStatus.SERVICE_UNAVAILABLE == respEntity.getStatusCode()) {
+			StatusResponse sr = JsonUtil.fromJson(body, StatusResponse.class);
+			throw new ApiFlowLimitedException(sr.getCode(), sr.getDesc());
+		} else {
+			throw new ExamCloudRuntimeException(
+					"unexpected http status code [" + respEntity.getStatusCode() + "]");
+		}
+	}
+
+	/**
+	 * 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) {
+		return exchange(url, method, null, body, responseType);
+	}
+
+	/**
+	 * exchange
+	 *
+	 * @author WANGWEI
+	 * @param url
+	 * @param method
+	 * @param httpHeaders
+	 * @param body
+	 * @param responseType
+	 * @return
+	 */
+	protected <T> T exchange(String url, HttpMethod method, HttpHeaders httpHeaders, Object body,
+			Class<T> responseType) {
+
+		long startTime = System.currentTimeMillis();
+
+		if (null == httpHeaders) {
+			httpHeaders = new HttpHeaders();
+		}
+
+		httpHeaders.add("Trace-Id", ThreadLocalUtil.getTraceId());
+		httpHeaders.add("timestamp", String.valueOf(startTime));
+		httpHeaders.add("App-Id", String.valueOf(AppSelfHolder.get().getAppId()));
+		httpHeaders.add("App-Code", String.valueOf(AppSelfHolder.get().getAppCode()));
+
+		String joinStr = StringUtil.join(AppSelfHolder.get().getAppId(),
+				AppSelfHolder.get().getAppCode(), startTime, AppSelfHolder.get().getSecretKey());
+		byte[] bytes = SHA256.encode(joinStr);
+		String accessToken = ByteUtil.toHexAscii(bytes);
+		httpHeaders.add("Access-Token", accessToken);
+
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			INTERFACE_LOG.info("[CALL-IN]. url=" + url);
+			if (null == body) {
+				INTERFACE_LOG.info("[CALL-REQ]. request= void");
+			} else if (body instanceof JsonSerializable) {
+				INTERFACE_LOG.info("[CALL-REQ]. request=" + JsonUtil.toJson(body, excludeFields));
+			}
+		}
+
+		HttpEntity<Object> requestEntity = null;
+		if (null == body) {
+			requestEntity = new HttpEntity<Object>(httpHeaders);
+		} else {
+			requestEntity = new HttpEntity<Object>(body, httpHeaders);
+		}
+
+		T respBody = null;
+		try {
+			ResponseEntity<String> respEntity = getRestTemplate().exchange(url, method,
+					requestEntity, String.class);
+			respBody = getRespBody(respEntity, responseType);
+
+			if (INTERFACE_LOG.isDebugEnabled() && getLogProperties().isNormalResponseLogEnable()) {
+				String respEntityBody = respEntity.getBody();
+				int responseJsonMaxSize = getLogProperties().getResponseLogJsonMaxSize();
+				if (null == respEntityBody) {
+					INTERFACE_LOG.debug("[CALL-RESP]. response= void");
+				} else if (respEntityBody.length() > responseJsonMaxSize) {
+					INTERFACE_LOG.debug("[CALL-RESP]. response= too large");
+				} else {
+					INTERFACE_LOG.debug("[CALL-RESP]. response=" + respEntityBody);
+				}
+			}
+			if (INTERFACE_LOG.isInfoEnabled()) {
+				INTERFACE_LOG.info(StringUtil.join("[CALL-OK]. url=" + url,
+						" ; cost " + (System.currentTimeMillis() - startTime), " ms."));
+			}
+		} catch (ApiFlowLimitedException e) {
+			INTERFACE_LOG.error(StringUtil.join("[CALL-FAIL]. url=" + url,
+					" ; cost " + (System.currentTimeMillis() - startTime), " ms."));
+			INTERFACE_LOG.error("[CALL-RESP]. response=" + e.toJson());
+			throw e;
+		} catch (StatusException e) {
+			INTERFACE_LOG.error(StringUtil.join("[CALL-FAIL]. url=" + url,
+					" ; cost " + (System.currentTimeMillis() - startTime), " ms."));
+			INTERFACE_LOG.error("[CALL-RESP]. response=" + e.toJson());
+			throw e;
+		} catch (Exception e) {
+			INTERFACE_LOG.error(StringUtil.join("[CALL-FATAL]. url=" + url, " ;", e.getMessage()));
+			throw e;
+		}
+
+		return respBody;
+	}
+
+	/**
+	 * post请求
+	 *
+	 * @author WANGWEI
+	 * @param requestMapping
+	 * @param body
+	 * @param responseType
+	 * @return
+	 */
+	protected <T> T post(String appName, String requestMapping, BaseRequest body,
+			Class<T> responseType) {
+		String url = buildUrl(appName, requestMapping);
+		return exchange(url, HttpMethod.POST, body, responseType);
+	}
+
+	/**
+	 * post请求
+	 * 
+	 * @param requestMapping
+	 * @param body
+	 */
+	protected void post(String appName, String requestMapping, BaseRequest body) {
+		String url = buildUrl(appName, requestMapping);
+		exchange(url, HttpMethod.POST, body, null);
+	}
+
+	/**
+	 * post请求
+	 * 
+	 * @param requestMapping
+	 */
+	protected void post(String appName, String requestMapping) {
+		String url = buildUrl(appName, requestMapping);
+		exchange(url, HttpMethod.POST, null, null);
+	}
+
+	/**
+	 * 文件表单提交
+	 *
+	 * @author WANGWEI
+	 * @param requestMapping
+	 * @param req
+	 * @param file
+	 * @param responseType
+	 * @return
+	 * @throws Exception
+	 */
+	public <T> T postForm(String appName, String requestMapping, FormRequest req,
+			Class<T> responseType) {
+		String url = buildUrl(appName, requestMapping);
+
+		MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
+		HttpHeaders httpHeaders = new HttpHeaders();
+		httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+		List<FormFilePart> formFilePartList = req.getFormFilePartList();
+		if (CollectionUtils.isNotEmpty(formFilePartList)) {
+			for (FormFilePart part : formFilePartList) {
+				FileSystemResource resource = new CustomFileSystemResource(part.getFile(),
+						part.getFilename());
+				params.add(part.getParamName(), resource);
+			}
+		}
+
+		String json = JsonUtil.toJson(req);
+		Map<String, String> otherParams = JsonUtil.json2Map(json);
+
+		for (Entry<String, String> entry : otherParams.entrySet()) {
+			params.add(entry.getKey(), entry.getValue());
+		}
+
+		return exchange(url, HttpMethod.POST, httpHeaders, params, responseType);
+	}
+
+}

+ 65 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/CloudServiceRedirector.java

@@ -0,0 +1,65 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+
+/**
+ * 云服务 重定向
+ *
+ * @author WANGWEI
+ * @date 2019年6月5日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class CloudServiceRedirector {
+
+	private static Map<String, String> rules = Maps.newHashMap();
+
+	private static String suffix;
+
+	/**
+	 * 设置重定向映射
+	 *
+	 * @author WANGWEI
+	 * @param originAppName
+	 *            原始服务
+	 * @param targetAppName
+	 *            重定向服务
+	 */
+	public static void setRedirection(String originAppName, String targetAppName) {
+		rules.put(originAppName, targetAppName);
+	}
+
+	/**
+	 * 设置所有服务附加后缀映射.优先级低于 {@link #setRedirection}
+	 * 
+	 * @see #setRedirection
+	 *
+	 * @author WANGWEI
+	 * @param suffixName
+	 */
+	public static void appendSuffix(String suffixName) {
+		CloudServiceRedirector.suffix = suffixName;
+	}
+
+	/**
+	 * 获取重定向映射.不存在时返回null
+	 *
+	 * @author WANGWEI
+	 * @param originAppName
+	 * @return
+	 */
+	public static String getRedirection(String originAppName) {
+
+		String redirection = rules.get(originAppName);
+		if (null != redirection) {
+			return redirection;
+		}
+		if (null != suffix) {
+			return originAppName + suffix;
+		}
+
+		return null;
+	}
+
+}

+ 28 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/CustomFileSystemResource.java

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.io.File;
+
+import org.springframework.core.io.FileSystemResource;
+
+/**
+ * 从定义文件名
+ *
+ * @author WANGWEI
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class CustomFileSystemResource extends FileSystemResource {
+
+	private String redefinedFileName;
+
+	public CustomFileSystemResource(File file, String redefinedFileName) {
+		super(file);
+		this.redefinedFileName = redefinedFileName;
+	}
+
+	@Override
+	public String getFilename() {
+		return redefinedFileName;
+	}
+
+}

+ 67 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/ExamCloudDiscoveryClient.java

@@ -0,0 +1,67 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.stereotype.Component;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+
+/**
+ * 注册中心客户端
+ *
+ * @author WANGWEI
+ * @date 2019年5月28日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class ExamCloudDiscoveryClient {
+
+	@Autowired(required = false)
+	private DiscoveryClient discoveryClient;
+
+	private Cache<String, List<String>> cache = CacheBuilder.newBuilder().maximumSize(100)
+			.expireAfterWrite(30, TimeUnit.SECONDS).concurrencyLevel(10).recordStats().build();
+
+	/**
+	 * 获取实例的URL
+	 *
+	 * @author WANGWEI
+	 * @param appName
+	 * @return
+	 */
+	public String getInstanceUrl(String appName) {
+
+		List<String> urlList = cache.getIfPresent(appName);
+		if (CollectionUtils.isNotEmpty(urlList)) {
+			String url = urlList.get(0);
+			return url;
+		}
+		urlList = Lists.newArrayList();
+
+		List<ServiceInstance> instances = discoveryClient.getInstances(appName);
+		if (instances.isEmpty()) {
+			throw new ExamCloudRuntimeException("no instance!");
+		}
+
+		for (ServiceInstance serviceInstance : instances) {
+			String host = serviceInstance.getHost();
+			int port = serviceInstance.getPort();
+			String url = "http://" + host + ":" + port;
+			urlList.add(url);
+		}
+		cache.put(appName, urlList);
+
+		String url = urlList.get(0);
+		return url;
+	}
+
+}

+ 56 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/RibbonClientsConfiguration.java

@@ -0,0 +1,56 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.netflix.loadbalancer.IPing;
+import com.netflix.loadbalancer.IRule;
+import com.netflix.loadbalancer.PingUrl;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * RibbonClients 配置
+ *
+ * @author WANGWEI
+ * @date 2019年1月28日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+public class RibbonClientsConfiguration {
+
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(CloudClientConfiguration.class);
+
+	@Bean
+	public IRule ribbonRule() {
+
+		String rule = PropertyHolder.getString("examcloud.rpc.loadbalance.rule", "BestAvailableRule");
+
+		if (rule.equals("RoundRobinRule")) {
+			return new com.netflix.loadbalancer.RoundRobinRule();
+		} else if (rule.equals("ZoneAvoidanceRule")) {
+			return new com.netflix.loadbalancer.ZoneAvoidanceRule();
+		} else if (rule.equals("RetryRule")) {
+			return new com.netflix.loadbalancer.RetryRule();
+		} else if (rule.equals("RandomRule")) {
+			return new com.netflix.loadbalancer.RandomRule();
+		} else if (rule.equals("BestAvailableRule")) {
+			return new com.netflix.loadbalancer.BestAvailableRule();
+		} else if (rule.equals("WeightedResponseTimeRule")) {
+			return new com.netflix.loadbalancer.WeightedResponseTimeRule();
+		} else {
+			DEBUG_LOG.info(
+					"value of property[examcloud.rpc.balance.rule] is wrong. will use default value [BestAvailableRule] .");
+			return new com.netflix.loadbalancer.WeightedResponseTimeRule();
+		}
+	}
+
+	@Bean
+	public IPing ribbonPing() {
+		return new PingUrl();
+	}
+
+}

+ 43 - 0
src/main/java/cn/com/qmth/examcloud/web/config/LogProperties.java

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.web.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 日志属性
+ *
+ * @author WANGWEI
+ * @date 2019年3月20日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@ConfigurationProperties("examcloud.web.log")
+public class LogProperties {
+
+	/**
+	 * 是否记录正常相应信息
+	 */
+	private boolean normalResponseLogEnable = true;
+
+	/**
+	 * 正常响应信息json长度限制
+	 */
+	private int responseLogJsonMaxSize = 200;
+
+	public boolean isNormalResponseLogEnable() {
+		return normalResponseLogEnable;
+	}
+
+	public void setNormalResponseLogEnable(boolean normalResponseLogEnable) {
+		this.normalResponseLogEnable = normalResponseLogEnable;
+	}
+
+	public int getResponseLogJsonMaxSize() {
+		return responseLogJsonMaxSize;
+	}
+
+	public void setResponseLogJsonMaxSize(int responseLogJsonMaxSize) {
+		this.responseLogJsonMaxSize = responseLogJsonMaxSize;
+	}
+
+}

+ 98 - 0
src/main/java/cn/com/qmth/examcloud/web/config/SystemProperties.java

@@ -0,0 +1,98 @@
+package cn.com.qmth.examcloud.web.config;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+import cn.com.qmth.examcloud.web.support.ClasspathHelper;
+
+/**
+ * 系统配置
+ *
+ * @author WANGWEI
+ * @date 2018年9月7日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@ConfigurationProperties("examcloud.web.sys")
+public class SystemProperties {
+
+	private String dataDir;
+
+	private String tempDataDir;
+
+	private String logDir;
+
+	/**
+	 * 构造函数
+	 *
+	 */
+	public SystemProperties() {
+
+		if (StringUtils.isBlank(dataDir)) {
+			String classpath = ClasspathHelper.getClasspath();
+			String path = new File(classpath).getParent() + File.separator + "data";
+			dataDir = PathUtil.getCanonicalPath(new File(path));
+		}
+
+		if (StringUtils.isBlank(tempDataDir)) {
+			String classpath = ClasspathHelper.getClasspath();
+			String path = new File(classpath).getParent() + File.separator + "temp";
+			tempDataDir = PathUtil.getCanonicalPath(new File(path));
+		}
+
+		if (StringUtils.isBlank(logDir)) {
+			String classpath = ClasspathHelper.getClasspath();
+			String path = new File(classpath).getParent() + File.separator + "logs";
+			logDir = PathUtil.getCanonicalPath(new File(path));
+		}
+
+		try {
+			FileUtils.forceMkdir(new File(dataDir));
+		} catch (IOException e) {
+			throw new ExamCloudRuntimeException("fail to make data dir. path=" + dataDir);
+		}
+
+		try {
+			FileUtils.forceMkdir(new File(tempDataDir));
+		} catch (IOException e) {
+			throw new ExamCloudRuntimeException("fail to make temp data dir. path=" + tempDataDir);
+		}
+
+		System.setProperty("dataDir", dataDir);
+		System.setProperty("empDataDir", tempDataDir);
+		System.setProperty("logDir", logDir);
+
+	}
+
+	public String getDataDir() {
+		return dataDir;
+	}
+
+	public void setDataDir(String dataDir) {
+		this.dataDir = dataDir;
+	}
+
+	public String getTempDataDir() {
+		return tempDataDir;
+	}
+
+	public void setTempDataDir(String tempDataDir) {
+		this.tempDataDir = tempDataDir;
+	}
+
+	public String getLogDir() {
+		return logDir;
+	}
+
+	public void setLogDir(String logDir) {
+		this.logDir = logDir;
+	}
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceAutoConfigure.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.web.druid;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import com.alibaba.druid.pool.DruidDataSource;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+import cn.com.qmth.examcloud.web.druid.stat.DruidFilterConfiguration;
+import cn.com.qmth.examcloud.web.druid.stat.DruidSpringAopConfiguration;
+import cn.com.qmth.examcloud.web.druid.stat.DruidStatViewServletConfiguration;
+import cn.com.qmth.examcloud.web.druid.stat.DruidWebStatFilterConfiguration;
+
+@Configuration
+@ConditionalOnClass(DruidDataSource.class)
+@AutoConfigureBefore(DataSourceAutoConfiguration.class)
+@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
+@Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class,
+		DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class})
+public class DruidDataSourceAutoConfigure {
+
+	private static final Logger LOGGER = LoggerFactory
+			.getLogger(DruidDataSourceAutoConfigure.class);
+
+	@Bean(initMethod = "init")
+	@ConditionalOnMissingBean
+	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.alibaba.druid.pool.DruidDataSource", matchIfMissing = false)
+	public DataSource dataSource() {
+		LOGGER.info("Init DruidDataSource");
+		return new DruidDataSourceWrapper();
+	}
+}

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceBuilder.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.web.druid;
+
+import com.alibaba.druid.pool.DruidDataSource;
+
+public class DruidDataSourceBuilder {
+
+	public static DruidDataSourceBuilder create() {
+		return new DruidDataSourceBuilder();
+	}
+
+	/**
+	 * For build multiple DruidDataSource, detail see document.
+	 *
+	 * ------- The method is history, and now you can use 'new
+	 * DruidDataSourceWrapper()' instead.
+	 */
+	public DruidDataSource build() {
+		return new DruidDataSourceWrapper();
+	}
+
+}

+ 98 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceWrapper.java

@@ -0,0 +1,98 @@
+package cn.com.qmth.examcloud.web.druid;
+
+import com.alibaba.druid.filter.config.ConfigFilter;
+import com.alibaba.druid.filter.encoding.EncodingConvertFilter;
+import com.alibaba.druid.filter.logging.CommonsLogFilter;
+import com.alibaba.druid.filter.logging.Log4j2Filter;
+import com.alibaba.druid.filter.logging.Log4jFilter;
+import com.alibaba.druid.filter.logging.Slf4jLogFilter;
+import com.alibaba.druid.filter.stat.StatFilter;
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.wall.WallFilter;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("spring.datasource.druid")
+class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean {
+
+	private static final long serialVersionUID = -1485354381332792381L;
+
+	@Autowired
+	private DataSourceProperties basicProperties;
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		if (super.getUsername() == null) {
+			super.setUsername(basicProperties.determineUsername());
+		}
+		if (super.getPassword() == null) {
+			super.setPassword(basicProperties.determinePassword());
+		}
+		if (super.getUrl() == null) {
+			super.setUrl(basicProperties.determineUrl());
+		}
+		if (super.getDriverClassName() == null) {
+			super.setDriverClassName(basicProperties.getDriverClassName());
+		}
+	}
+
+	@Autowired(required = false)
+	public void addStatFilter(StatFilter statFilter) {
+		super.filters.add(statFilter);
+	}
+
+	@Autowired(required = false)
+	public void addConfigFilter(ConfigFilter configFilter) {
+		super.filters.add(configFilter);
+	}
+
+	@Autowired(required = false)
+	public void addEncodingConvertFilter(EncodingConvertFilter encodingConvertFilter) {
+		super.filters.add(encodingConvertFilter);
+	}
+
+	@Autowired(required = false)
+	public void addSlf4jLogFilter(Slf4jLogFilter slf4jLogFilter) {
+		super.filters.add(slf4jLogFilter);
+	}
+
+	@Autowired(required = false)
+	public void addLog4jFilter(Log4jFilter log4jFilter) {
+		super.filters.add(log4jFilter);
+	}
+
+	@Autowired(required = false)
+	public void addLog4j2Filter(Log4j2Filter log4j2Filter) {
+		super.filters.add(log4j2Filter);
+	}
+
+	@Autowired(required = false)
+	public void addCommonsLogFilter(CommonsLogFilter commonsLogFilter) {
+		super.filters.add(commonsLogFilter);
+	}
+
+	@Autowired(required = false)
+	public void addWallFilter(WallFilter wallFilter) {
+		super.filters.add(wallFilter);
+	}
+
+	/**
+	 * Ignore the 'maxEvictableIdleTimeMillis < minEvictableIdleTimeMillis'
+	 * validate, it will be validated again in {@link DruidDataSource#init()}.
+	 *
+	 * for fix issue #3084, #2763
+	 *
+	 * @since 1.1.14
+	 */
+	@Override
+	public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
+		try {
+			super.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+		} catch (IllegalArgumentException ignore) {
+			super.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
+		}
+	}
+}

+ 72 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/RemoveDruidAdConfig.java

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.web.druid;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.alibaba.druid.util.Utils;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+
+/**
+ * 去除druid监控页面底部的广告
+ */
+@Configuration
+@ConditionalOnWebApplication
+@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
+@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true", matchIfMissing = true)
+public class RemoveDruidAdConfig {
+
+	@Bean
+	public FilterRegistrationBean<?> removeDruidAdFilterRegistrationBean(
+			DruidStatProperties properties) {
+		// 获取web监控页面的参数
+		DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+		// 提取common.js的配置路径
+		String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+		String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+
+		final String filePath = "support/http/resources/js/common.js";
+
+		// 创建filter进行过滤
+		Filter filter = new Filter() {
+			@Override
+			public void init(FilterConfig filterConfig) throws ServletException {
+			}
+
+			@Override
+			public void doFilter(ServletRequest request, ServletResponse response,
+					FilterChain chain) throws IOException, ServletException {
+				chain.doFilter(request, response);
+				// 重置缓冲区,响应头不会被重置
+				response.resetBuffer();
+				// 获取common.js
+				String text = Utils.readFromResource(filePath);
+				// 正则替换banner, 除去底部的广告信息
+				text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+				text = text.replaceAll("powered.*?shrek.wang</a>", "");
+				response.getWriter().write(text);
+			}
+
+			@Override
+			public void destroy() {
+			}
+		};
+		FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
+		registrationBean.setFilter(filter);
+		registrationBean.addUrlPatterns(commonJsPattern);
+		return registrationBean;
+	}
+}

+ 196 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/properties/DruidStatProperties.java

@@ -0,0 +1,196 @@
+package cn.com.qmth.examcloud.web.druid.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("spring.datasource.druid")
+public class DruidStatProperties {
+	private String[] aopPatterns;
+
+	private StatViewServlet statViewServlet = new StatViewServlet();
+
+	private WebStatFilter webStatFilter = new WebStatFilter();
+
+	public String[] getAopPatterns() {
+		return aopPatterns;
+	}
+
+	public void setAopPatterns(String[] aopPatterns) {
+		this.aopPatterns = aopPatterns;
+	}
+
+	public StatViewServlet getStatViewServlet() {
+		return statViewServlet;
+	}
+
+	public void setStatViewServlet(StatViewServlet statViewServlet) {
+		this.statViewServlet = statViewServlet;
+	}
+
+	public WebStatFilter getWebStatFilter() {
+		return webStatFilter;
+	}
+
+	public void setWebStatFilter(WebStatFilter webStatFilter) {
+		this.webStatFilter = webStatFilter;
+	}
+
+	public static class StatViewServlet {
+		/**
+		 * Enable StatViewServlet, default false.
+		 */
+		private boolean enabled;
+
+		private String urlPattern;
+
+		private String allow;
+
+		private String deny;
+
+		private String loginUsername;
+
+		private String loginPassword;
+
+		private String resetEnable;
+
+		public boolean isEnabled() {
+			return enabled;
+		}
+
+		public void setEnabled(boolean enabled) {
+			this.enabled = enabled;
+		}
+
+		public String getUrlPattern() {
+			return urlPattern;
+		}
+
+		public void setUrlPattern(String urlPattern) {
+			this.urlPattern = urlPattern;
+		}
+
+		public String getAllow() {
+			return allow;
+		}
+
+		public void setAllow(String allow) {
+			this.allow = allow;
+		}
+
+		public String getDeny() {
+			return deny;
+		}
+
+		public void setDeny(String deny) {
+			this.deny = deny;
+		}
+
+		public String getLoginUsername() {
+			return loginUsername;
+		}
+
+		public void setLoginUsername(String loginUsername) {
+			this.loginUsername = loginUsername;
+		}
+
+		public String getLoginPassword() {
+			return loginPassword;
+		}
+
+		public void setLoginPassword(String loginPassword) {
+			this.loginPassword = loginPassword;
+		}
+
+		public String getResetEnable() {
+			return resetEnable;
+		}
+
+		public void setResetEnable(String resetEnable) {
+			this.resetEnable = resetEnable;
+		}
+	}
+
+	public static class WebStatFilter {
+		/**
+		 * Enable WebStatFilter, default false.
+		 */
+		private boolean enabled;
+
+		private String urlPattern;
+
+		private String exclusions;
+
+		private String sessionStatMaxCount;
+
+		private String sessionStatEnable;
+
+		private String principalSessionName;
+
+		private String principalCookieName;
+
+		private String profileEnable;
+
+		public boolean isEnabled() {
+			return enabled;
+		}
+
+		public void setEnabled(boolean enabled) {
+			this.enabled = enabled;
+		}
+
+		public String getUrlPattern() {
+			return urlPattern;
+		}
+
+		public void setUrlPattern(String urlPattern) {
+			this.urlPattern = urlPattern;
+		}
+
+		public String getExclusions() {
+			return exclusions;
+		}
+
+		public void setExclusions(String exclusions) {
+			this.exclusions = exclusions;
+		}
+
+		public String getSessionStatMaxCount() {
+			return sessionStatMaxCount;
+		}
+
+		public void setSessionStatMaxCount(String sessionStatMaxCount) {
+			this.sessionStatMaxCount = sessionStatMaxCount;
+		}
+
+		public String getSessionStatEnable() {
+			return sessionStatEnable;
+		}
+
+		public void setSessionStatEnable(String sessionStatEnable) {
+			this.sessionStatEnable = sessionStatEnable;
+		}
+
+		public String getPrincipalSessionName() {
+			return principalSessionName;
+		}
+
+		public void setPrincipalSessionName(String principalSessionName) {
+			this.principalSessionName = principalSessionName;
+		}
+
+		public String getPrincipalCookieName() {
+			return principalCookieName;
+		}
+
+		public void setPrincipalCookieName(String principalCookieName) {
+			this.principalCookieName = principalCookieName;
+		}
+
+		public String getProfileEnable() {
+			return profileEnable;
+		}
+
+		public void setProfileEnable(String profileEnable) {
+			this.profileEnable = profileEnable;
+		}
+	}
+}

+ 108 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidFilterConfiguration.java

@@ -0,0 +1,108 @@
+package cn.com.qmth.examcloud.web.druid.stat;
+
+import com.alibaba.druid.filter.config.ConfigFilter;
+import com.alibaba.druid.filter.encoding.EncodingConvertFilter;
+import com.alibaba.druid.filter.logging.*;
+import com.alibaba.druid.filter.stat.StatFilter;
+import com.alibaba.druid.wall.WallConfig;
+import com.alibaba.druid.wall.WallFilter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+public class DruidFilterConfiguration {
+
+	@Bean
+	@ConfigurationProperties(FILTER_STAT_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_STAT_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public StatFilter statFilter() {
+		return new StatFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_CONFIG_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_CONFIG_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public ConfigFilter configFilter() {
+		return new ConfigFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_ENCODING_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_ENCODING_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public EncodingConvertFilter encodingConvertFilter() {
+		return new EncodingConvertFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_SLF4J_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_SLF4J_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public Slf4jLogFilter slf4jLogFilter() {
+		return new Slf4jLogFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_LOG4J_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_LOG4J_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public Log4jFilter log4jFilter() {
+		return new Log4jFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_LOG4J2_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_LOG4J2_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public Log4j2Filter log4j2Filter() {
+		return new Log4j2Filter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_COMMONS_LOG_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_COMMONS_LOG_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public CommonsLogFilter commonsLogFilter() {
+		return new CommonsLogFilter();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_WALL_CONFIG_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_WALL_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public WallConfig wallConfig() {
+		return new WallConfig();
+	}
+
+	@Bean
+	@ConfigurationProperties(FILTER_WALL_PREFIX)
+	@ConditionalOnProperty(prefix = FILTER_WALL_PREFIX, name = "enabled")
+	@ConditionalOnMissingBean
+	public WallFilter wallFilter(WallConfig wallConfig) {
+		WallFilter filter = new WallFilter();
+		filter.setConfig(wallConfig);
+		return filter;
+	}
+
+	private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
+
+	private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
+
+	private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
+
+	private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
+
+	private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
+
+	private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
+
+	private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
+
+	private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
+
+	private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
+
+}

+ 33 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidSpringAopConfiguration.java

@@ -0,0 +1,33 @@
+package cn.com.qmth.examcloud.web.druid.stat;
+
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Advisor;
+import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
+import org.springframework.aop.support.RegexpMethodPointcutAdvisor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+
+import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+
+@ConditionalOnProperty("spring.datasource.druid.aop-patterns")
+public class DruidSpringAopConfiguration {
+
+	@Bean
+	public Advice advice() {
+		return new DruidStatInterceptor();
+	}
+
+	@Bean
+	public Advisor advisor(DruidStatProperties properties) {
+		return new RegexpMethodPointcutAdvisor(properties.getAopPatterns(), advice());
+	}
+
+	@Bean
+	public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
+		DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
+		advisorAutoProxyCreator.setProxyTargetClass(true);
+		return advisorAutoProxyCreator;
+	}
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidStatViewServletConfiguration.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.web.druid.stat;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+import com.alibaba.druid.support.http.StatViewServlet;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+
+@ConditionalOnWebApplication
+@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
+public class DruidStatViewServletConfiguration {
+
+	@Bean
+	public ServletRegistrationBean<StatViewServlet> statViewServletRegistrationBean(
+			DruidStatProperties properties) {
+		DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+		ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>();
+		registrationBean.setServlet(new StatViewServlet());
+		registrationBean.addUrlMappings(
+				config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*");
+		if (config.getAllow() != null) {
+			registrationBean.addInitParameter("allow", config.getAllow());
+		}
+		if (config.getDeny() != null) {
+			registrationBean.addInitParameter("deny", config.getDeny());
+		}
+		if (config.getLoginUsername() != null) {
+			registrationBean.addInitParameter("loginUsername", config.getLoginUsername());
+		}
+		if (config.getLoginPassword() != null) {
+			registrationBean.addInitParameter("loginPassword", config.getLoginPassword());
+		}
+		if (config.getResetEnable() != null) {
+			registrationBean.addInitParameter("resetEnable", config.getResetEnable());
+		}
+		return registrationBean;
+	}
+}

+ 48 - 0
src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidWebStatFilterConfiguration.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.examcloud.web.druid.stat;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+import com.alibaba.druid.support.http.WebStatFilter;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+
+@ConditionalOnWebApplication
+@ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true")
+public class DruidWebStatFilterConfiguration {
+	@Bean
+	public FilterRegistrationBean<WebStatFilter> webStatFilterRegistrationBean(
+			DruidStatProperties properties) {
+		DruidStatProperties.WebStatFilter config = properties.getWebStatFilter();
+		FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>();
+		WebStatFilter filter = new WebStatFilter();
+		registrationBean.setFilter(filter);
+		registrationBean
+				.addUrlPatterns(config.getUrlPattern() != null ? config.getUrlPattern() : "/*");
+		registrationBean.addInitParameter("exclusions",
+				config.getExclusions() != null
+						? config.getExclusions()
+						: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
+		if (config.getSessionStatEnable() != null) {
+			registrationBean.addInitParameter("sessionStatEnable", config.getSessionStatEnable());
+		}
+		if (config.getSessionStatMaxCount() != null) {
+			registrationBean.addInitParameter("sessionStatMaxCount",
+					config.getSessionStatMaxCount());
+		}
+		if (config.getPrincipalSessionName() != null) {
+			registrationBean.addInitParameter("principalSessionName",
+					config.getPrincipalSessionName());
+		}
+		if (config.getPrincipalCookieName() != null) {
+			registrationBean.addInitParameter("principalCookieName",
+					config.getPrincipalCookieName());
+		}
+		if (config.getProfileEnable() != null) {
+			registrationBean.addInitParameter("profileEnable", config.getProfileEnable());
+		}
+		return registrationBean;
+	}
+}

+ 62 - 0
src/main/java/cn/com/qmth/examcloud/web/enums/HttpServletRequestAttribute.java

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.web.enums;
+
+/**
+ * servlet请求属性
+ *
+ * @author WANGWEI
+ * @date 2019年3月21日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum HttpServletRequestAttribute {
+
+	/**
+	 * 请求映射
+	 */
+	$_MAPPING,
+
+	/**
+	 * API 信息
+	 */
+	$_API_INFO,
+
+	/**
+	 * http status恒为200
+	 */
+	$_ALWAYS_OK,
+
+	/**
+	 * 接入用户
+	 */
+	$_ACCESS_USER,
+
+	/**
+	 * 自定义顺序锁
+	 */
+	$_CUSTOM_SEQUENCE_LOCK,
+
+	/**
+	 * 已鉴权(其他拦截器处理)
+	 */
+	$_AUTHORIZED_BY_OTHER_INTERCEPTOR,
+
+	/**
+	 * 企业顶级机构(对外服务接口)
+	 */
+	$_ENTERPRISE_ROOT_ORG_ID,
+
+	/**
+	 * 接口调用异常
+	 */
+	$_EXCEPTION_HAPPENED,
+
+	/**
+	 * METRICS Timer context
+	 */
+	$_METRICS_TIMER_CTX,
+
+	/**
+	 * ApiStatisticInterceptor 开始时间
+	 */
+	API_STATISTIC_INTERCEPTOR_START_TIME,
+
+}

+ 24 - 0
src/main/java/cn/com/qmth/examcloud/web/exception/ApiFlowLimitedException.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.web.exception;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+
+/**
+ * 接口限流异常
+ *
+ * @author WANGWEI
+ * @date 2019年8月6日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ApiFlowLimitedException extends StatusException {
+
+	private static final long serialVersionUID = -3088431872660870243L;
+
+	public ApiFlowLimitedException(String code, String desc) {
+		super(code, desc);
+	}
+
+	public ApiFlowLimitedException(String code, String desc, Throwable cause) {
+		super(code, desc, cause);
+	}
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/web/exception/SequenceLockException.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.web.exception;
+
+/**
+ * 顺序请求异常
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public class SequenceLockException extends RuntimeException {
+
+	private static final long serialVersionUID = 6799267531884646906L;
+
+	public SequenceLockException() {
+		super();
+	}
+
+	public SequenceLockException(String message) {
+		super(message);
+	}
+
+}

+ 425 - 0
src/main/java/cn/com/qmth/examcloud/web/facepp/FaceppClient.java

@@ -0,0 +1,425 @@
+package cn.com.qmth.examcloud.web.facepp;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * face++ 客户端
+ *
+ * @author WANGWEI
+ * @date 2019年9月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class FaceppClient {
+
+	protected static ExamCloudLog log = ExamCloudLogFactory.getLog(FaceppClient.class);
+
+	private static CloseableHttpClient httpclient;
+
+	private static RequestConfig requestConfig;
+
+	private static FaceppClient faceppClient;
+
+	private static String apiKey;
+
+	private static String apiSecret;
+
+	private static String compareUrl = "https://api-cn.faceplusplus.com/facepp/v3/compare";
+
+	private FaceppClient() {
+	}
+
+	/**
+	 * 获取单例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static FaceppClient getClient() {
+		if (null == faceppClient) {
+			synchronized (FaceppClient.class) {
+				if (null == faceppClient) {
+					faceppClient = new FaceppClient();
+				}
+			}
+		}
+
+		return faceppClient;
+	}
+
+	static {
+		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(60,
+				TimeUnit.SECONDS);
+		cm.setValidateAfterInactivity(1000);
+		cm.setMaxTotal(8000);
+		cm.setDefaultMaxPerRoute(200);
+
+		requestConfig = RequestConfig.custom().setConnectionRequestTimeout(500)
+				.setSocketTimeout(20000).setConnectTimeout(20000)
+				.setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
+
+		httpclient = HttpClients.custom().setConnectionManager(cm).disableAutomaticRetries()
+				.setDefaultRequestConfig(requestConfig).build();
+
+		apiKey = PropertyHolder.getString("$facepp.apiKey");
+		apiSecret = PropertyHolder.getString("$facepp.apiSecret");
+
+		if (StringUtils.isBlank(apiKey)) {
+			log.error("'facepp.apiKey' is not configured");
+		}
+		if (StringUtils.isBlank(apiSecret)) {
+			log.error("'facepp.apiSecret' is not configured");
+		}
+	}
+
+	/**
+	 * 人脸识别
+	 *
+	 * @author WANGWEI
+	 * @param faceToken
+	 * @param imageUrl
+	 * @return
+	 */
+	public JsonHttpResponseHolder compareWithTokenAndImageUrl(String faceToken, String imageUrl) {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++ Request]. faceToken=" + faceToken + "; imageUrl=" + imageUrl);
+		}
+
+		String url = System.getProperty("facepp.compare.url");
+		if (StringUtils.isBlank(url)) {
+			url = compareUrl;
+		}
+
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(FaceppClient.requestConfig);
+
+		MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+		builder.addTextBody("api_key", apiKey);
+		builder.addTextBody("api_secret", apiSecret);
+		builder.addTextBody("face_token1", faceToken);
+		builder.addTextBody("image_url2", imageUrl);
+		HttpEntity httpEntity = builder.build();
+
+		httpPost.setEntity(httpEntity);
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		long s = System.currentTimeMillis();
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Face++ FAIL]. cost " + (System.currentTimeMillis() - s) + " ms.", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++]. faceToken=" + faceToken + "; imageUrl=" + imageUrl + "; cost "
+					+ (System.currentTimeMillis() - s) + " ms.");
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 人脸识别
+	 *
+	 * @author WANGWEI
+	 * @param faceToken
+	 * @param imageBase64
+	 * @return
+	 */
+	public JsonHttpResponseHolder compareWithTokenAndBase64(String faceToken, String imageBase64) {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++ Request]. faceToken=" + faceToken + "; imageBase64=?");
+		}
+
+		String url = System.getProperty("facepp.compare.url");
+		if (StringUtils.isBlank(url)) {
+			url = compareUrl;
+		}
+
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(FaceppClient.requestConfig);
+
+		MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+		builder.addTextBody("api_key", apiKey);
+		builder.addTextBody("api_secret", apiSecret);
+		builder.addTextBody("face_token1", faceToken);
+		builder.addTextBody("image_base64_2", imageBase64);
+		HttpEntity httpEntity = builder.build();
+
+		httpPost.setEntity(httpEntity);
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		long s = System.currentTimeMillis();
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Face++ FAIL].  cost " + (System.currentTimeMillis() - s) + " ms.", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++]. faceToken=" + faceToken + "; imageBase64=?; cost "
+					+ (System.currentTimeMillis() - s) + " ms.");
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 人脸识别
+	 *
+	 * @author WANGWEI
+	 * @param imageUrl1
+	 * @param imageUrl2
+	 * @return
+	 */
+	public JsonHttpResponseHolder compareWithImageUrl(String imageUrl1, String imageUrl2) {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++ Request]. imageUrl1=" + imageUrl1 + "; imageUrl2=" + imageUrl2);
+		}
+
+		String url = System.getProperty("facepp.compare.url");
+		if (StringUtils.isBlank(url)) {
+			url = compareUrl;
+		}
+
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setConfig(FaceppClient.requestConfig);
+
+		MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+		builder.addTextBody("api_key", apiKey);
+		builder.addTextBody("api_secret", apiSecret);
+		builder.addTextBody("image_url1", imageUrl1);
+		builder.addTextBody("image_url2", imageUrl2);
+		HttpEntity httpEntity = builder.build();
+
+		httpPost.setEntity(httpEntity);
+
+		CloseableHttpResponse response = null;
+		JsonHttpResponseHolder responseHolder = null;
+		long s = System.currentTimeMillis();
+		try {
+
+			response = httpclient.execute(httpPost);
+			int statusCode = response.getStatusLine().getStatusCode();
+			String entityStr = EntityUtils.toString(response.getEntity(), "UTF-8");
+			JSONObject obj = JSON.parseObject(entityStr);
+			responseHolder = new JsonHttpResponseHolder(statusCode, obj);
+
+			if (HttpStatus.SC_OK != responseHolder.getStatusCode()) {
+				log.error("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+						+ entityStr);
+			} else {
+				if (log.isDebugEnabled()) {
+					log.debug("[Face++ Response]. statusCode=" + statusCode + "; responseEntity="
+							+ entityStr);
+				}
+			}
+
+		} catch (Exception e) {
+			log.error("[Face++ FAIL]. cost " + (System.currentTimeMillis() - s) + " ms.", e);
+			throw new ExamCloudRuntimeException(e);
+		} finally {
+			IOUtils.closeQuietly(response);
+		}
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++]. image_url1=" + imageUrl1 + "; imageUrl2=" + imageUrl2 + "; cost "
+					+ (System.currentTimeMillis() - s) + " ms.");
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 人脸识别<br>
+	 * 优先使用主地址调用face++<br>
+	 * 主地址无效时,使用备用地址调用face++.<br>
+	 * 备用地址也无效时,使用备用地址下载数据,使用下载数据的base64加密串调用face++<br>
+	 *
+	 * @author WANGWEI
+	 * 
+	 * @param faceToken
+	 *            face++预存照片
+	 * @param imageUrl
+	 *            主地址
+	 * @param backupImageUrl
+	 *            备用地址
+	 * @return
+	 * @throws StatusException
+	 *             code为801,802,803表示图片地址无效
+	 */
+	public JsonHttpResponseHolder compareWithTokenAndImageUrl(String faceToken, String imageUrl,
+			String backupImageUrl) throws StatusException {
+
+		if (log.isDebugEnabled()) {
+			log.debug("[Face++ Request]. faceToken=" + faceToken + "; imageUrl=" + imageUrl
+					+ "; backupImageUrl=" + backupImageUrl);
+		}
+
+		JsonHttpResponseHolder responseHolder = null;
+
+		boolean exceptionWhenUsingImageUrl = false;
+		boolean exceptionWhenUsingBackupImageUrl = false;
+
+		try {
+			responseHolder = compareWithTokenAndImageUrl(faceToken, imageUrl);
+		} catch (ExamCloudRuntimeException e) {
+			exceptionWhenUsingImageUrl = true;
+		}
+
+		if (exceptionWhenUsingImageUrl) {
+			try {
+				responseHolder = compareWithTokenAndImageUrl(faceToken, backupImageUrl);
+			} catch (ExamCloudRuntimeException e) {
+				exceptionWhenUsingBackupImageUrl = true;
+			}
+		} else {
+			if (HttpStatus.SC_OK == responseHolder.getStatusCode()) {
+				return responseHolder;
+			}
+
+			String errMsg = responseHolder.getRespBody().getString("error_message");
+			if (retry(errMsg)) {
+				try {
+					responseHolder = compareWithTokenAndImageUrl(faceToken, backupImageUrl);
+				} catch (ExamCloudRuntimeException e) {
+					exceptionWhenUsingBackupImageUrl = true;
+				}
+			}
+
+		}
+
+		if (HttpStatus.SC_OK == responseHolder.getStatusCode()) {
+			return responseHolder;
+		}
+
+		String errMsg = responseHolder.getRespBody().getString("error_message");
+
+		if (exceptionWhenUsingBackupImageUrl || retry(errMsg)) {
+			HttpGet get = new HttpGet(backupImageUrl);
+			get.setConfig(FaceppClient.requestConfig);
+			CloseableHttpResponse response = null;
+			String imageBase64 = null;
+			long s = System.currentTimeMillis();
+			try {
+				response = httpclient.execute(get);
+
+				if (HttpStatus.SC_OK != response.getStatusLine().getStatusCode()) {
+					throw new StatusException("801",
+							"fail to download image file. url=" + backupImageUrl);
+				}
+
+				byte[] byteArray = EntityUtils.toByteArray(response.getEntity());
+				if (100 > byteArray.length) {
+					throw new StatusException("802", "invalid image size. url=" + backupImageUrl);
+				}
+
+				imageBase64 = Base64.encodeBase64String(byteArray);
+
+			} catch (StatusException e) {
+				log.error("fail to download image file. url=" + backupImageUrl, e);
+				throw e;
+			} catch (Exception e) {
+				log.error("fail to download image file. url=" + backupImageUrl, e);
+				throw new StatusException("803", "fail to download file. url=" + backupImageUrl, e);
+			} finally {
+				IOUtils.closeQuietly(response);
+			}
+
+			if (log.isDebugEnabled()) {
+				log.debug("download image file successfully; url=" + backupImageUrl + "; cost "
+						+ (System.currentTimeMillis() - s) + " ms.");
+			}
+
+			responseHolder = compareWithTokenAndBase64(faceToken, imageBase64);
+		}
+
+		return responseHolder;
+	}
+
+	/**
+	 * 是否重试
+	 *
+	 * @author WANGWEI
+	 * @param errMsg
+	 * @return
+	 */
+	private boolean retry(String errMsg) {
+		if (null != errMsg) {
+			if (errMsg.startsWith("INVALID_IMAGE_URL")
+					|| errMsg.startsWith("IMAGE_DOWNLOAD_TIMEOUT")) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+}

+ 59 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import java.io.File;
+import java.io.InputStream;
+
+public interface FileStorage {
+	
+	/**保存文件到存储服务
+	 * @param siteId 
+	 * @param env
+	 * @param file 文件流
+	 * @param md5 文件MD5 可为空
+	 * @return 返回路径
+	 */
+	public YunPathInfo saveFile(String siteId,FileStoragePathEnvInfo env,InputStream in,String md5);
+	
+	/**保存文件到存储服务
+	 * @param siteId 
+	 * @param env
+	 * @param file 文件
+	 * @param md5 文件MD5 可为空
+	 * @return 返回路径
+	 */
+	
+	public YunPathInfo saveFile(String siteId,FileStoragePathEnvInfo env,File file,String md5);
+	
+	/**获取可直接访问的文件地址
+	 * @param path 全路径,包含根目录,含协议名
+	 * @return 返回可直接访问的地址
+	 */
+	public String realPath(String path);
+	
+	/**获取可直接访问的文件地址(备用域名地址)
+	 * @param path 全路径,包含根目录,含协议名
+	 * @return 返回可直接访问的地址,如果没有配置备用地址,则返回主域名地址
+	 */
+	public String realPathBackup(String path);
+	
+	/**保存文件到存储服务,siteId为1,转换接口用
+	 * @param file 文件
+	 * @param path 全路径,含协议名
+	 * @return 返回路径
+	 */
+	public YunPathInfo saveFile(File file, String path);
+	
+	/**获取云存储签名
+	 * @param siteId
+	 * @param env
+	 * @param md5
+	 * @return
+	 */
+	public YunHttpRequest  getSignature(String siteId,FileStoragePathEnvInfo env,String md5);
+	
+	/**删除文件
+	 * @param path 全路径,含协议名
+	 */
+	public void deleteFile(String path) ;
+	
+}

+ 79 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageHelper.java

@@ -0,0 +1,79 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import org.apache.commons.lang3.StringUtils;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+
+public class FileStorageHelper {
+	private static String connector = "://";
+	
+	public static String getUpyunSiteOne(String path) {
+		return FileStorageType.UPYUN.name().toLowerCase() +"-1"+ connector + path;
+	}
+
+	public static String getUrl(String domain, String path) {
+		if (StringUtils.isBlank(domain)) {
+			return null;
+		}
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		if (path.startsWith("/")) {
+			path = path.substring(1);
+		}
+		if (domain.endsWith("/")) {
+			domain = domain.substring(0, domain.length());
+		}
+		return domain + "/" + path;
+	}
+
+	public static String getYunId(String path) {
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		if (path.indexOf(connector) == -1) {
+			throw new StatusException("7001", "文件路径格式错误:" + path);
+		}
+		String hpath = path.substring(0, path.indexOf(connector));
+		if (hpath.indexOf("-") == -1) {
+			throw new StatusException("7002", "文件路径格式错误:" + path);
+		}
+		String yunId = hpath.substring(hpath.indexOf("-") + 1);
+		if (StringUtils.isBlank(yunId)) {
+			throw new StatusException("7003", "文件路径格式错误:" + path);
+		}
+		return yunId;
+	}
+
+	public static String getHead(String path) {
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		if (path.indexOf(connector) == -1) {
+			throw new StatusException("8001", "文件路径格式错误:" + path);
+		}
+		String hpath = path.substring(0, path.indexOf(connector));
+		if (hpath.indexOf("-") == -1) {
+			throw new StatusException("8002", "文件路径格式错误:" + path);
+		}
+		String head = hpath.substring(0, hpath.indexOf("-"));
+		if (StringUtils.isBlank(head)) {
+			throw new StatusException("8003", "文件路径格式错误:" + path);
+		}
+		return head;
+	}
+
+	public static String getPath(String path) {
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		if (path.indexOf(connector) == -1) {
+			throw new StatusException("9001", "文件路径格式错误:" + path);
+		}
+		String rpath = path.substring(path.indexOf(connector) + 3);
+		if (StringUtils.isBlank(rpath)) {
+			throw new StatusException("9002", "文件路径格式错误:" + path);
+		}
+		return rpath;
+	}
+}

+ 160 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStoragePathEnvInfo.java

@@ -0,0 +1,160 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 云存储路径变量
+ *
+ */
+public class FileStoragePathEnvInfo implements JsonSerializable {
+
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -5101258662105160433L;
+
+	/**
+	 * 顶级机构ID
+	 */
+	private String rootOrgId;
+
+	/**
+	 * 顶级机构域名
+	 */
+	private String rootOrgDomain;
+
+	/**
+	 * 用户ID(包含普普通用户ID和studentId)
+	 */
+	private String userId;
+
+	/**
+	 * 时间戳
+	 */
+	private String timeMillis;
+
+	/**
+	 * 文件后缀(以"."开头,如 ".jpg",".zip")
+	 */
+	private String fileSuffix;
+
+	/**
+	 * 相对路径
+	 */
+	private String relativePath;
+
+	/**
+	 * 扩展属性
+	 */
+	private String ext1;
+
+	/**
+	 * 扩展属性
+	 */
+	private String ext2;
+
+	/**
+	 * 扩展属性
+	 */
+	private String ext3;
+
+	/**
+	 * 扩展属性
+	 */
+	private String ext4;
+
+	/**
+	 * 扩展属性
+	 */
+	private String ext5;
+
+	public String getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(String rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public String getRootOrgDomain() {
+		return rootOrgDomain;
+	}
+
+	public void setRootOrgDomain(String rootOrgDomain) {
+		this.rootOrgDomain = rootOrgDomain;
+	}
+
+	public String getUserId() {
+		return userId;
+	}
+
+	public void setUserId(String userId) {
+		this.userId = userId;
+	}
+
+	public String getTimeMillis() {
+		return timeMillis;
+	}
+
+	public void setTimeMillis(String timeMillis) {
+		this.timeMillis = timeMillis;
+	}
+
+	public String getFileSuffix() {
+		return fileSuffix;
+	}
+
+	public void setFileSuffix(String fileSuffix) {
+		this.fileSuffix = fileSuffix;
+	}
+
+	public String getRelativePath() {
+		return relativePath;
+	}
+
+	public void setRelativePath(String relativePath) {
+		this.relativePath = relativePath;
+	}
+
+	public String getExt1() {
+		return ext1;
+	}
+
+	public void setExt1(String ext1) {
+		this.ext1 = ext1;
+	}
+
+	public String getExt2() {
+		return ext2;
+	}
+
+	public void setExt2(String ext2) {
+		this.ext2 = ext2;
+	}
+
+	public String getExt3() {
+		return ext3;
+	}
+
+	public void setExt3(String ext3) {
+		this.ext3 = ext3;
+	}
+
+	public String getExt4() {
+		return ext4;
+	}
+
+	public void setExt4(String ext4) {
+		this.ext4 = ext4;
+	}
+
+	public String getExt5() {
+		return ext5;
+	}
+
+	public void setExt5(String ext5) {
+		this.ext5 = ext5;
+	}
+
+}

+ 66 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageSite.java

@@ -0,0 +1,66 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ */
+public class FileStorageSite implements JsonSerializable {
+
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 4367301242875120606L;
+
+	private String id;
+
+	private String name;
+
+	private String upyunId;
+
+	private String maxSize;
+
+	private String path;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getUpyunId() {
+		return upyunId;
+	}
+
+	public void setUpyunId(String upyunId) {
+		this.upyunId = upyunId;
+	}
+
+	public String getMaxSize() {
+		return maxSize;
+	}
+
+	public void setMaxSize(String maxSize) {
+		this.maxSize = maxSize;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+}

+ 17 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageType.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+public enum FileStorageType {
+
+	/**
+	 * 又拍云存储
+	 */
+	UPYUN,
+
+	/**
+	 * 阿里云存储
+	 */
+	ALIYUN,
+
+	
+
+}

+ 45 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/YunHttpRequest.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import java.util.Map;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class YunHttpRequest implements JsonSerializable {
+
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -6298376638986327265L;
+
+	private String accessUrl;
+
+	private String formUrl;
+
+	private Map<String, String> formParams;
+
+	public String getAccessUrl() {
+		return accessUrl;
+	}
+
+	public void setAccessUrl(String accessUrl) {
+		this.accessUrl = accessUrl;
+	}
+
+	public String getFormUrl() {
+		return formUrl;
+	}
+
+	public void setFormUrl(String formUrl) {
+		this.formUrl = formUrl;
+	}
+
+	public Map<String, String> getFormParams() {
+		return formParams;
+	}
+
+	public void setFormParams(Map<String, String> formParams) {
+		this.formParams = formParams;
+	}
+
+}

+ 47 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/YunPathInfo.java

@@ -0,0 +1,47 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+/**
+ * 路径
+ *
+ */
+public class YunPathInfo {
+
+	/**
+	 * 文件访问地址,http
+	 */
+	private String url;
+
+	/**
+	 * 返回包含协议名的地址,数据库直接存储用
+	 */
+	private String relativePath;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param url
+	 * @param relativePath
+	 */
+	public YunPathInfo(String url, String relativePath) {
+		super();
+		this.url = url;
+		this.relativePath = relativePath;
+	}
+
+	public String getUrl() {
+		return url;
+	}
+
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
+	public String getRelativePath() {
+		return relativePath;
+	}
+
+	public void setRelativePath(String relativePath) {
+		this.relativePath = relativePath;
+	}
+
+}

+ 499 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java

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

+ 127 - 0
src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java

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

+ 72 - 0
src/main/java/cn/com/qmth/examcloud/web/helpers/GlobalHelper.java

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.web.helpers;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.springframework.data.repository.CrudRepository;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+
+/**
+ * 全局 helper
+ *
+ * @author WANGWEI
+ * @date 2019年3月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class GlobalHelper {
+
+	/**
+	 * 顶级机构一致性校验
+	 *
+	 * @author WANGWEI
+	 * @param rootOrgIds
+	 */
+	public static void uniformRootOrg(Long... rootOrgIds) {
+		if (!Util.equals(Arrays.asList(rootOrgIds))) {
+			throw new StatusException("120", "非法请求(顶级机构不一致)");
+		}
+	}
+
+	/**
+	 * 获取存在的实体
+	 *
+	 * @author WANGWEI
+	 * @param repo
+	 * @param id
+	 * @param se
+	 * @return
+	 */
+	public static <ID, T> T getPresentEntity(CrudRepository<T, ID> repo, ID id, Class<T> c) {
+		Optional<T> optional = repo.findById(id);
+		if (!optional.isPresent()) {
+			String simpleName = c.getSimpleName();
+			String desc = String.format("%s (ID: %s) is not present", simpleName,
+					JsonUtil.toJson(id));
+			throw new StatusException("122", desc);
+		} else {
+			return optional.get();
+		}
+	}
+
+	/**
+	 * 获取实体(不存在返回null)
+	 *
+	 * @author WANGWEI
+	 * @param repo
+	 * @param id
+	 * @param c
+	 * @return
+	 */
+	public static <ID, T> T getEntity(CrudRepository<T, ID> repo, ID id, Class<T> c) {
+		Optional<T> optional = repo.findById(id);
+		if (!optional.isPresent()) {
+			return null;
+		} else {
+			return optional.get();
+		}
+	}
+
+}

+ 136 - 0
src/main/java/cn/com/qmth/examcloud/web/helpers/SequenceLockHelper.java

@@ -0,0 +1,136 @@
+package cn.com.qmth.examcloud.web.helpers;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import com.google.common.collect.Lists;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.exception.SequenceLockException;
+import cn.com.qmth.examcloud.web.interceptor.SeqlockInterceptor;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * 并发顺序锁
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public class SequenceLockHelper {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(SequenceLockHelper.class);
+
+	private static final String LOCK_PREFIX = "$_LOCK_C:";
+
+	private static RedisClient redisClient;
+
+	private static RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+
+		return redisClient;
+	}
+
+	/**
+	 * 获取顺序请求锁<br>
+	 * 
+	 * @param args
+	 * @throws SequenceLockException
+	 *             获取锁失败时抛出异常
+	 */
+	public static void getLock(Object... args) throws SequenceLockException {
+
+		String key = LOCK_PREFIX + StringUtils.join(Arrays.asList(args), "_");
+
+		int timeout = 60 * 2;
+
+		if (getRedisClient().setIfAbsent(key, ThreadLocalUtil.getTraceId(), timeout)) {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug("locked");
+			}
+			@SuppressWarnings("unchecked")
+			List<String> keyList = (List<String>) ServletUtil.getRequest()
+					.getAttribute(HttpServletRequestAttribute.$_CUSTOM_SEQUENCE_LOCK.name());
+			if (null == keyList) {
+				keyList = Lists.newArrayList();
+			}
+			keyList.add(key);
+			ServletUtil.getRequest().setAttribute(
+					HttpServletRequestAttribute.$_CUSTOM_SEQUENCE_LOCK.name(), keyList);
+		} else {
+			Long expire = getRedisClient().getExpire(key, TimeUnit.SECONDS);
+			if (null == expire || expire > timeout) {
+				getRedisClient().delete(key);
+			}
+
+			throw new SequenceLockException("请求等待,请稍后重试!");
+		}
+	}
+	
+
+	/**
+	 * 释放锁<br>
+	 * 接口调用不要调用此方法释放锁,此锁会在拦截器<{@link SeqlockInterceptor}中释放.
+	 *
+	 * @author WANGWEI
+	 * @param args
+	 */
+	public static void releaseLock(Object... args) {
+		String key = LOCK_PREFIX + StringUtils.join(Arrays.asList(args), "_");
+		getRedisClient().expire((String) key, 100, TimeUnit.MILLISECONDS);
+		@SuppressWarnings("unchecked")
+		List<String> keyList = (List<String>) ServletUtil.getRequest()
+				.getAttribute(HttpServletRequestAttribute.$_CUSTOM_SEQUENCE_LOCK.name());
+		if (null != keyList) {
+			if (keyList.contains(key)) {
+				keyList.remove(key);
+			}
+		}
+	}
+	
+	   /**
+     * 获取顺序请求锁<br>
+     * 
+     * @param args
+     * @throws SequenceLockException
+     *             获取锁失败时抛出异常
+     */
+    public static void getLockSimple(Object... args) throws SequenceLockException {
+
+        String key = LOCK_PREFIX + StringUtils.join(Arrays.asList(args), "_");
+
+        int timeout = 60 * 2;
+
+        if (!getRedisClient().setIfAbsent(key, ThreadLocalUtil.getTraceId(), timeout)) {
+            Long expire = getRedisClient().getExpire(key, TimeUnit.SECONDS);
+            if (null == expire || expire > timeout) {
+                getRedisClient().delete(key);
+            }
+
+            throw new SequenceLockException("请求等待,请稍后重试!");
+        }
+    }
+    
+    /**
+     * 释放锁<br>
+     * 接口调用不要调用此方法释放锁,此锁会在拦截器<{@link SeqlockInterceptor}中释放.
+     *
+     * @author WANGWEI
+     * @param args
+     */
+    public static void releaseLockSimple(Object... args) {
+        String key = LOCK_PREFIX + StringUtils.join(Arrays.asList(args), "_");
+        getRedisClient().delete(key);
+    }
+
+}

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

@@ -0,0 +1,95 @@
+package cn.com.qmth.examcloud.web.helpers.tree;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.api.commons.exchange.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/web/helpers/tree/TreeNode.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.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);
+
+}

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

@@ -0,0 +1,120 @@
+package cn.com.qmth.examcloud.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.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.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
+	 * @return
+	 */
+	public static EleTreeNode convert2OneEleTreeNode(EleTreeNode root,
+			List<? extends TreeNode> treeNodeList, List<String> disabledCodeList) {
+
+		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/web/helpers/tree/ZtreeNode.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.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;
+	}
+
+}

+ 309 - 0
src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiFlowLimitedInterceptor.java

@@ -0,0 +1,309 @@
+package cn.com.qmth.examcloud.web.interceptor;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.math.RandomUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.RateLimiter;
+import com.googlecode.aviator.AviatorEvaluator;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.actuator.ApiStatusInfo;
+import cn.com.qmth.examcloud.web.actuator.ApiStatusInfoHolder;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * API 流量限制截器
+ *
+ * @author WANGWEI
+ * @date 2019年7月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ApiFlowLimitedInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory
+			.getLog(ApiFlowLimitedInterceptor.class);
+
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private static RateLimiter rateLimiter;
+
+	private static boolean enable = true;
+
+	private static int allowedRate = 0;
+
+	private static int minCallRate = 0;
+
+	private static Properties props = new Properties();
+
+	private static Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();
+
+	private static Map<String, Integer> permitsPerSecondMap = Maps.newConcurrentMap();
+
+	static {
+		init();
+	}
+
+	private static void init() {
+
+		int permitsPerSecond = PropertyHolder.getInt("examcloud.api.permitsPerSecond", 100000);
+		rateLimiter = RateLimiter.create(permitsPerSecond);
+
+		enable = PropertyHolder.getBoolean("examcloud.api.flowLimited.enable", true);
+		allowedRate = PropertyHolder.getInt("examcloud.api.flowLimited.allowedRate", 5);
+		minCallRate = PropertyHolder.getInt("examcloud.api.flowLimited.minCallRate", 10);
+
+		refreshConfig();
+
+		for (Entry<Object, Object> entry : props.entrySet()) {
+			String key = (String) entry.getKey();
+			String value = (String) entry.getValue();
+
+			if (key.endsWith("[S]")) {
+				Integer curPermitsPerSecond = null;
+				try {
+					curPermitsPerSecond = Integer.parseInt(value);
+				} catch (NumberFormatException e) {
+					LOG.error("error value. key= " + key, e);
+					continue;
+				}
+
+				if (curPermitsPerSecond < 0) {
+					LOG.error("value is less than 0. key= " + key);
+					continue;
+				}
+
+				if (curPermitsPerSecond > 10000) {
+					LOG.error("value is more than 10000. key= " + key);
+					continue;
+				}
+
+				RateLimiter curRateLimiter = RateLimiter.create(curPermitsPerSecond);
+				limiterMap.put(key, curRateLimiter);
+				permitsPerSecondMap.put(key, curPermitsPerSecond);
+			}
+		}
+
+		new Thread(new Runnable() {
+			@Override
+			public void run() {
+
+				while (true) {
+					refreshConfig();
+
+					Map<String, Integer> updated = Maps.newHashMap();
+
+					for (Entry<String, Integer> entry : permitsPerSecondMap.entrySet()) {
+						String key = entry.getKey();
+						Integer value = entry.getValue();
+
+						String curValue = (String) props.get(key);
+						if (StringUtils.isBlank(curValue)) {
+							continue;
+						}
+						Integer curPermitsPerSecond = null;
+						try {
+							curPermitsPerSecond = Integer.parseInt(curValue);
+						} catch (NumberFormatException e) {
+							LOG.error("error value. key= " + key, e);
+							continue;
+						}
+
+						if (curPermitsPerSecond < 0) {
+							LOG.error("value is less than 0. key= " + key);
+							continue;
+						}
+
+						if (curPermitsPerSecond > 10000) {
+							LOG.error("value is more than 10000. key= " + key);
+							continue;
+						}
+
+						if (!value.equals(curPermitsPerSecond)) {
+							RateLimiter curRateLimiter = limiterMap.get(key);
+							curRateLimiter.setRate(curPermitsPerSecond);
+							updated.put(key, curPermitsPerSecond);
+						}
+
+					}
+
+					permitsPerSecondMap.putAll(updated);
+
+					Util.sleep(10);
+				}
+			}
+
+		}).start();
+
+	}
+
+	private static void refreshConfig() {
+		try {
+			Properties newProps = new Properties();
+
+			Properties nextProps = new Properties();
+
+			PropertiesUtil.loadFromResource("limited.properties", newProps);
+			for (Entry<Object, Object> entry : newProps.entrySet()) {
+				String key = (String) entry.getKey();
+				String value = (String) entry.getValue();
+				if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
+					continue;
+				}
+
+				nextProps.put(key.trim(), value.trim());
+			}
+
+			props = nextProps;
+
+		} catch (Exception e) {
+			LOG.error("fail to refresh API config.", e);
+		}
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+
+		if (!enable) {
+			return true;
+		}
+
+		boolean acquired = rateLimiter.tryAcquire();
+		if (!acquired) {
+
+			if (INTERFACE_LOG.isErrorEnabled()) {
+				INTERFACE_LOG.error("[Limited]. G.");
+			}
+
+			response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
+			ServletUtil.returnJson(new StatusResponse("503", "limited. G"), response);
+			return false;
+		}
+
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+
+		if (null == apiInfo) {
+			return true;
+		}
+
+		ApiStatusInfo apiStatusInfo = null;
+		try {
+			apiStatusInfo = ApiStatusInfoHolder.getApiStatusInfo(apiInfo.getMapping());
+		} catch (Exception e) {
+			// ignore
+		}
+
+		if (null == apiStatusInfo) {
+			return true;
+		}
+
+		String rateLimiterKey = apiInfo.getMapping() + "[S]";
+		RateLimiter limiter = limiterMap.get(rateLimiterKey);
+
+		if (null != limiter) {
+			acquired = limiter.tryAcquire();
+			if (!acquired) {
+
+				if (INTERFACE_LOG.isErrorEnabled()) {
+					INTERFACE_LOG.error("[Limited]. S. mapping=" + apiInfo.getMapping());
+				}
+
+				response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
+				ServletUtil.returnJson(new StatusResponse("503", "limited. S"), response);
+				return false;
+			}
+		}
+
+		String expression = null;
+		String expressionKey = apiInfo.getMapping() + "[E]";
+		try {
+			expression = (String) props.get(expressionKey);
+		} catch (Exception e) {
+			LOG.error("error value. key= " + expressionKey, e);
+		}
+		if (StringUtils.isBlank(expression)) {
+			return true;
+		}
+		expression = expression.trim();
+
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("expression=" + expression + " ; mapping=" + apiInfo.getMapping());
+		}
+
+		Map<String, Object> env = Maps.newHashMap();
+		env.put("mean", apiStatusInfo.getMean());
+		env.put("max", apiStatusInfo.getMax());
+		env.put("min", apiStatusInfo.getMin());
+		env.put("meanRate", apiStatusInfo.getMeanRate());
+		env.put("oneMinRate", apiStatusInfo.getOneMinuteRate());
+		env.put("errPercent", apiStatusInfo.getErrorPercent());
+		env.put("errMeanRate", apiStatusInfo.getErrorMeanRate());
+		boolean limited = false;
+		try {
+			limited = (Boolean) AviatorEvaluator.execute(expression, env, true);
+		} catch (Exception e) {
+			LOG.error("fail to get value of expression. expression= " + expression, e);
+		}
+		if (limited) {
+
+			int random = RandomUtils.nextInt(100);
+			if (random <= allowedRate) {
+				double oneMinuteRate = apiStatusInfo.getOneMinuteRate();
+
+				int curMinCallRate = minCallRate;
+				String minCallRateKey = apiInfo.getMapping() + "[R]";
+				try {
+					String value = (String) props.get(minCallRateKey);
+					if (StringUtils.isNotBlank(value)) {
+						curMinCallRate = Integer.parseInt(value.trim());
+					}
+				} catch (Exception e) {
+					LOG.error("error value. key= " + minCallRateKey, e);
+				}
+
+				if (LOG.isDebugEnabled()) {
+					LOG.debug(
+							"minCallRate=" + curMinCallRate + "; mapping=" + apiInfo.getMapping());
+				}
+
+				if (oneMinuteRate < curMinCallRate) {
+					return true;
+				}
+			}
+
+			if (INTERFACE_LOG.isErrorEnabled()) {
+				INTERFACE_LOG.error("[Limited]. ER. mapping=" + apiInfo.getMapping());
+			}
+
+			response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
+			ServletUtil.returnJson(new StatusResponse("503", "limited. ER"), response);
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+
+	}
+}

+ 117 - 0
src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiStatisticInterceptor.java

@@ -0,0 +1,117 @@
+package cn.com.qmth.examcloud.web.interceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import com.codahale.metrics.Timer.Context;
+
+import cn.com.qmth.examcloud.web.actuator.MetricNames;
+import cn.com.qmth.examcloud.web.actuator.MetricRegistryHolder;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+
+/**
+ * API统计拦截器<br>
+ * Timer 和 Histogram 只能使用一个<br>
+ * Timer 为最近耗时<br>
+ * Histogram 为全局耗时<br>
+ * 
+ *
+ * @author WANGWEI
+ * @date 2019年7月24日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ApiStatisticInterceptor implements HandlerInterceptor {
+
+	private static String algorithm;
+
+	{
+		// 耗时直方图算法:U:uniform ; B:biased
+		algorithm = PropertyHolder.getString("$API.statistic.histograms.algorithm", "B");
+		if (!(algorithm.equals("B") || algorithm.equals("U"))) {
+			throw new RuntimeException("histograms algorithm must be 'U' or 'B'");
+		}
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+
+		if (null == apiInfo) {
+			return true;
+		}
+
+		// Timer 统计
+		if (algorithm.equals("B")) {
+			Timer timer = MetricRegistryHolder.getDefalut()
+					.timer(MetricRegistry.name(MetricNames.API_TIMER.name(), apiInfo.getMapping()));
+			Context ctx = timer.time();
+			request.setAttribute(HttpServletRequestAttribute.$_METRICS_TIMER_CTX.name(), ctx);
+		}
+
+		if (algorithm.equals("U")) {
+			request.setAttribute(
+					HttpServletRequestAttribute.API_STATISTIC_INTERCEPTOR_START_TIME.name(),
+					System.currentTimeMillis());
+		}
+
+		return true;
+	}
+
+	@Override
+	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+			Object handler, Exception ex) throws Exception {
+
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+
+		if (null == apiInfo) {
+			return;
+		}
+
+		// Timer 统计
+		if (algorithm.equals("B")) {
+			Context ctx = (Context) request
+					.getAttribute(HttpServletRequestAttribute.$_METRICS_TIMER_CTX.name());
+			if (null != ctx) {
+				ctx.stop();
+			}
+		}
+
+		// Histogram 统计
+		if (algorithm.equals("U")) {
+			Histogram histogram = MetricRegistryHolder.getDefalut().histogram(
+					MetricRegistry.name(MetricNames.API_HISTOGRAM.name(), apiInfo.getMapping()));
+			Long startTime = (Long) request.getAttribute(
+					HttpServletRequestAttribute.API_STATISTIC_INTERCEPTOR_START_TIME.name());
+			histogram.update(System.currentTimeMillis() - startTime);
+		}
+
+		// 失败速率
+		Boolean hasException = (Boolean) request
+				.getAttribute(HttpServletRequestAttribute.$_EXCEPTION_HAPPENED.name());
+
+		if (null != hasException && hasException) {
+			Meter apiErrorMeter = MetricRegistryHolder.getDefalut().meter(
+					MetricRegistry.name(MetricNames.API_ERROR_METER.name(), apiInfo.getMapping()));
+			apiErrorMeter.mark();
+		}
+
+		// 请求速率
+		Meter apiMeter = MetricRegistryHolder.getDefalut()
+				.meter(MetricRegistry.name(MetricNames.API_METER.name(), apiInfo.getMapping()));
+		apiMeter.mark();
+
+	}
+
+}

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

@@ -0,0 +1,136 @@
+package cn.com.qmth.examcloud.web.interceptor;
+
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.http.HttpStatus;
+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 cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.support.ApiId;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ApiInfoHolder;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * 首发拦截器
+ *
+ * @author WANGWEI
+ * @date 2018年10月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public class FirstInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(FirstInterceptor.class);
+
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		ThreadLocalUtil.clearAll();
+		String traceId = request.getHeader("Trace-Id");
+		if (StringUtils.isBlank(traceId)) {
+			traceId = ThreadLocalUtil.next();
+		} else {
+			ThreadLocalUtil.setTraceId(traceId);
+		}
+
+		// 设置log4j线程上下文
+		ThreadContext.put("TRACE_ID", traceId);
+		ThreadContext.put("CALLER", "$$$$$");
+
+		String path = request.getServletPath();
+		String method = request.getMethod();
+
+		Set<String> headerNames = new HashSet<String>(20);
+		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);
+				headers.put(name, value);
+				headerNames.add(name);
+			}
+			INTERFACE_LOG.debug(StringUtil.join("[preHandle]. path=\"", path, "\"; method=[",
+					method, "]", "; headers=", JsonUtil.toJson(headers)));
+		}
+
+		if (path.equals("/error")) {
+			response.setStatus(HttpStatus.NOT_FOUND.value());
+			ServletUtil.returnJson(new StatusResponse(String.valueOf(HttpStatus.NOT_FOUND.value()),
+					String.valueOf(HttpStatus.NOT_FOUND.getReasonPhrase())), response);
+			return false;
+		}
+
+		if ((!org.springframework.http.HttpMethod.OPTIONS.matches(method))
+				&& handler instanceof HandlerMethod) {
+			HandlerMethod handlerMethod = (HandlerMethod) handler;
+			ApiId apiId = handlerMethod.getMethodAnnotation(ApiId.class);
+
+			ApiInfo apiInfo = null;
+			if (null != apiId) {
+				apiInfo = ApiInfoHolder.getApiInfo(apiId.value());
+			} else {
+				apiInfo = ApiInfoHolder.getApiInfo(handlerMethod.getMethod());
+			}
+			String mapping = apiInfo.getMapping();
+			request.setAttribute(HttpServletRequestAttribute.$_API_INFO.name(), apiInfo);
+
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("[preHandle]. mapping = " + mapping);
+			}
+			request.setAttribute(HttpServletRequestAttribute.$_MAPPING.name(), mapping);
+
+		} else {
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("not handle by HandlerMethod.");
+			}
+
+			String mapping = StringUtils.join("_[", path, "][", method, "]");
+
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("[preHandle]. mapping = " + mapping);
+			}
+
+			request.setAttribute(HttpServletRequestAttribute.$_MAPPING.name(), mapping);
+			return true;
+		}
+
+		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... ...");
+		// 清理log4j线程上下文
+		ThreadContext.clearAll();
+	}
+
+}

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

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.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;
+
+/**
+ * 全局 请求顺序锁<br>
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface GlobalSequenceLock {
+
+}

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

@@ -0,0 +1,139 @@
+package cn.com.qmth.examcloud.web.interceptor;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.collections.CollectionUtils;
+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.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * 顺序锁拦截器
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] 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 GLOBAL_LOCK_PREFIX = "$_LOCK_G:";
+
+	private static final String SESSION_LOCK_PREFIX = "$_LOCK_S:";
+
+	private static final String LOCK_ATTRIBUTE = "$sequenceLock";
+
+	/**
+	 * 构造函数
+	 *
+	 * @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... ...");
+
+		if (handler instanceof HandlerMethod) {
+
+			HandlerMethod handlerMethod = (HandlerMethod) handler;
+			GlobalSequenceLock globalSeqlock = handlerMethod
+					.getMethodAnnotation(GlobalSequenceLock.class);
+			SessionSequenceLock sessionSeqlock = handlerMethod
+					.getMethodAnnotation(SessionSequenceLock.class);
+
+			String mapping = (String) request
+					.getAttribute(HttpServletRequestAttribute.$_MAPPING.name());
+
+			if (null != globalSeqlock) {
+				String key = GLOBAL_LOCK_PREFIX + mapping;
+
+				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceId(), 60 * 5)) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug("partition locked");
+					}
+
+					request.setAttribute(LOCK_ATTRIBUTE, key);
+					return true;
+				} else {
+					response.setStatus(HttpStatus.CONFLICT.value());
+					ServletUtil.returnJson(new StatusResponse("409", "请求等待,请稍后重试!"), response);
+					return false;
+				}
+
+			} else if (null != sessionSeqlock) {
+
+				User user = (User) request
+						.getAttribute(HttpServletRequestAttribute.$_ACCESS_USER.name());
+
+				String key = SESSION_LOCK_PREFIX + user.getKey();
+
+				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceId(), 60 * 5)) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug("sesssion locked");
+					}
+					request.setAttribute(LOCK_ATTRIBUTE, key);
+					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... ...");
+
+		Object key = request.getAttribute(LOCK_ATTRIBUTE);
+		if (null != key) {
+			redisClient.delete((String) key);
+		}
+
+		@SuppressWarnings("unchecked")
+		List<String> keyList = (List<String>) ServletUtil.getRequest()
+				.getAttribute(HttpServletRequestAttribute.$_CUSTOM_SEQUENCE_LOCK.name());
+		if (CollectionUtils.isNotEmpty(keyList)) {
+			for (String cur : keyList) {
+				redisClient.delete(cur);
+			}
+		}
+
+	}
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSequenceLock.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.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;
+
+/**
+ * session级 请求顺序锁<br>
+ * 用一鉴权用户(必须是用户接入)顺序调用
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SessionSequenceLock {
+
+}

+ 70 - 0
src/main/java/cn/com/qmth/examcloud/web/jpa/DataIntegrityViolationTransverter.java

@@ -0,0 +1,70 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.dao.DataIntegrityViolationException;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+
+/**
+ * 数据完整性校验转换器
+ *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public class DataIntegrityViolationTransverter {
+
+	private static Map<String, UniqueRule> uniqueRuleMap = Maps.newHashMap();
+
+	public static void setUniqueRules(List<UniqueRule> rules) {
+		for (UniqueRule rule : rules) {
+			uniqueRuleMap.put(rule.getIndexName(), rule);
+		}
+	}
+
+	/**
+	 * 唯一性约束转换
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 */
+	public static void throwIfDuplicateEntry(DataIntegrityViolationException e) {
+
+		Throwable cause = e.getCause();
+		if (null != cause) {
+			Throwable cause2 = cause.getCause();
+			if (cause instanceof org.hibernate.exception.ConstraintViolationException) {
+				if (null != cause2) {
+					if (cause2 instanceof java.sql.SQLIntegrityConstraintViolationException) {
+						throwIfDuplicateEntry(
+								(java.sql.SQLIntegrityConstraintViolationException) cause2);
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * 唯一性约束转换
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 */
+	public static void throwIfDuplicateEntry(SQLIntegrityConstraintViolationException e) {
+		String message = e.getMessage();
+		// Duplicate entry '*' for key '*'
+		if (message.startsWith("Duplicate entry") && message.contains("IDX_")) {
+			String index = message.substring(message.indexOf("IDX_"), message.length() - 1);
+			UniqueRule rule = uniqueRuleMap.get(index);
+			if (null != rule) {
+				throw new StatusException(rule.getCode(), rule.getDesc());
+			}
+		}
+	}
+
+}

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

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+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.api.commons.exchange.JsonSerializable;
+
+/**
+ * JPA 数据库实体父类
+ *
+ * @author WANGWEI
+ * @date 2018年10月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class JpaEntity implements JsonSerializable {
+
+	private static final long serialVersionUID = 1561273677157469875L;
+
+	/**
+	 * 更新时间
+	 */
+	@LastModifiedDate
+	@Column(nullable = false)
+	@Temporal(TemporalType.TIMESTAMP)
+	private Date updateTime;
+
+	/**
+	 * 创建时间
+	 */
+	@CreatedDate
+	@Column(nullable = false, updatable = false)
+	@Temporal(TemporalType.TIMESTAMP)
+	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;
+	}
+
+}

+ 49 - 0
src/main/java/cn/com/qmth/examcloud/web/jpa/UniqueRule.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+public final class UniqueRule {
+
+	private String indexName;
+
+	private String code;
+
+	private String desc;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param indexName
+	 * @param code
+	 * @param desc
+	 */
+	public UniqueRule(String indexName, String code, String desc) {
+		super();
+		this.indexName = indexName;
+		this.code = code;
+		this.desc = desc;
+	}
+
+	public String getIndexName() {
+		return indexName;
+	}
+
+	public void setIndexName(String indexName) {
+		this.indexName = indexName;
+	}
+
+	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;
+	}
+
+}

+ 32 - 0
src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdAndStatusJpaEntity.java

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.MappedSuperclass;
+
+/**
+ * ID and status
+ *
+ * @author WANGWEI
+ * @date 2019年2月20日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@MappedSuperclass
+public abstract class WithIdAndStatusJpaEntity extends WithStatusJpaEntity {
+
+	private static final long serialVersionUID = 2909770208688213445L;
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	private Long id;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+}

+ 32 - 0
src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdJpaEntity.java

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.MappedSuperclass;
+
+/**
+ * ID
+ *
+ * @author WANGWEI
+ * @date 2019年2月20日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@MappedSuperclass
+public abstract class WithIdJpaEntity extends JpaEntity {
+
+	private static final long serialVersionUID = 2909770208688213445L;
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	private Long id;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+}

+ 46 - 0
src/main/java/cn/com/qmth/examcloud/web/jpa/WithStatusJpaEntity.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import javax.persistence.Column;
+import javax.persistence.MappedSuperclass;
+
+/**
+ * status
+ *
+ * @author WANGWEI
+ * @date 2019年2月20日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@MappedSuperclass
+public abstract class WithStatusJpaEntity extends JpaEntity {
+
+	private static final long serialVersionUID = -5204331536862187353L;
+
+	/**
+	 * 是否可用
+	 */
+	@Column(nullable = false)
+	private Boolean enabled;
+
+	/**
+	 * 是否被销毁
+	 */
+	@Column(nullable = false)
+	private Boolean destroyed;
+
+	public Boolean getEnabled() {
+		return enabled;
+	}
+
+	public void setEnabled(Boolean enabled) {
+		this.enabled = enabled;
+	}
+
+	public Boolean getDestroyed() {
+		return destroyed;
+	}
+
+	public void setDestroyed(Boolean destroyed) {
+		this.destroyed = destroyed;
+	}
+
+}

+ 84 - 0
src/main/java/cn/com/qmth/examcloud/web/mongodb/MongodbDetector.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.web.mongodb;
+
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Map;
+
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * mongodb 侦察器
+ *
+ * @author WANGWEI
+ * @date 2018年11月29日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@Order(99)
+public class MongodbDetector implements ApplicationRunner {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(MongodbDetector.class);
+
+	public void start() {
+		String appName = PropertyHolder.getString("spring.application.name");
+
+		String ip = null;
+		try {
+			InetAddress localHost = Inet4Address.getLocalHost();
+			ip = localHost.getHostAddress();
+		} catch (Exception e) {
+			// ingnore
+		}
+
+		Class<?> mongoTemplateClass = null;
+		Object mongoTemplateObject = null;
+
+		try {
+			mongoTemplateClass = Class
+					.forName("org.springframework.data.mongodb.core.MongoTemplate");
+			mongoTemplateObject = SpringContextHolder.getBean(mongoTemplateClass);
+		} catch (Exception e) {
+			// ingnore
+			LOG.info("mongodb useless");
+			return;
+		}
+
+		try {
+			Method getDbMethod = mongoTemplateClass.getMethod("getDb");
+			Object mongoDatabaseObject = getDbMethod.invoke(mongoTemplateObject);
+			Method getNameMethod = mongoDatabaseObject.getClass().getMethod("getName");
+			Object dbName = getNameMethod.invoke(mongoDatabaseObject);
+			LOG.info("mongo.db=" + dbName);
+
+			Map<String, String> data = Maps.newHashMap();
+			data.put("startupTime", DateUtil.chinaNow());
+			data.put("appName", appName);
+			data.put("ip", ip);
+
+			Method insertMethod = mongoTemplateClass.getMethod("insert", Object.class,
+					String.class);
+			insertMethod.invoke(mongoTemplateObject, data, "startup");
+		} catch (Exception e) {
+			LOG.error("unexpected mongodb exception");
+		}
+
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+		start();
+	}
+
+}

+ 83 - 0
src/main/java/cn/com/qmth/examcloud/web/redis/CustomRedisConfiguration.java

@@ -0,0 +1,83 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+
+@Configuration
+public class CustomRedisConfiguration {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(CustomRedisConfiguration.class);
+
+	@Bean
+	public RedisTemplate<String, Object> redisTemplate(
+			@Autowired(required = false) RedisConnectionFactory redisConnectionFactory) {
+
+		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+
+		if (null == redisConnectionFactory) {
+			LOG.info("no instance of RedisConnectionFactory.");
+			redisTemplate.setConnectionFactory(new MarkedJedisConnectionFactory());
+			return redisTemplate;
+		}
+
+		LOG.info("start to create a custom instance of RedisTemplate... ...");
+
+		redisTemplate.setConnectionFactory(redisConnectionFactory);
+		Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
+				Object.class);
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+		objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+		jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+		redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+		redisTemplate.setKeySerializer(new StringRedisSerializer());
+		redisTemplate.afterPropertiesSet();
+
+		LOG.info("custom instance of RedisTemplate is created.");
+
+		return redisTemplate;
+	}
+
+	@Bean
+	public RedisClient redisClient(
+			@Autowired(required = true) RedisTemplate<String, Object> redisTemplate) {
+
+		LOG.info("start to create a custom instance of RedisClient... ...");
+
+		RedisClient redisClient = new SimpleRedisClient(redisTemplate);
+		RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
+		if (connectionFactory instanceof MarkedJedisConnectionFactory) {
+			redisClient.setEnable(false);
+			LOG.info("disabled instance of RedisClient is created. please be careful !");
+		} else {
+			redisClient.set("test", "test");
+			LOG.info("enabled instance of RedisClient is created.");
+		}
+
+		return redisClient;
+	}
+
+	/**
+	 * 标识工厂
+	 *
+	 * @author WANGWEI
+	 * @date 2019年3月22日
+	 * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+	 */
+	private class MarkedJedisConnectionFactory extends JedisConnectionFactory {
+	}
+
+}

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

@@ -0,0 +1,153 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * redis client
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface RedisClient {
+
+	/**
+	 * 是否可用
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public boolean isEnable();
+
+	/**
+	 * 设置是否可用
+	 *
+	 * @author WANGWEI
+	 * @param enable
+	 */
+	public void setEnable(boolean enable);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 */
+	public void set(String key, Object value);
+	
+	/**
+	 * @param key
+	 * @param subkey
+	 * @param value
+	 */
+	public void set(String key,String subkey, Object value);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 * @param timeout
+	 */
+	public void set(String key, Object value, int timeout);
+	
+    /**
+     * @param key
+     * @param subkey
+     * @param value
+     * @param timeout
+     */
+    public void set(String key,String subkey, Object value, int timeout);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param timeout
+	 */
+	public void expire(String key, int timeout);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param timeout
+	 * @param unit
+	 */
+	public void expire(String key, final long timeout, final TimeUnit unit);
+
+	/**
+	 * 方法注释
+	 *
+	 * @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);
+	
+	/** hash get
+	 * @param <T>
+	 * @param key
+	 * @param subkey
+	 * @param c
+	 * @return
+	 */
+	public <T> T get(String key, String subkey, Class<T> c);
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 */
+	public void delete(String key);
+	
+	public void delete(String key,String subkey);
+
+	/**
+	 * 方法注释
+	 *
+	 * @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);
+
+	/**
+	 * 获取失效时间
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param timeUnit
+	 * @return
+	 */
+	public Long getExpire(String key, TimeUnit timeUnit);
+
+}

+ 185 - 0
src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java

@@ -0,0 +1,185 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.RedisTemplate;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+
+/**
+ * redis client
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public final class SimpleRedisClient implements RedisClient {
+
+	private static final ExamCloudLog REDIS_LOG = ExamCloudLogFactory.getLog("REDIS_LOGGER");
+
+	private RedisTemplate<String, Object> redisTemplate;
+
+	private boolean enable = true;
+
+	public SimpleRedisClient(RedisTemplate<String, Object> redisTemplate) {
+		super();
+		this.redisTemplate = redisTemplate;
+	}
+
+	private void beforeMethod() {
+		if (!enable) {
+			throw new ExamCloudRuntimeException("RedisClient is not enabled");
+		}
+	}
+
+	private void afterMethod(String method, long startTimeMillis) {
+		if (REDIS_LOG.isDebugEnabled()) {
+			String s = String.format("[SimpleRedisClient.%s] cost %d ms.", method,
+					System.currentTimeMillis() - startTimeMillis);
+			REDIS_LOG.debug(s);
+		}
+	}
+
+	@Override
+	public void set(String key, Object value) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.opsForValue().set(key, value);
+		afterMethod("set(String key, Object value)", s);
+	}
+
+	@Override
+	public void set(String key, Object value, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.opsForValue().set(key, value,timeout,TimeUnit.SECONDS);
+		afterMethod("set(String key, Object value, int timeout)", s);
+	}
+
+	@Override
+	public void expire(String key, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
+		afterMethod("expire(String key, int timeout)", s);
+	}
+
+	/**
+	 * 方法注释
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param timeout
+	 * @param unit
+	 */
+	public void expire(String key, final long timeout, final TimeUnit unit) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.expire(key, timeout, unit);
+		afterMethod("(String key, final long timeout, final TimeUnit unit)", s);
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		expire(key, timeout);
+		afterMethod("get(String key, Class<T> c, int timeout)", s);
+		return t;
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		afterMethod("get(String key, Class<T> c)", s);
+		return t;
+	}
+
+
+    @Override
+    public <T> T get(String key,String hashKey, Class<T> c) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        Object object = redisTemplate.opsForHash().get(key,hashKey);
+        @SuppressWarnings("unchecked")
+        T t = (T) object;
+        afterMethod("get(String key, Class<T> c)", s);
+        return t;
+    }
+	@Override
+	public void delete(String key) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.opsForValue().set(key, null);
+		expire(key, 0);
+		afterMethod("delete(String key)", s);
+	}
+
+	@Override
+	public void convertAndSend(String channel, Object message) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.convertAndSend(channel, message);
+		afterMethod("convertAndSend(String channel, Object message)", s);
+	}
+
+	@Override
+	public Boolean setIfAbsent(String key, String value, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
+		afterMethod("setIfAbsent(String key, String value, int timeout)", s);
+		return b;
+	}
+
+	@Override
+	public boolean isEnable() {
+		return enable;
+	}
+
+	@Override
+	public void setEnable(boolean enable) {
+		this.enable = enable;
+	}
+
+	@Override
+	public Long getExpire(String key, TimeUnit timeUnit) {
+		return redisTemplate.getExpire(key, timeUnit);
+	}
+
+    @Override
+    public void set(String key, String subkey, Object value, int timeout) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+		expire(key, timeout);
+		set(key,subkey, value);
+        afterMethod("set(String key,String subkey,Object value, int timeout)", s);
+
+    }
+
+    @Override
+    public void delete(String key, String subkey) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        redisTemplate.opsForHash().delete(subkey, subkey);
+        afterMethod("delete(String key,String subkey)", s);
+    }
+
+    @Override
+    public void set(String key, String subkey, Object value) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        redisTemplate.opsForHash().put(key,subkey, value);
+        afterMethod("set(String key,String subkey, Object value)", s);
+    }
+
+}

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

@@ -0,0 +1,154 @@
+package cn.com.qmth.examcloud.web.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import cn.com.qmth.examcloud.api.commons.CloudService;
+import cn.com.qmth.examcloud.api.commons.EnterpriseService;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * 请求鉴权
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RequestPermissionInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory
+			.getLog(RequestPermissionInterceptor.class);
+
+	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	private ResourceManager resourceManager;
+
+	private RedisClient redisClient;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param resourceManager
+	 * @param redisClient
+	 */
+	public RequestPermissionInterceptor(ResourceManager resourceManager, RedisClient redisClient) {
+		super();
+		this.resourceManager = resourceManager;
+		this.redisClient = redisClient;
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		LOG.debug("preHandle... ...");
+
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+
+		if (null != apiInfo) {
+			Class<?> ctrClass = apiInfo.getBeanType();
+			if (CloudService.class.isAssignableFrom(ctrClass)
+					|| EnterpriseService.class.isAssignableFrom(ctrClass)) {
+				return true;
+			}
+		}
+
+		String mapping = (String) request
+				.getAttribute(HttpServletRequestAttribute.$_MAPPING.name());
+
+		boolean naked = resourceManager.isNaked(apiInfo, mapping);
+		if (naked) {
+			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) || StringUtils.isBlank(token)) {
+			response.setStatus(HttpStatus.FORBIDDEN.value());
+			ServletUtil.returnJson(new StatusResponse("403", "unallowed"), response);
+			return false;
+		}
+
+		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;
+		}
+
+		User user = redisClient.get(key, User.class);
+
+		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;
+		}
+
+		redisClient.expire(key, user.getSessionTimeout());
+
+		ThreadContext.put("CALLER", key);
+
+		if (!resourceManager.hasPermission(user, apiInfo, mapping)) {
+			response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value());
+			ServletUtil.returnJson(new StatusResponse("405", "no permission."), response);
+			return false;
+		}
+
+		request.setAttribute(HttpServletRequestAttribute.$_ACCESS_USER.name(), 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... ...");
+	}
+
+}

+ 46 - 0
src/main/java/cn/com/qmth/examcloud/web/security/ResourceManager.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.web.security;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+
+/**
+ * 资源管理器
+ *
+ * @author WANGWEI
+ * @date 2019年2月15日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface ResourceManager {
+
+	/**
+	 * 获取APP 密钥
+	 *
+	 * @author WANGWEI
+	 * @param appId
+	 * @return
+	 */
+	AccessApp getAccessApp(Long appId);
+
+	/**
+	 * 请求的接口是否裸奔
+	 *
+	 * @author WANGWEI
+	 * @param apiInfo
+	 * @param mapping
+	 * @return
+	 */
+	boolean isNaked(ApiInfo apiInfo, String mapping);
+
+	/**
+	 * 分区用户是否有权限访问接口
+	 *
+	 * @author WANGWEI
+	 * @param user
+	 * @param apiInfo
+	 * @param mapping
+	 * @return
+	 */
+	boolean hasPermission(User user, ApiInfo apiInfo, String mapping);
+
+}

+ 189 - 0
src/main/java/cn/com/qmth/examcloud/web/security/RpcInterceptor.java

@@ -0,0 +1,189 @@
+package cn.com.qmth.examcloud.web.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import cn.com.qmth.examcloud.api.commons.CloudService;
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.ByteUtil;
+import cn.com.qmth.examcloud.commons.util.SHA256;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ServletUtil;
+import cn.com.qmth.examcloud.web.support.StatusResponse;
+
+/**
+ * spring cloud 请求接入
+ *
+ * @author WANGWEI
+ * @date 2018年5月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public final class RpcInterceptor implements HandlerInterceptor {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(RpcInterceptor.class);
+
+	private ResourceManager resourceManager;
+
+	/**
+	 * 构造函数
+	 *
+	 * @param resourceManager
+	 */
+	public RpcInterceptor(ResourceManager resourceManager) {
+		super();
+		this.resourceManager = resourceManager;
+	}
+
+	@Override
+	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+			Object handler) throws Exception {
+		LOG.debug("preHandle... ...");
+
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+		if (null == apiInfo) {
+			return true;
+		}
+		Class<?> ctrClass = apiInfo.getBeanType();
+		if (!CloudService.class.isAssignableFrom(ctrClass)) {
+			return true;
+		}
+
+		String appId = request.getHeader("App-Id");
+		String appCode = request.getHeader("App-Code");
+		String timestamp = request.getHeader("timestamp");
+		String accessToken = request.getHeader("Access-Token");
+		if (StringUtils.isBlank(appId)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' is blank"), response);
+			return false;
+		}
+		Long appIdLong = null;
+		try {
+			appIdLong = Long.parseLong(appId);
+		} catch (Exception e) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' must be a long"), response);
+			return false;
+		}
+		if (StringUtils.isBlank(appCode)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Code' is blank"), response);
+			return false;
+		}
+		if (StringUtils.isBlank(timestamp)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Code' is blank"), response);
+			return false;
+		}
+		Long timestampLong = null;
+		try {
+			timestampLong = Long.parseLong(timestamp);
+		} catch (Exception e) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'timestamp' must be a long"), response);
+			return false;
+		}
+
+		if (StringUtils.isBlank(accessToken)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'Access-Token' is blank"), response);
+			return false;
+		}
+
+		AccessApp accessApp = null;
+		try {
+			accessApp = resourceManager.getAccessApp(appIdLong);
+		} catch (Exception e) {
+			LOG.error("fail to get App info. appId=" + appIdLong, e);
+		}
+		if (null == accessApp) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' is wrong"), response);
+			return false;
+		}
+		if (!appCode.equals(accessApp.getAppCode())) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' & 'App-Code' are wrong"), response);
+			return false;
+		}
+
+		if (null != accessApp.getTimeRange()) {
+			long currentTimeMillis = System.currentTimeMillis();
+			if (Math.abs(currentTimeMillis - timestampLong) > accessApp.getTimeRange()) {
+				response.setStatus(HttpStatus.UNAUTHORIZED.value());
+				ServletUtil.returnJson(
+						new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+								"'timestamp' is out"),
+						response);
+				return false;
+			}
+		}
+
+		String secretKey = accessApp.getSecretKey();
+		String joinStr = StringUtil.join(appId, appCode, timestamp, secretKey);
+		byte[] bytes = SHA256.encode(joinStr);
+		String hexAscii = ByteUtil.toHexAscii(bytes);
+
+		if (!hexAscii.equalsIgnoreCase(accessToken)) {
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil.returnJson(new StatusResponse(
+					String.valueOf(HttpStatus.UNAUTHORIZED.value()), "access failure"), response);
+			return false;
+		}
+
+		ThreadContext.put("CALLER", "APP:" + accessApp.getAppCode());
+		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 setResourceManager(ResourceManager resourceManager) {
+		this.resourceManager = resourceManager;
+	}
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ApiId.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.web.support;
+
+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;
+
+/**
+ * 接口ID 注解
+ *
+ * @author WANGWEI
+ * @date 2019年3月15日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ApiId {
+
+	int value();
+}

+ 121 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ApiInfo.java

@@ -0,0 +1,121 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.io.Serializable;
+
+/**
+ * 接口
+ *
+ * @author WANGWEI
+ * @date 2018年5月14日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+
+public class ApiInfo implements Serializable {
+
+	private static final long serialVersionUID = 1553810211239843543L;
+
+	/**
+	 * ID
+	 */
+	private Integer id;
+
+	/**
+	 * 映射
+	 */
+	private String mapping;
+
+	/**
+	 * mapping路径
+	 */
+	private String requestPath;
+
+	/**
+	 * http方法
+	 */
+	private String httpMethod;
+
+	/**
+	 * 接口描述
+	 */
+	private String description;
+
+	/**
+	 * 请求处理类
+	 */
+	private transient Class<?> beanType;
+
+	/**
+	 * 接口日志忽略堆栈
+	 */
+	private boolean withoutStackTrace;
+
+	/**
+	 * 接口裸奔
+	 */
+	private boolean naked;
+
+	public Integer getId() {
+		return id;
+	}
+
+	public void setId(Integer id) {
+		this.id = id;
+	}
+
+	public String getMapping() {
+		return mapping;
+	}
+
+	public void setMapping(String mapping) {
+		this.mapping = mapping;
+	}
+
+	public String getRequestPath() {
+		return requestPath;
+	}
+
+	public void setRequestPath(String requestPath) {
+		this.requestPath = requestPath;
+	}
+
+	public String getHttpMethod() {
+		return httpMethod;
+	}
+
+	public void setHttpMethod(String httpMethod) {
+		this.httpMethod = httpMethod;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public Class<?> getBeanType() {
+		return beanType;
+	}
+
+	public void setBeanType(Class<?> beanType) {
+		this.beanType = beanType;
+	}
+
+	public boolean isWithoutStackTrace() {
+		return withoutStackTrace;
+	}
+
+	public void setWithoutStackTrace(boolean withoutStackTrace) {
+		this.withoutStackTrace = withoutStackTrace;
+	}
+
+	public boolean isNaked() {
+		return naked;
+	}
+
+	public void setNaked(boolean naked) {
+		this.naked = naked;
+	}
+
+}

+ 177 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ApiInfoHolder.java

@@ -0,0 +1,177 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
+import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import io.swagger.annotations.ApiOperation;
+
+@Component
+@Order(100)
+public class ApiInfoHolder implements ApplicationRunner {
+
+	private static final Map<String, ApiInfo> INDEX_BY_MAPPING = Maps.newConcurrentMap();
+
+	private static final Map<Integer, ApiInfo> INDEX_BY_API_ID = Maps.newConcurrentMap();
+
+	private static final Map<Method, ApiInfo> INDEX_BY_METHOD = Maps.newConcurrentMap();
+
+	private static Set<ApiInfo> apiInfoSet = Sets.newHashSet();
+
+	@Autowired
+	private RequestMappingHandlerMapping requestMappingHandlerMapping;
+
+	/**
+	 * 通过apiId获取ApiInfo
+	 *
+	 * @author WANGWEI
+	 * @param interfaceId
+	 * @return
+	 */
+	public static ApiInfo getApiInfo(Integer interfaceId) {
+		return INDEX_BY_API_ID.get(interfaceId);
+	}
+
+	/**
+	 * 通过方法获取ApiInfo
+	 *
+	 * @author WANGWEI
+	 * @param method
+	 * @return
+	 */
+	public static ApiInfo getApiInfo(Method method) {
+		return INDEX_BY_METHOD.get(method);
+	}
+
+	/**
+	 * 通过mapping获取ApiInfo
+	 *
+	 * @author WANGWEI
+	 * @param mapping
+	 * @return
+	 */
+	public static ApiInfo getApiInfo(String mapping) {
+		return INDEX_BY_MAPPING.get(mapping);
+	}
+
+	/**
+	 * 获取@ApiId注解的ApiInfo集合
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static Set<ApiInfo> getApiInfoSet() {
+		return apiInfoSet;
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+
+		Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping
+				.getHandlerMethods();
+
+		Set<Integer> apiIdSet = Sets.newHashSet();
+
+		for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
+			RequestMappingInfo requestMappingInfo = entry.getKey();
+			HandlerMethod handlerMethod = entry.getValue();
+
+			Class<?> beanType = handlerMethod.getBeanType();
+
+			RequestMapping requestMappingAnnotationOfClass = AnnotationUtils
+					.findAnnotation(beanType, RequestMapping.class);
+
+			RequestMapping requestMappingAnnotationOfMethod = handlerMethod
+					.getMethodAnnotation(RequestMapping.class);
+
+			ApiId apiId = handlerMethod.getMethodAnnotation(ApiId.class);
+			WithoutStackTrace withoutStackTrace = handlerMethod
+					.getMethodAnnotation(WithoutStackTrace.class);
+			Naked naked = handlerMethod.getMethodAnnotation(Naked.class);
+
+			ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
+
+			RequestMethodsRequestCondition requestMethodsRequestCondition = requestMappingInfo
+					.getMethodsCondition();
+			Set<RequestMethod> requestMethodSet = requestMethodsRequestCondition.getMethods();
+
+			String[] mappingURIsOfClass = null;
+			String[] mappingURIsOfMethod = null;
+
+			if (null != requestMappingAnnotationOfClass) {
+				mappingURIsOfClass = requestMappingAnnotationOfClass.path();
+			}
+			if (null != requestMappingAnnotationOfMethod) {
+				mappingURIsOfMethod = requestMappingAnnotationOfMethod.path();
+			}
+
+			String mappingIdentifyOfClass = null;
+			String mappingIdentifyOfMethod = null;
+
+			if (null != mappingURIsOfClass) {
+				mappingIdentifyOfClass = StringUtils.join(mappingURIsOfClass, ",");
+			}
+			if (null != mappingURIsOfMethod) {
+				mappingIdentifyOfMethod = StringUtils.join(mappingURIsOfMethod, ",");
+			}
+
+			String methods = StringUtils.join(requestMethodSet, ",");
+
+			String mapping = StringUtils.join("[", mappingIdentifyOfClass, "][",
+					mappingIdentifyOfMethod, "][", methods, "]");
+
+			PatternsRequestCondition patternsRequestCondition = requestMappingInfo
+					.getPatternsCondition();
+
+			String requestPath = StringUtils.join(patternsRequestCondition.getPatterns(), ",");
+
+			ApiInfo apiInfo = new ApiInfo();
+			if (null != apiOperation) {
+				apiInfo.setDescription(apiOperation.value());
+			}
+			apiInfo.setHttpMethod(methods);
+			apiInfo.setMapping(mapping);
+			apiInfo.setRequestPath(requestPath);
+			apiInfo.setBeanType(beanType);
+			if (null != withoutStackTrace) {
+				apiInfo.setWithoutStackTrace(withoutStackTrace.value());
+			}
+			if (null != naked) {
+				apiInfo.setNaked(naked.value());
+			}
+
+			if (null != apiId) {
+				if (apiIdSet.contains(apiId.value())) {
+					throw new ExamCloudRuntimeException("duplicate apiId. apiId=" + apiId.value());
+				}
+				apiIdSet.add(apiId.value());
+				apiInfo.setId(apiId.value());
+				INDEX_BY_API_ID.put(apiInfo.getId(), apiInfo);
+				apiInfoSet.add(apiInfo);
+			}
+			INDEX_BY_METHOD.put(handlerMethod.getMethod(), apiInfo);
+
+			INDEX_BY_MAPPING.put(mapping, apiInfo);
+		}
+	}
+
+}

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

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.web.support;
+
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+
+/**
+ * classpath 定位
+ *
+ * @author WANGWEI
+ * @date 2019年11月8日
+ * @Copyright (c) 2018-? WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public class ClasspathHelper {
+
+	private static final String LOCATION_FILE = "classpath.location";
+
+	/**
+	 * 获取classpath
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static String getClasspath() {
+		String location = PathUtil.getResoucePath(LOCATION_FILE);
+		String classPath = location.substring(0, location.lastIndexOf(LOCATION_FILE));
+		return classPath;
+	}
+
+}

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

@@ -0,0 +1,235 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+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.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.api.commons.exchange.BaseResponse;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.ObjectUtil;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.config.LogProperties;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+
+/**
+ * spring mvc controller aspect.
+ *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Component
+@Aspect
+public class ControllerAspect {
+
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(ControllerAspect.class);
+
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	@Autowired(required = false)
+	HttpMethodProcessor httpMethodProcessor;
+
+	@Autowired
+	LogProperties logProperties;
+
+	@Autowired(required = false)
+	RedisClient redisClient;
+
+	/**
+	 * 构造函数
+	 *
+	 */
+	public ControllerAspect() {
+		super();
+		DEBUG_LOG.info("Aspect class [ControllerAspect]  is active!");
+	}
+
+	private static String[] excludeFields = new String[]{"password", ".*Password"};
+
+	/**
+	 * 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();
+
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			INTERFACE_LOG
+					.info(StringUtil.join("[HTTP-IN]. path=\"", path, "\", method=[", method, "]"));
+		}
+
+		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], excludeFields));
+				} 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;
+		try {
+			if (null != httpMethodProcessor) {
+				httpMethodProcessor.beforeMethod(request, args);
+			}
+			// 执行
+			ret = joinPoint.proceed();
+
+			request.setAttribute(HttpServletRequestAttribute.$_EXCEPTION_HAPPENED.name(),
+					new Boolean(false));
+
+		} catch (Throwable e) {
+
+			request.setAttribute(HttpServletRequestAttribute.$_EXCEPTION_HAPPENED.name(),
+					new Boolean(true));
+
+			if (null != httpMethodProcessor) {
+				try {
+					httpMethodProcessor.onException(request, args, e);
+				} catch (Exception ex) {
+					INTERFACE_LOG.error("[AFTER-METHOD].onException()", ex);
+				}
+			}
+			INTERFACE_LOG.error(StringUtil.join("[HTTP-FAIL]. path=\"", path, "\", method=[",
+					method, "] ; cost ", System.currentTimeMillis() - startTime, " ms."));
+			throw new RuntimeException(e);
+		}
+
+		if (null != httpMethodProcessor) {
+			try {
+				httpMethodProcessor.onSuccess(request, args, ret);
+			} catch (Exception ex) {
+				INTERFACE_LOG.error("[AFTER-METHOD].onSuccess()", ex);
+			}
+		}
+
+		if (null != ret && BaseResponse.class.isAssignableFrom(ret.getClass())) {
+			BaseResponse baseResponse = (BaseResponse) ret;
+			baseResponse.setCost(System.currentTimeMillis() - startTime);
+		}
+
+		if (INTERFACE_LOG.isDebugEnabled() && logProperties.isNormalResponseLogEnable()) {
+			boolean jsonSerializable = false;
+			if (null == ret) {
+				INTERFACE_LOG.debug("[HTTP-RESP]. status=" + HttpStatus.OK);
+				INTERFACE_LOG.debug("[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.debug("[HTTP-RESP]. status=" + re.getStatusCodeValue());
+					if (jsonSerializable) {
+						String json = JsonUtil.toJson(body);
+						int responseJsonMaxSize = logProperties.getResponseLogJsonMaxSize();
+						if (json.length() > responseJsonMaxSize) {
+							INTERFACE_LOG.debug("[HTTP-RESP]. response= too large");
+						} else {
+							INTERFACE_LOG.debug("[HTTP-RESP]. response=" + json);
+						}
+					}
+				}
+			} 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]. status=" + HttpStatus.OK.value());
+				if (jsonSerializable) {
+					String json = JsonUtil.toJson(ret);
+					int responseJsonMaxSize = logProperties.getResponseLogJsonMaxSize();
+					if (json.length() > responseJsonMaxSize) {
+						INTERFACE_LOG.debug("[HTTP-RESP]. response= too large");
+					} else {
+						INTERFACE_LOG.debug("[HTTP-RESP]. response=" + json);
+					}
+				}
+			}
+		}
+
+		HttpServletResponse response = ServletUtil.getResponse();
+		response.setHeader("Trace-Id", ThreadLocalUtil.getTraceId());
+		response.setHeader("cost", String.valueOf(System.currentTimeMillis() - startTime));
+
+		INTERFACE_LOG.info(StringUtil.join("[HTTP-OK]. path=\"", path, "\", method=[", method,
+				"] ; cost ", System.currentTimeMillis() - startTime, " ms."));
+
+		return ret;
+	}
+
+}

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

@@ -0,0 +1,350 @@
+package cn.com.qmth.examcloud.web.support;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.Role;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.api.commons.security.enums.RoleMeta;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.ObjectUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import com.google.common.collect.Lists;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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(HttpServletRequestAttribute.$_ACCESS_USER.name());
+        if (null == accessUser) {
+            throw new StatusException("252", "请重新登陆");
+        }
+        return accessUser;
+    }
+
+    /**
+     * 设置http响应码总是为200
+     *
+     * @author WANGWEI
+     */
+    protected void setAlwaysOKResponse() {
+        ServletUtil.getRequest().setAttribute(HttpServletRequestAttribute.$_ALWAYS_OK.name(), true);
+    }
+
+    /**
+     * 获取企业的顶级机构(非session状态)
+     *
+     * @return
+     * @author WANGWEI
+     */
+    protected Long getEnterpriseRootOrgId() {
+        Long rootOrgId = (Long) getRequest()
+                .getAttribute(HttpServletRequestAttribute.$_ENTERPRISE_ROOT_ORG_ID.name());
+        if (null == rootOrgId) {
+            throw new StatusException("280", "安全接入的顶级机构ID为空");
+        }
+        return rootOrgId;
+    }
+
+    /**
+     * 获取顶级机构(session状态)
+     *
+     * @return
+     * @author WANGWEI
+     */
+    protected Long getRootOrgId() {
+        Long rootOrgId = getAccessUser().getRootOrgId();
+        return rootOrgId;
+    }
+
+    /**
+     * 判断是否超级管理员
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 验证顶级机构隔离
+     *
+     * @param rootOrgId
+     * @author WANGWEI
+     */
+    protected void validateRootOrgIsolation(Long rootOrgId) {
+        if ((!isSuperAdmin()) && (!rootOrgId.equals(getRootOrgId()))) {
+            throw new StatusException("250", "非法请求");
+        }
+    }
+
+    /**
+     * 获取接入用户的角色ID集合
+     *
+     * @return
+     * @author WANGWEI
+     */
+    protected List<Long> getAccessUserRoleIdList() {
+        List<Role> roleList = getAccessUser().getRoleList();
+        List<Long> roleIdList = Lists.newArrayList();
+        for (Role cur : roleList) {
+            roleIdList.add(cur.getRoleId());
+        }
+        return roleIdList;
+    }
+
+    /**
+     * 判断用户是否拥有指定的角色
+     *
+     * @param role
+     * @return
+     * @author WANGWEI
+     */
+    protected Boolean hasRole(RoleMeta role) {
+        List<Role> roleList = getAccessUser().getRoleList();
+        for (Role cur : roleList) {
+            if (cur.getRoleCode().equals(role.name())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取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;
+    }
+
+    /**
+     * 文件导出
+     *
+     * @param fileName
+     * @param file
+     * @author WANGWEI
+     */
+    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);
+        }
+    }
+
+    /**
+     * 文件导出
+     *
+     * @param fileName
+     * @param bytes
+     * @author WANGWEI
+     */
+    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);
+        }
+    }
+
+    /**
+     * 转换为数据库模糊查询匹配模式
+     */
+    protected String toSqlSearchPattern(String column) {
+        if (StringUtils.isBlank(column)) {
+            return "%";
+        }
+        return "%" + column.trim() + "%";
+    }
+
+    /**
+     * 转换为数据库模糊查询匹配模式 (左边全匹配、右边模糊匹配)
+     */
+    protected String toSqlRightLike(String column) {
+        if (StringUtils.isBlank(column)) {
+            return "%";
+        }
+        return column.trim() + "%";
+    }
+
+    /**
+     * 获取非空参数
+     *
+     * @param paramName
+     * @return
+     * @author WANGWEI
+     */
+    protected String getRequiredStringParam(String paramName) {
+        String value = getRequest().getParameter(paramName);
+        if (StringUtils.isBlank(value)) {
+            throw new StatusException("520", "param is  missing. paramName=" + paramName);
+        } else {
+            return value.trim();
+        }
+    }
+
+    /**
+     * 获取参数
+     *
+     * @param paramName
+     * @return
+     * @author WANGWEI
+     */
+    protected String getStringParam(String paramName) {
+        String value = getRequest().getParameter(paramName);
+        if (StringUtils.isBlank(value)) {
+            return null;
+        } else {
+            return value.trim();
+        }
+    }
+
+    /**
+     * 参数trim.<br>
+     * set null if blank.
+     *
+     * @param bean
+     * @author WANGWEI
+     */
+    protected void trim(Object bean) {
+        trim(bean, false);
+    }
+
+    /**
+     * 参数trim
+     *
+     * @param bean
+     * @param nullIfBlank
+     * @author WANGWEI
+     */
+    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);
+        }
+    }
+
+}

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

@@ -0,0 +1,290 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.support.MissingServletRequestPartException;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.web.cloud.AppSelfHolder;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.exception.ApiFlowLimitedException;
+import cn.com.qmth.examcloud.web.exception.SequenceLockException;
+import cn.com.qmth.examcloud.web.jpa.DataIntegrityViolationTransverter;
+
+/**
+ * 异常转换器
+ *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@ControllerAdvice
+@ResponseBody
+public class CustomExceptionHandler {
+
+	/**
+	 * 接口日志
+	 */
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+			.getLog("INTERFACE_LOGGER");
+
+	/**
+	 * 异常处理
+	 *
+	 * @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) {
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500", "系统异常");
+			cause = e;
+		} else if (cause instanceof ApiFlowLimitedException) {
+			body = new StatusResponse("503", "limited. RPC");
+		} else if (cause instanceof StatusException) {
+			StatusException se = (StatusException) cause;
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-" + se.getCode(),
+					se.getDesc());
+		} else if (cause instanceof DataIntegrityViolationException) {
+			try {
+				DataIntegrityViolationTransverter
+						.throwIfDuplicateEntry((DataIntegrityViolationException) cause);
+				body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500", "数据完整性错误");
+			} catch (StatusException se) {
+				body = new StatusResponse(se.getCode(), se.getDesc());
+			}
+		} else if (cause instanceof IllegalStateException) {
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+					cause.getMessage());
+		} else if (cause instanceof javax.validation.ConstraintViolationException) {
+			javax.validation.ConstraintViolationException cvExcp = (ConstraintViolationException) cause;
+			Set<ConstraintViolation<?>> constraintViolations = cvExcp.getConstraintViolations();
+
+			StringBuffer errorMsg = new StringBuffer();
+			boolean isFirst = true;
+			for (ConstraintViolation<?> cv : constraintViolations) {
+				if (isFirst) {
+					errorMsg.append(cv.getMessage());
+					isFirst = false;
+				} else {
+					errorMsg.append("; ").append(cv.getMessage());
+				}
+			}
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+					errorMsg.toString());
+		} else if (cause instanceof SequenceLockException) {
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+					cause.getMessage());
+		} else if (cause instanceof org.springframework.jdbc.CannotGetJdbcConnectionException
+				|| cause instanceof org.springframework.orm.jpa.JpaSystemException
+				|| cause instanceof org.springframework.transaction.CannotCreateTransactionException) {
+			body = new StatusResponse("503", "limited. JDBC");
+		} else {
+			body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500", "系统异常");
+			cause = e;
+		}
+
+		return asResult(cause, body, request);
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public ResponseEntity<StatusResponse> handleException(MethodArgumentNotValidException e,
+			HttpServletRequest request) {
+		BindingResult result = e.getBindingResult();
+		List<ObjectError> allErrors = result.getAllErrors();
+		StringBuffer errorMsg = new StringBuffer();
+		boolean isFirst = true;
+		for (ObjectError err : allErrors) {
+			if (isFirst) {
+				errorMsg.append(err.getDefaultMessage());
+				isFirst = false;
+			} else {
+				errorMsg.append("; ").append(err.getDefaultMessage());
+			}
+		}
+		StatusResponse body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+				errorMsg.toString());
+		return asResult(e, body, request);
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(MissingServletRequestParameterException.class)
+	public ResponseEntity<StatusResponse> handleException(MissingServletRequestParameterException e,
+			HttpServletRequest request) {
+
+		StatusResponse body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+				e.getMessage());
+		return asResult(e, body, request);
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(MissingServletRequestPartException.class)
+	public ResponseEntity<StatusResponse> handleException(MissingServletRequestPartException e,
+			HttpServletRequest request) {
+
+		StatusResponse body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+				e.getMessage());
+		return asResult(e, body, request);
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(HttpMessageNotReadableException.class)
+	public ResponseEntity<StatusResponse> handleException(HttpMessageNotReadableException e,
+			HttpServletRequest request) {
+
+		StatusResponse body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500",
+				"Required request body is missing");
+		return asResult(e, body, request);
+	}
+
+	/**
+	 * 异常处理
+	 *
+	 * @author WANGWEI
+	 * @param e
+	 * @param request
+	 * @return
+	 */
+	@ExceptionHandler(Exception.class)
+	public ResponseEntity<StatusResponse> handleException(Exception e, HttpServletRequest request) {
+
+		StatusResponse body = new StatusResponse(AppSelfHolder.get().getAppCode() + "-500", "致命错误");
+
+		return asResult(e, body, request);
+	}
+
+	/**
+	 * 构建响应结果
+	 *
+	 * @author WANGWEI
+	 * @param t
+	 * @param body
+	 * @param request
+	 * @return
+	 */
+	private ResponseEntity<StatusResponse> asResult(Throwable t, StatusResponse body,
+			HttpServletRequest request) {
+		boolean alwaysOK = alwaysOK(request);
+		ApiInfo apiInfo = (ApiInfo) request
+				.getAttribute(HttpServletRequestAttribute.$_API_INFO.name());
+
+		boolean printStackTrace = true;
+		if (null != apiInfo) {
+			printStackTrace = !apiInfo.isWithoutStackTrace();
+		}
+		if (!printStackTrace) {
+			String forcePrintStackTrace = System.getProperty("log.forcePrintStackTrace");
+			if (null != forcePrintStackTrace
+					&& forcePrintStackTrace.equalsIgnoreCase(Boolean.toString(true))) {
+				printStackTrace = true;
+			}
+		}
+
+		HttpStatus httpStatus = null;
+		if (alwaysOK) {
+			if (t instanceof ApiFlowLimitedException
+					|| t instanceof org.springframework.jdbc.CannotGetJdbcConnectionException
+					|| t instanceof org.springframework.orm.jpa.JpaSystemException
+					|| t instanceof org.springframework.transaction.CannotCreateTransactionException) {
+				httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
+			} else {
+				httpStatus = HttpStatus.OK;
+			}
+		} else {
+			if (t instanceof ApiFlowLimitedException
+					|| t instanceof org.springframework.jdbc.CannotGetJdbcConnectionException
+					|| t instanceof org.springframework.orm.jpa.JpaSystemException
+					|| t instanceof org.springframework.transaction.CannotCreateTransactionException) {
+				httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
+			} else {
+				httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
+			}
+		}
+
+		INTERFACE_LOG.error("[HTTP-RESP]. status=" + httpStatus.value());
+
+		if (printStackTrace) {
+			INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body), t);
+		} else {
+			INTERFACE_LOG.error("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+		}
+
+		return new ResponseEntity<StatusResponse>(body, httpStatus);
+	}
+
+	/**
+	 * 是否总是响应200
+	 *
+	 * @author WANGWEI
+	 * @param request
+	 * @return
+	 */
+	private boolean alwaysOK(HttpServletRequest request) {
+		boolean alwaysOK = false;
+		Object attribute = request.getAttribute(HttpServletRequestAttribute.$_ALWAYS_OK.name());
+		if (null != attribute) {
+			try {
+				alwaysOK = (boolean) attribute;
+			} catch (Exception ex) {
+				// ignore
+			}
+		}
+		return alwaysOK;
+	}
+
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff