Sfoglia il codice sorgente

将examcloud-web合并至examcloud-support模块

deason 2 anni fa
parent
commit
404d9677a5
100 ha cambiato i file con 5578 aggiunte e 243 eliminazioni
  1. 106 1
      examcloud-support/pom.xml
  2. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/CacheConstants.java
  3. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/Constants.java
  4. 107 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusEndpoint.java
  5. 185 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfo.java
  6. 142 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoCollector.java
  7. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ApiStatusInfoHolder.java
  8. 274 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/DataReportor.java
  9. 125 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/HistogramInfo.java
  10. 75 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/MeterInfo.java
  11. 1 1
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricNames.java
  12. 10 10
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricRegistryHolder.java
  13. 48 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ReportInfo.java
  14. 22 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/ReportorHolder.java
  15. 175 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/TimerInfo.java
  16. 79 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/aliyun/AliYunAccount.java
  17. 63 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSite.java
  18. 135 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/aliyun/AliyunSiteManager.java
  19. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/aliyun/OssClient.java
  20. 392 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/baidu/BaiduClient.java
  21. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/bootstrap/AppBootstrap.java
  22. 121 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/bootstrap/BootstrapSecurityUtil.java
  23. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/bootstrap/PropertyHolder.java
  24. 56 58
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java
  25. 9 9
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/DefaultFullObjectCacheWatcher.java
  26. 29 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCache.java
  27. 28 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectCacheWatcher.java
  28. 114 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/FullObjectRedisCache.java
  29. 41 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashCache.java
  30. 16 15
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java
  31. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheProcessor.java
  32. 107 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheTrigger.java
  33. 47 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/ObjectCache.java
  34. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheProcessor.java
  35. 88 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/ObjectRedisCacheTrigger.java
  36. 42 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/RandomCacheBean.java
  37. 85 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/RandomObjectRedisCache.java
  38. 47 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/RefreshCacheReq.java
  39. 29 29
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java
  40. 37 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelf.java
  41. 44 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelfHolder.java
  42. 139 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientConfiguration.java
  43. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientSupport.java
  44. 62 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/CloudServiceRedirector.java
  45. 11 11
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/CustomFileSystemResource.java
  46. 65 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/ExamCloudDiscoveryClient.java
  47. 53 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/RibbonClientsConfiguration.java
  48. 43 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/config/LogProperties.java
  49. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/config/SystemProperties.java
  50. 9 11
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceAutoConfigure.java
  51. 21 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceBuilder.java
  52. 98 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceWrapper.java
  53. 65 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/RemoveDruidAdConfig.java
  54. 202 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/properties/DruidStatProperties.java
  55. 111 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidFilterConfiguration.java
  56. 17 18
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidSpringAopConfiguration.java
  57. 40 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidStatViewServletConfiguration.java
  58. 48 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidWebStatFilterConfiguration.java
  59. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/enums/HttpServletRequestAttribute.java
  60. 7 7
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/exception/ApiFlowLimitedException.java
  61. 7 7
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/exception/SequenceLockException.java
  62. 409 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/facepp/FaceppClient.java
  63. 2 2
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java
  64. 80 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageHelper.java
  65. 159 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStoragePathEnvInfo.java
  66. 66 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageSite.java
  67. 16 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorageType.java
  68. 45 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/YunHttpRequest.java
  69. 46 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/YunPathInfo.java
  70. 29 29
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java
  71. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunRefreshCdn.java
  72. 5 5
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java
  73. 71 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/GlobalHelper.java
  74. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/SequenceLockHelper.java
  75. 95 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/tree/EleTreeNode.java
  76. 27 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/tree/TreeNode.java
  77. 118 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/tree/TreeUtil.java
  78. 59 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/helpers/tree/ZtreeNode.java
  79. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiFlowLimitedInterceptor.java
  80. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/ApiStatisticInterceptor.java
  81. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/FirstInterceptor.java
  82. 1 5
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/GlobalSequenceLock.java
  83. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/SeqlockInterceptor.java
  84. 1 5
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSequenceLock.java
  85. 68 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/DataIntegrityViolationTransverter.java
  86. 57 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/JpaEntity.java
  87. 49 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/UniqueRule.java
  88. 10 10
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdAndStatusJpaEntity.java
  89. 10 10
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdJpaEntity.java
  90. 46 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/WithStatusJpaEntity.java
  91. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/mongodb/MongodbDetector.java
  92. 77 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/redis/CustomRedisConfiguration.java
  93. 155 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/redis/RedisClient.java
  94. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java
  95. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/DataRule.java
  96. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/DataRuleInterceptor.java
  97. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/RequestPermissionInterceptor.java
  98. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/ResourceManager.java
  99. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/RpcInterceptor.java
  100. 0 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/SecurityProperty.java

+ 106 - 1
examcloud-support/pom.xml

@@ -14,9 +14,114 @@
     <dependencies>
         <dependency>
             <groupId>cn.com.qmth.examcloud</groupId>
-            <artifactId>examcloud-web</artifactId>
+            <artifactId>examcloud-commons</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud</groupId>
+            <artifactId>examcloud-api-commons</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.framework</groupId>
+            <artifactId>config-center-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>log4j</groupId>
+                    <artifactId>log4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-cdn</artifactId>
+        </dependency>
         <dependency>
             <groupId>cn.com.qmth.examcloud</groupId>
             <artifactId>examcloud-question-commons</artifactId>

+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/support/CacheConstants.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/support/CacheConstants.java


+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/support/Constants.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/support/Constants.java


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

@@ -0,0 +1,107 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+import cn.com.qmth.examcloud.commons.util.FreeMarkerUtil;
+import cn.com.qmth.examcloud.commons.util.ResourceLoader;
+import com.google.common.collect.Maps;
+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 java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 接口状态
+ *
+ * @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
+     *
+     * @return
+     * @author WANGWEI
+     */
+    @ReadOperation(produces = {MediaType.TEXT_HTML_VALUE})
+    public String getByDefault() {
+        return get("");
+    }
+
+    /**
+     * url: /actuator/api-status/ignored?order=XX
+     *
+     * @param order
+     * @return
+     * @author WANGWEI
+     */
+    @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
examcloud-support/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;
+    }
+
+}

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

@@ -0,0 +1,142 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+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;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+@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;
+    }
+
+}

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


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

@@ -0,0 +1,274 @@
+package cn.com.qmth.examcloud.web.actuator;
+
+import com.codahale.metrics.Timer;
+import com.codahale.metrics.*;
+import com.google.common.collect.Lists;
+
+import java.util.*;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 数据报告
+ *
+ * @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
examcloud-support/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
examcloud-support/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;
+    }
+
+}

+ 1 - 1
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricNames.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricNames.java

@@ -9,6 +9,6 @@ package cn.com.qmth.examcloud.web.actuator;
  */
 public enum MetricNames {
 
-	API_TIMER, API_HISTOGRAM, API_METER, API_ERROR_METER
+    API_TIMER, API_HISTOGRAM, API_METER, API_ERROR_METER
 
 }

+ 10 - 10
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricRegistryHolder.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/actuator/MetricRegistryHolder.java

@@ -11,16 +11,16 @@ import com.codahale.metrics.MetricRegistry;
  */
 public class MetricRegistryHolder {
 
-	private static MetricRegistry defaultMetricRegistry = new MetricRegistry();
+    private static MetricRegistry defaultMetricRegistry = new MetricRegistry();
 
-	/**
-	 * 获取默认
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	public static MetricRegistry getDefalut() {
-		return defaultMetricRegistry;
-	}
+    /**
+     * 获取默认
+     *
+     * @return
+     * @author WANGWEI
+     */
+    public static MetricRegistry getDefalut() {
+        return defaultMetricRegistry;
+    }
 
 }

+ 48 - 0
examcloud-support/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
examcloud-support/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
examcloud-support/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;
+    }
+
+}

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

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

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

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

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

@@ -0,0 +1,135 @@
+package cn.com.qmth.examcloud.web.aliyun;
+
+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;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.thoughtworks.xstream.XStream;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 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);
+            }
+        }
+    }
+
+    /**
+     * 方法注释
+     *
+     * @param siteId
+     * @return
+     * @author
+     */
+    public static AliyunSite getAliyunSite(String siteId) {
+        AliyunSite aliyunSite = SITE_HOLDERS.get(siteId);
+
+        if (null == aliyunSite) {
+            throw new StatusException("20006", "aliyun.xml siteId not exist.");
+        }
+        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());
+    }
+
+}

+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/aliyun/OssClient.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/aliyun/OssClient.java


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

@@ -0,0 +1,392 @@
+package cn.com.qmth.examcloud.web.baidu;
+
+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.util.JsonUtil;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * baidu 客户端
+ *
+ * @author WANGWEI
+ * @date 2019年9月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class BaiduClient {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BaiduClient.class);
+
+    private static CloseableHttpClient httpclient;
+
+    private static RequestConfig requestConfig;
+
+    private static BaiduClient baiduClient;
+
+    private static String apiKey;
+
+    private static String secretKey;
+
+    private BaiduClient() {
+    }
+
+    /**
+     * 获取单例
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 活体检测
+     *
+     * @param imageUrl
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 活体检测
+     *
+     * @param imageUrl
+     * @return
+     * @author WANGWEI
+     */
+    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>
+     *
+     * @param imageUrl       主地址
+     * @param backupImageUrl 备用地址
+     * @return
+     * @throws StatusException code为901,902,903表示图片地址无效
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 是否重试
+     *
+     * @param errCode
+     * @return
+     * @author WANGWEI
+     */
+    private boolean retry(long errCode) {
+        return 222204 == errCode || 222013 == errCode;
+    }
+
+}

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


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

@@ -0,0 +1,121 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.AES;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * 安全工具
+ *
+ * @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 = "$$.";
+
+    /**
+     * 加密
+     *
+     * @param s
+     * @param secretKey
+     * @author WANGWEI
+     */
+    public static String encrypt(String s, String secretKey) {
+        AES aes = new AES(secretKey + RANDOM);
+        String encrypted = aes.encrypt(s);
+        return encrypted;
+    }
+
+    /**
+     * 解密
+     *
+     * @param s
+     * @param secretKey
+     * @author WANGWEI
+     */
+    public static String decrypt(String s, String secretKey) {
+        AES aes = new AES(secretKey + RANDOM);
+        String decrypted = aes.decrypt(s);
+        return decrypted;
+    }
+
+    /**
+     * 解密
+     *
+     * @param props
+     * @param secretKey
+     * @author WANGWEI
+     */
+    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());
+        }
+    }
+
+    /**
+     * 解密
+     *
+     * @param props
+     * @param secretKey
+     * @author WANGWEI
+     */
+    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());
+        }
+    }
+
+}

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


+ 56 - 58
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java

@@ -1,7 +1,12 @@
 package cn.com.qmth.examcloud.web.cache;
 
-import java.util.Map;
-
+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.util.JsonUtil;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import com.google.common.collect.Maps;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -9,14 +14,7 @@ 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.util.JsonUtil;
-import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import java.util.Map;
 
 /**
  * cache
@@ -29,54 +27,54 @@ import cn.com.qmth.examcloud.web.support.SpringContextHolder;
 @RequestMapping("cache")
 public class CacheCloudServiceProvider implements CloudService {
 
-	private static final long serialVersionUID = -5326807830421467943L;
+    private static final long serialVersionUID = -5326807830421467943L;
 
     private static final Logger LOG = LoggerFactory.getLogger(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")
+    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();
@@ -100,7 +98,7 @@ public class CacheCloudServiceProvider implements CloudService {
                 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];
@@ -127,8 +125,8 @@ public class CacheCloudServiceProvider implements CloudService {
                 throw new StatusException("008002", "class not found", e);
             }
         }
-        hashCache.refresh(expectedKeys,expectedSubKeys);
-        Object object = hashCache.get(expectedKeys,expectedSubKeys);
+        hashCache.refresh(expectedKeys, expectedSubKeys);
+        Object object = hashCache.get(expectedKeys, expectedSubKeys);
         return JsonUtil.toJson(object);
     }
 

+ 9 - 9
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/DefaultFullObjectCacheWatcher.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/DefaultFullObjectCacheWatcher.java

@@ -9,16 +9,16 @@ package cn.com.qmth.examcloud.web.cache;
  */
 public class DefaultFullObjectCacheWatcher implements FullObjectCacheWatcher {
 
-	private Boolean allLoaded = false;
+    private Boolean allLoaded = false;
 
-	@Override
-	public boolean allLoaded() {
-		return allLoaded;
-	}
+    @Override
+    public boolean allLoaded() {
+        return allLoaded;
+    }
 
-	@Override
-	public void setAllLoaded(boolean allLoaded) {
-		this.allLoaded = allLoaded;
-	}
+    @Override
+    public void setAllLoaded(boolean allLoaded) {
+        this.allLoaded = allLoaded;
+    }
 
 }

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

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

+ 28 - 0
examcloud-support/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 {
+
+    /**
+     * 缓存是否全部加载
+     *
+     * @return
+     * @author WANGWEI
+     */
+    public boolean allLoaded();
+
+    /**
+     * 设置缓存是否全部加载
+     *
+     * @param allLoaded
+     * @author WANGWEI
+     */
+    public void setAllLoaded(boolean allLoaded);
+
+}

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

@@ -0,0 +1,114 @@
+package cn.com.qmth.examcloud.web.cache;
+
+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;
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import java.util.List;
+
+/**
+ * 全量reids缓存
+ *
+ * @param <T>
+ * @author WANGWEI
+ * @date 2019年3月13日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+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
examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashCache.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * Hash缓存
+ *
+ * @param <T>
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+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);
+
+}

+ 16 - 15
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java

@@ -1,18 +1,17 @@
 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;
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
 
 /**
  * Hash redis缓存
  *
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
  * @param <T>
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
  */
 public abstract class HashRedisCache<T extends RandomCacheBean> implements HashCache<T> {
 
@@ -33,28 +32,29 @@ public abstract class HashRedisCache<T extends RandomCacheBean> implements HashC
         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);
+        Object object = getRedisClient().get(key, subkey, Object.class);
         @SuppressWarnings("unchecked")
         T t = (T) object;
         return t;
     }
+
     @Override
-    public T get(Object[] keys,Object[] subkeys) {
+    public T get(Object[] keys, Object[] subkeys) {
         String key = buildKey(keys);
         String subkey = buildSubKey(subkeys);
-        T t = getFromCache(key,subkey);
+        T t = getFromCache(key, subkey);
 
         if (null == t) {
-            refresh(keys,subkeys);
+            refresh(keys, subkeys);
         }
-        t = getFromCache(key,subkey);
+        t = getFromCache(key, subkey);
         return t;
     }
 
@@ -65,25 +65,26 @@ public abstract class HashRedisCache<T extends RandomCacheBean> implements HashC
     }
 
     @Override
-    public void refresh(Object[] keys,Object[] subkeys) {
+    public void refresh(Object[] keys, Object[] subkeys) {
         String key = buildKey(keys);
         String subkey = buildSubKey(subkeys);
         T t = null;
         try {
-            t = loadFromResource(keys,subkeys);
+            t = loadFromResource(keys, subkeys);
         } catch (StatusException e) {
             throw e;
         } catch (Exception e) {
-            throw new ExamCloudRuntimeException("fail to load data. key=" + key+" subkey="+subkey, 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);
+            getRedisClient().set(key, subkey, t, timeout);
         } else {
-            getRedisClient().delete(key,subkey);
+            getRedisClient().delete(key, subkey);
         }
     }
+
 }

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


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

@@ -0,0 +1,107 @@
+package cn.com.qmth.examcloud.web.cache;
+
+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;
+import com.google.common.collect.Maps;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 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();
+
+    /**
+     * 开火
+     *
+     * @param appName
+     * @param keys
+     * @return
+     * @author
+     */
+    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
examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/ObjectCache.java

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

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


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

@@ -0,0 +1,88 @@
+package cn.com.qmth.examcloud.web.cache;
+
+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;
+import com.google.common.collect.Maps;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 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();
+
+    /**
+     * 开火
+     *
+     * @param appName
+     * @param keys
+     * @return
+     * @author WANGWEI
+     */
+    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
examcloud-support/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;
+    }
+
+}

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

@@ -0,0 +1,85 @@
+package cn.com.qmth.examcloud.web.cache;
+
+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;
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+/**
+ * 随机redis缓存
+ *
+ * @param <T>
+ * @author WANGWEI
+ * @date 2019年4月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+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
examcloud-support/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;
+    }
+
+}

+ 29 - 29
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java

@@ -6,68 +6,68 @@ import cn.com.qmth.examcloud.api.commons.exchange.BaseRequest;
 /**
  * 刷新Hash缓存请求
  *
- * @author 
+ * @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 String[] keys;
 
-	private BasicDataType[] typeArray;
-	
-	private String[] subKeys;
+    private BasicDataType[] typeArray;
+
+    private String[] subKeys;
 
     private BasicDataType[] subTypeArray;
 
-	public String getClassName() {
-		return className;
-	}
+    public String getClassName() {
+        return className;
+    }
 
-	public void setClassName(String className) {
-		this.className = className;
-	}
+    public void setClassName(String className) {
+        this.className = className;
+    }
 
-	public String[] getKeys() {
-		return keys;
-	}
+    public String[] getKeys() {
+        return keys;
+    }
 
-	public void setKeys(String[] keys) {
-		this.keys = keys;
-	}
+    public void setKeys(String[] keys) {
+        this.keys = keys;
+    }
 
-	public BasicDataType[] getTypeArray() {
-		return typeArray;
-	}
+    public BasicDataType[] getTypeArray() {
+        return typeArray;
+    }
+
+    public void setTypeArray(BasicDataType[] typeArray) {
+        this.typeArray = 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
examcloud-support/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
+     *
+     * @return
+     * @author WANGWEI
+     */
+    Long getAppId();
+
+    /**
+     * 获取当前APP识别码<br>
+     * (通常为1到3位大写字母和数字组合)
+     *
+     * @return
+     * @author WANGWEI
+     */
+    String getAppCode();
+
+    /**
+     * 获取当前APP 密钥
+     *
+     * @return
+     * @author WANGWEI
+     */
+    String getSecretKey();
+
+}

+ 44 - 0
examcloud-support/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;
+
+    /**
+     * 获取
+     *
+     * @return
+     * @author WANGWEI
+     */
+    public static AppSelf get() {
+        if (null == appSelf) {
+            appSelf = SpringContextHolder.getBean(AppSelf.class);
+            if (null == appSelf) {
+                throw new ExamCloudRuntimeException("no AppSelf");
+            }
+        }
+
+        return appSelf;
+    }
+
+    /**
+     * 设置
+     *
+     * @param appSelf
+     * @author WANGWEI
+     */
+    public static void set(AppSelf appSelf) {
+        AppSelfHolder.appSelf = appSelf;
+    }
+
+}

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

@@ -0,0 +1,139 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.*;
+import org.springframework.web.client.DefaultResponseErrorHandler;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 Logger LOG = LoggerFactory.getLogger(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 {
+            LOG.info(
+                    "value of property[examcloud.rpc.httpRequestFactory] is wrong. will use default value [OkHttp3] .");
+            return buildOkHttp3ClientHttpRequestFactory();
+        }
+    }
+
+    /**
+     * Simple
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+}

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


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

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * 云服务 重定向
+ *
+ * @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;
+
+    /**
+     * 设置重定向映射
+     *
+     * @param originAppName 原始服务
+     * @param targetAppName 重定向服务
+     * @author WANGWEI
+     */
+    public static void setRedirection(String originAppName, String targetAppName) {
+        rules.put(originAppName, targetAppName);
+    }
+
+    /**
+     * 设置所有服务附加后缀映射.优先级低于 {@link #setRedirection}
+     *
+     * @param suffixName
+     * @author WANGWEI
+     * @see #setRedirection
+     */
+    public static void appendSuffix(String suffixName) {
+        CloudServiceRedirector.suffix = suffixName;
+    }
+
+    /**
+     * 获取重定向映射.不存在时返回null
+     *
+     * @param originAppName
+     * @return
+     * @author WANGWEI
+     */
+    public static String getRedirection(String originAppName) {
+
+        String redirection = rules.get(originAppName);
+        if (null != redirection) {
+            return redirection;
+        }
+        if (null != suffix) {
+            return originAppName + suffix;
+        }
+
+        return null;
+    }
+
+}

+ 11 - 11
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cloud/CustomFileSystemResource.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/cloud/CustomFileSystemResource.java

@@ -1,9 +1,9 @@
 package cn.com.qmth.examcloud.web.cloud;
 
-import java.io.File;
-
 import org.springframework.core.io.FileSystemResource;
 
+import java.io.File;
+
 /**
  * 从定义文件名
  *
@@ -13,16 +13,16 @@ import org.springframework.core.io.FileSystemResource;
  */
 public class CustomFileSystemResource extends FileSystemResource {
 
-	private String redefinedFileName;
+    private String redefinedFileName;
 
-	public CustomFileSystemResource(File file, String redefinedFileName) {
-		super(file);
-		this.redefinedFileName = redefinedFileName;
-	}
+    public CustomFileSystemResource(File file, String redefinedFileName) {
+        super(file);
+        this.redefinedFileName = redefinedFileName;
+    }
 
-	@Override
-	public String getFilename() {
-		return redefinedFileName;
-	}
+    @Override
+    public String getFilename() {
+        return redefinedFileName;
+    }
 
 }

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

@@ -0,0 +1,65 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+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 java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 注册中心客户端
+ *
+ * @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
+     *
+     * @param appName
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+}

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

@@ -0,0 +1,53 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import com.netflix.loadbalancer.IPing;
+import com.netflix.loadbalancer.IRule;
+import com.netflix.loadbalancer.PingUrl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * RibbonClients 配置
+ *
+ * @author WANGWEI
+ * @date 2019年1月28日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+public class RibbonClientsConfiguration {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RibbonClientsConfiguration.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 {
+            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
examcloud-support/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;
+    }
+
+}

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


+ 9 - 11
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceAutoConfigure.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/DruidDataSourceAutoConfigure.java

@@ -1,7 +1,11 @@
 package cn.com.qmth.examcloud.web.druid;
 
-import javax.sql.DataSource;
-
+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;
+import com.alibaba.druid.pool.DruidDataSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
@@ -15,23 +19,17 @@ 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;
+import javax.sql.DataSource;
 
 @Configuration
 @ConditionalOnClass(DruidDataSource.class)
 @AutoConfigureBefore(DataSourceAutoConfiguration.class)
 @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
 @Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class,
-		DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class})
+        DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class})
 public class DruidDataSourceAutoConfigure {
 
-	private static final Logger LOG = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
+    private static final Logger LOG = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
 
     @Bean(initMethod = "init")
     @ConditionalOnMissingBean

+ 21 - 0
examcloud-support/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.
+     * <p>
+     * ------- The method is history, and now you can use 'new
+     * DruidDataSourceWrapper()' instead.
+     */
+    public DruidDataSource build() {
+        return new DruidDataSourceWrapper();
+    }
+
+}

+ 98 - 0
examcloud-support/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()}.
+     * <p>
+     * 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;
+        }
+    }
+
+}

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

@@ -0,0 +1,65 @@
+package cn.com.qmth.examcloud.web.druid;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+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 javax.servlet.*;
+import java.io.IOException;
+
+/**
+ * 去除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;
+    }
+
+}

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

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

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

@@ -0,0 +1,111 @@
+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.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.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";
+
+}

+ 17 - 18
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidSpringAopConfiguration.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/druid/stat/DruidSpringAopConfiguration.java

@@ -1,5 +1,7 @@
 package cn.com.qmth.examcloud.web.druid.stat;
 
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;
 import org.aopalliance.aop.Advice;
 import org.springframework.aop.Advisor;
 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
@@ -7,27 +9,24 @@ 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 Advice advice() {
+        return new DruidStatInterceptor();
+    }
+
+    @Bean
+    public Advisor advisor(DruidStatProperties properties) {
+        return new RegexpMethodPointcutAdvisor(properties.getAopPatterns(), advice());
+    }
 
-	@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;
+    }
 
-	@Bean
-	public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
-		DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
-		advisorAutoProxyCreator.setProxyTargetClass(true);
-		return advisorAutoProxyCreator;
-	}
 }

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

@@ -0,0 +1,40 @@
+package cn.com.qmth.examcloud.web.druid.stat;
+
+import cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+import com.alibaba.druid.support.http.StatViewServlet;
+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;
+
+@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
examcloud-support/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 cn.com.qmth.examcloud.web.druid.properties.DruidStatProperties;
+import com.alibaba.druid.support.http.WebStatFilter;
+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;
+
+@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;
+    }
+
+}

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


+ 7 - 7
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/exception/ApiFlowLimitedException.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/exception/ApiFlowLimitedException.java

@@ -11,14 +11,14 @@ import cn.com.qmth.examcloud.commons.exception.StatusException;
  */
 public class ApiFlowLimitedException extends StatusException {
 
-	private static final long serialVersionUID = -3088431872660870243L;
+    private static final long serialVersionUID = -3088431872660870243L;
 
-	public ApiFlowLimitedException(String code, String desc) {
-		super(code, desc);
-	}
+    public ApiFlowLimitedException(String code, String desc) {
+        super(code, desc);
+    }
 
-	public ApiFlowLimitedException(String code, String desc, Throwable cause) {
-		super(code, desc, cause);
-	}
+    public ApiFlowLimitedException(String code, String desc, Throwable cause) {
+        super(code, desc, cause);
+    }
 
 }

+ 7 - 7
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/exception/SequenceLockException.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/exception/SequenceLockException.java

@@ -9,14 +9,14 @@ package cn.com.qmth.examcloud.web.exception;
  */
 public class SequenceLockException extends RuntimeException {
 
-	private static final long serialVersionUID = 6799267531884646906L;
+    private static final long serialVersionUID = 6799267531884646906L;
 
-	public SequenceLockException() {
-		super();
-	}
+    public SequenceLockException() {
+        super();
+    }
 
-	public SequenceLockException(String message) {
-		super(message);
-	}
+    public SequenceLockException(String message) {
+        super(message);
+    }
 
 }

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

@@ -0,0 +1,409 @@
+package cn.com.qmth.examcloud.web.facepp;
+
+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.web.bootstrap.PropertyHolder;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * face++ 客户端
+ *
+ * @author WANGWEI
+ * @date 2019年9月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class FaceppClient {
+
+    private static final Logger LOG = LoggerFactory.getLogger(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() {
+    }
+
+    /**
+     * 获取单例
+     *
+     * @return
+     * @author WANGWEI
+     */
+    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");
+        }
+    }
+
+    /**
+     * 人脸识别
+     *
+     * @param faceToken
+     * @param imageUrl
+     * @return
+     * @author WANGWEI
+     */
+    public JsonHttpResponseHolder compareWithTokenAndImageUrl(String faceToken, String imageUrl) {
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[Face++ Request]. faceToken=" + faceToken + "; imageUrl=" + imageUrl);
+        }
+
+        String url = PropertyHolder.getString("facepp.compare.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;
+    }
+
+    /**
+     * 人脸识别
+     *
+     * @param faceToken
+     * @param imageBase64
+     * @return
+     * @author WANGWEI
+     */
+    public JsonHttpResponseHolder compareWithTokenAndBase64(String faceToken, String imageBase64) {
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[Face++ Request]. faceToken=" + faceToken + "; imageBase64=?");
+        }
+
+        String url = PropertyHolder.getString("facepp.compare.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;
+    }
+
+    /**
+     * 人脸识别
+     *
+     * @param imageUrl1
+     * @param imageUrl2
+     * @return
+     * @author WANGWEI
+     */
+    public JsonHttpResponseHolder compareWithImageUrl(String imageUrl1, String imageUrl2) {
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[Face++ Request]. imageUrl1=" + imageUrl1 + "; imageUrl2=" + imageUrl2);
+        }
+
+        String url = PropertyHolder.getString("facepp.compare.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>
+     *
+     * @param faceToken      face++预存照片
+     * @param imageUrl       主地址
+     * @param backupImageUrl 备用地址
+     * @return
+     * @throws StatusException code为801,802,803表示图片地址无效
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 是否重试
+     *
+     * @param errMsg
+     * @return
+     * @author WANGWEI
+     */
+    private boolean retry(String errMsg) {
+        if (null != errMsg) {
+            if (errMsg.startsWith("INVALID_IMAGE_URL")
+                    || errMsg.startsWith("IMAGE_DOWNLOAD_TIMEOUT")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 2 - 2
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java

@@ -28,8 +28,8 @@ public interface FileStorage {
      * @return 返回路径
      */
     YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN);
-    
-    YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN,Long cacheAge);
+
+    YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN, Long cacheAge);
 
     /**
      * 保存文件到存储服务

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

@@ -0,0 +1,80 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import org.apache.commons.lang3.StringUtils;
+
+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;
+    }
+
+}

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

@@ -0,0 +1,159 @@
+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
examcloud-support/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;
+    }
+
+}

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

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

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

@@ -0,0 +1,45 @@
+package cn.com.qmth.examcloud.web.filestorage;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+import java.util.Map;
+
+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;
+    }
+
+}

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

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

+ 29 - 29
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java

@@ -490,19 +490,19 @@ public class AliyunFileStorageImpl implements FileStorage {
         }
     }
 
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN,
-			Long cacheAge) {
-		try (InputStream in = new FileInputStream(file);) {
-            return saveFile(siteId, env, in, md5, refreshCDN,cacheAge);
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN,
+                                Long cacheAge) {
+        try (InputStream in = new FileInputStream(file);) {
+            return saveFile(siteId, env, in, md5, refreshCDN, cacheAge);
         } catch (Exception e) {
             throw new StatusException("5001", "上传出错", e);
         }
-	}
-	
-    private YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN,Long cacheAge) {
+    }
+
+    private YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN, Long cacheAge) {
         try {
-            String relativePath = uploadObject(siteId, env, in, md5,cacheAge);
+            String relativePath = uploadObject(siteId, env, in, md5, cacheAge);
             AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
             AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
             String url = FileStorageHelper.getUrl(ac.getDomain(), relativePath);
@@ -517,8 +517,8 @@ public class AliyunFileStorageImpl implements FileStorage {
             throw new StatusException("6001", "上传出错", e);
         }
     }
-    
-    private String uploadObject(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5,Long cacheAge) throws IOException, DecoderException {
+
+    private String uploadObject(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, Long cacheAge) throws IOException, DecoderException {
         AliyunSite as = AliyunSiteManager.getAliyunSite(siteId);
         AliYunAccount ac = AliyunSiteManager.getAliYunAccountByAliyunId(as.getAliyunId());
         String bucket = ac.getBucket();
@@ -526,26 +526,26 @@ public class AliyunFileStorageImpl implements FileStorage {
         // 阿里云文件路径
         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);
-//            if(cacheAge!=null) {
-//            	meta.setCacheControl("max-age="+cacheAge);
-//            }
-//            oss.putObject(bucket, path, in, meta);
-//        } else {
-//            oss.putObject(bucket, path, in);
-//        }
-        ObjectMetadata meta = new  ObjectMetadata();
-        if (StringUtils.isNotBlank(md5) ) {
-            md5 = Base64.getEncoder().encodeToString(Hex.decodeHex(md5) );
-            meta.setContentMD5(md5 );
+        //        if (StringUtils.isNotBlank(md5)) {
+        //            md5 = Base64.getEncoder().encodeToString(Hex.decodeHex(md5));
+        //            ObjectMetadata meta = new ObjectMetadata();
+        //            meta.setContentMD5(md5);
+        //            if(cacheAge!=null) {
+        //            	meta.setCacheControl("max-age="+cacheAge);
+        //            }
+        //            oss.putObject(bucket, path, in, meta);
+        //        } else {
+        //            oss.putObject(bucket, path, in);
+        //        }
+        ObjectMetadata meta = new ObjectMetadata();
+        if (StringUtils.isNotBlank(md5)) {
+            md5 = Base64.getEncoder().encodeToString(Hex.decodeHex(md5));
+            meta.setContentMD5(md5);
         }
-        if(cacheAge!=null ) {
-        	meta.setCacheControl("max-age="+cacheAge );
+        if (cacheAge != null) {
+            meta.setCacheControl("max-age=" + cacheAge);
         }
-        oss.putObject(bucket, path, in, meta );
+        oss.putObject(bucket, path, in, meta);
 
         return path;
     }

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


+ 5 - 5
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java

@@ -113,14 +113,14 @@ public class UpyunFileStorageImpl implements FileStorage {
         return path;
     }
 
-	@Override
-	public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN,
-			Long cacheAge) {
-		try (InputStream in = new FileInputStream(file);) {
+    @Override
+    public YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN,
+                                Long cacheAge) {
+        try (InputStream in = new FileInputStream(file);) {
             return saveFile(siteId, env, in, md5, refreshCDN);
         } catch (Exception e) {
             throw new StatusException("1001", "上传出错", e);
         }
-	}
+    }
 
 }

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

@@ -0,0 +1,71 @@
+package cn.com.qmth.examcloud.web.helpers;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * 全局 helper
+ *
+ * @author WANGWEI
+ * @date 2019年3月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class GlobalHelper {
+
+    /**
+     * 顶级机构一致性校验
+     *
+     * @param rootOrgIds
+     * @author WANGWEI
+     */
+    public static void uniformRootOrg(Long... rootOrgIds) {
+        if (!Util.equals(Arrays.asList(rootOrgIds))) {
+            throw new StatusException("120", "非法请求(顶级机构不一致)");
+        }
+    }
+
+    /**
+     * 获取存在的实体
+     *
+     * @param repo
+     * @param id
+     * @param se
+     * @return
+     * @author WANGWEI
+     */
+    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)
+     *
+     * @param repo
+     * @param id
+     * @param c
+     * @return
+     * @author WANGWEI
+     */
+    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();
+        }
+    }
+
+}

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


+ 95 - 0
examcloud-support/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 cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+import java.util.List;
+
+/**
+ * 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
examcloud-support/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);
+
+}

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

@@ -0,0 +1,118 @@
+package cn.com.qmth.examcloud.web.helpers.tree;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 树工具
+ *
+ * @author WANG WEI
+ */
+public class TreeUtil {
+
+    /**
+     * 转换
+     *
+     * @param treeNode
+     * @param c
+     * @return
+     * @author WANG WEI
+     */
+    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);
+        }
+    }
+
+    /**
+     * 转换
+     *
+     * @param treeNodeList
+     * @param c
+     * @return
+     * @author WANGWEI
+     */
+    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;
+    }
+
+    /**
+     * 转换
+     *
+     * @param root
+     * @param treeNodeList
+     * @param disabledCodeList
+     * @return
+     * @author WANGWEI
+     */
+    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
examcloud-support/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;
+    }
+
+}

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


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


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


+ 1 - 5
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/interceptor/GlobalSequenceLock.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/GlobalSequenceLock.java

@@ -1,10 +1,6 @@
 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;
+import java.lang.annotation.*;
 
 /**
  * 全局 请求顺序锁<br>

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


+ 1 - 5
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSequenceLock.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSequenceLock.java

@@ -1,10 +1,6 @@
 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;
+import java.lang.annotation.*;
 
 /**
  * session级 请求顺序锁<br>

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

@@ -0,0 +1,68 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import com.google.common.collect.Maps;
+import org.springframework.dao.DataIntegrityViolationException;
+
+import java.sql.SQLIntegrityConstraintViolationException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 数据完整性校验转换器
+ *
+ * @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);
+        }
+    }
+
+    /**
+     * 唯一性约束转换
+     *
+     * @param e
+     * @author WANGWEI
+     */
+    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);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 唯一性约束转换
+     *
+     * @param e
+     * @author WANGWEI
+     */
+    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());
+            }
+        }
+    }
+
+}

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

@@ -0,0 +1,57 @@
+package cn.com.qmth.examcloud.web.jpa;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.util.Date;
+
+/**
+ * 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
examcloud-support/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;
+    }
+
+}

+ 10 - 10
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdAndStatusJpaEntity.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdAndStatusJpaEntity.java

@@ -15,18 +15,18 @@ import javax.persistence.MappedSuperclass;
 @MappedSuperclass
 public abstract class WithIdAndStatusJpaEntity extends WithStatusJpaEntity {
 
-	private static final long serialVersionUID = 2909770208688213445L;
+    private static final long serialVersionUID = 2909770208688213445L;
 
-	@Id
-	@GeneratedValue(strategy = GenerationType.IDENTITY)
-	private Long id;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
 
-	public Long getId() {
-		return id;
-	}
+    public Long getId() {
+        return id;
+    }
 
-	public void setId(Long id) {
-		this.id = id;
-	}
+    public void setId(Long id) {
+        this.id = id;
+    }
 
 }

+ 10 - 10
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdJpaEntity.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/jpa/WithIdJpaEntity.java

@@ -15,18 +15,18 @@ import javax.persistence.MappedSuperclass;
 @MappedSuperclass
 public abstract class WithIdJpaEntity extends JpaEntity {
 
-	private static final long serialVersionUID = 2909770208688213445L;
+    private static final long serialVersionUID = 2909770208688213445L;
 
-	@Id
-	@GeneratedValue(strategy = GenerationType.IDENTITY)
-	private Long id;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
 
-	public Long getId() {
-		return id;
-	}
+    public Long getId() {
+        return id;
+    }
 
-	public void setId(Long id) {
-		this.id = id;
-	}
+    public void setId(Long id) {
+        this.id = id;
+    }
 
 }

+ 46 - 0
examcloud-support/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;
+    }
+
+}

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


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

@@ -0,0 +1,77 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+@Configuration
+public class CustomRedisConfiguration {
+
+    private static final Logger LOG = LoggerFactory.getLogger(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;
+        }
+
+        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("RedisTemplate init...");
+
+        return redisTemplate;
+    }
+
+    @Bean
+    public RedisClient redisClient(@Autowired(required = true) RedisTemplate<String, Object> redisTemplate) {
+        RedisClient redisClient = new SimpleRedisClient(redisTemplate);
+        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
+
+        if (connectionFactory instanceof MarkedJedisConnectionFactory) {
+            redisClient.setEnable(false);
+            LOG.warn("redisClient init... enable = false !!!");
+        } else {
+            redisClient.set("test", "test");
+            LOG.info("redisClient init... enable = true");
+        }
+
+        return redisClient;
+    }
+
+    /**
+     * 标识工厂
+     *
+     * @author WANGWEI
+     * @date 2019年3月22日
+     * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+     */
+    private class MarkedJedisConnectionFactory extends JedisConnectionFactory {
+
+    }
+
+}

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

@@ -0,0 +1,155 @@
+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 {
+
+    /**
+     * 是否可用
+     *
+     * @return
+     * @author WANGWEI
+     */
+    public boolean isEnable();
+
+    /**
+     * 设置是否可用
+     *
+     * @param enable
+     * @author WANGWEI
+     */
+    public void setEnable(boolean enable);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param value
+     * @author WANGWEI
+     */
+    public void set(String key, Object value);
+
+    /**
+     * @param key
+     * @param subkey
+     * @param value
+     */
+    public void set(String key, String subkey, Object value);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param value
+     * @param timeout
+     * @author WANGWEI
+     */
+    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);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param timeout
+     * @author WANGWEI
+     */
+    public void expire(String key, int timeout);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param timeout
+     * @param unit
+     * @author WANGWEI
+     */
+    public void expire(String key, final long timeout, final TimeUnit unit);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param c
+     * @param timeout
+     * @return
+     * @author WANGWEI
+     */
+    public <T> T get(String key, Class<T> c, int timeout);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @param c
+     * @return
+     * @author WANGWEI
+     */
+    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);
+
+    /**
+     * 方法注释
+     *
+     * @param key
+     * @author WANGWEI
+     */
+    public void delete(String key);
+
+    public void delete(String key, String subkey);
+
+    /**
+     * 方法注释
+     *
+     * @param channel
+     * @param message
+     * @author WANGWEI
+     */
+    public void convertAndSend(String channel, Object message);
+
+    /**
+     * (在key不存在时,创建并设置value 返回true; key存在时,会反回false)
+     *
+     * @param key
+     * @param value
+     * @param timeout
+     * @return
+     * @author WANGWEI
+     */
+    public Boolean setIfAbsent(String key, String value, int timeout);
+
+    /**
+     * 获取失效时间
+     *
+     * @param key
+     * @param timeUnit
+     * @return
+     * @author WANGWEI
+     */
+    public Long getExpire(String key, TimeUnit timeUnit);
+
+}

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


+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/security/DataRule.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/DataRule.java


+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/security/DataRuleInterceptor.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/DataRuleInterceptor.java


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


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


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


+ 0 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/security/SecurityProperty.java → examcloud-support/src/main/java/cn/com/qmth/examcloud/web/security/SecurityProperty.java


Some files were not shown because too many files changed in this diff