Эх сурвалжийг харах

merge from release_v4.1.1

deason 3 жил өмнө
parent
commit
39044d7bf8
38 өөрчлөгдсөн 1701 нэмэгдсэн , 586 устгасан
  1. 20 0
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/helpers/poi/ExcelReader.java
  2. 20 0
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/helpers/poi/XlsxHandler.java
  3. 20 20
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/Calculator.java
  4. 38 0
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/FileUtil.java
  5. 46 5
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/JsonMapper.java
  6. 294 291
      examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/OKHttpUtil.java
  7. 4 1
      examcloud-starters/examcloud-geetest-starter/src/main/java/cn/com/qmth/examcloud/starters/greetest/service/impl/GeetestServiceImpl.java
  8. 23 3
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/Constants.java
  9. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/ExamProperties.java
  10. 14 6
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/FaceBiopsyScheme.java
  11. 15 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java
  12. 29 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java
  13. 251 250
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/excel/ExcelExportUtil.java
  14. 22 1
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java
  15. 62 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/UploadResult.java
  16. 60 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/QuestionBodyHandler.java
  17. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/AudioTextHandler.java
  18. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java
  19. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/HtmlTextHandler.java
  20. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ImageTextHandler.java
  21. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandler.java
  22. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandlerFactory.java
  23. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/BlockBean.java
  24. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionBean.java
  25. 1 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionCollectionBean.java
  26. 100 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Block.java
  27. 28 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/BlockType.java
  28. 40 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Control.java
  29. 100 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Param.java
  30. 42 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Result.java
  31. 284 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/RichTextConverter.java
  32. 45 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Section.java
  33. 50 0
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/TagConstant.java
  34. 8 3
      examcloud-support/src/main/java/cn/com/qmth/examcloud/support/helper/FaceBiopsyHelper.java
  35. 2 0
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/FileStorage.java
  36. 60 4
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java
  37. 10 0
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/UpyunFileStorageImpl.java
  38. 4 2
      examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/CustomExceptionHandler.java

+ 20 - 0
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/helpers/poi/ExcelReader.java

@@ -2,6 +2,7 @@ package cn.com.qmth.examcloud.commons.helpers.poi;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.InputStream;
 import java.util.List;
 
 import org.apache.commons.io.IOUtils;
@@ -161,5 +162,24 @@ public class ExcelReader {
 
 		return list;
 	}
+	
+	public static List<String[]> readSheetBySax(InputStream is, int sheetId, int columnSize) {
+		List<String[]> list = Lists.newArrayList();
+
+		XlsxHandler xlsxHandler = new XlsxHandler(columnSize) {
+			@Override
+			public void optRows(int sheetIndex, int curRow, String[] row) {
+				list.add(row);
+			}
+		};
+
+		try {
+			xlsxHandler.processOneSheet(is, sheetId);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+
+		return list;
+	}
 
 }

+ 20 - 0
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/helpers/poi/XlsxHandler.java

@@ -88,6 +88,26 @@ public abstract class XlsxHandler extends DefaultHandler {
 			IOUtils.closeQuietly(pkg);
 		}
 	}
+	
+	public void processOneSheet(InputStream is, int sheetId) throws Exception {
+		OPCPackage pkg = null;
+		try {
+			pkg = OPCPackage.open(is);
+			XSSFReader r = new XSSFReader(pkg);
+			SharedStringsTable sst = r.getSharedStringsTable();
+
+			XMLReader parser = XMLReaderFactory.createXMLReader();
+			this.sst = sst;
+			parser.setContentHandler(this);
+			is = r.getSheet("rId" + sheetId);
+			sheetIndex++;
+			InputSource sheetSource = new InputSource(is);
+			parser.parse(sheetSource);
+		} finally {
+			IOUtils.closeQuietly(is);
+			IOUtils.closeQuietly(pkg);
+		}
+	}
 
 	/**
 	 * 方法注释

+ 20 - 20
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/Calculator.java

@@ -11,31 +11,31 @@ import java.math.BigDecimal;
  */
 public class Calculator {
 
-	public static double add(double v1, double v2) {
-		BigDecimal b1 = new BigDecimal(Double.toString(v1));
-		BigDecimal b2 = new BigDecimal(Double.toString(v2));
-		return b1.add(b2).doubleValue();
+    public static double add(double v1, double v2) {
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        return b1.add(b2).doubleValue();
 
-	}
+    }
 
-	public static double subtract(double v1, double v2) {
-		BigDecimal b1 = new BigDecimal(Double.toString(v1));
-		BigDecimal b2 = new BigDecimal(Double.toString(v2));
-		return b1.subtract(b2).doubleValue();
+    public static double subtract(double v1, double v2) {
+        BigDecimal b1 = new BigDecimal(Double.toString(v1));
+        BigDecimal b2 = new BigDecimal(Double.toString(v2));
+        return b1.subtract(b2).doubleValue();
 
-	}
+    }
 
-	public static double multiply(double v1, double v2) {
-		BigDecimal b1 = new BigDecimal(v1);
-		BigDecimal b2 = new BigDecimal(v2);
-		return b1.multiply(b2).doubleValue();
+    public static double multiply(double v1, double v2) {
+        BigDecimal b1 = BigDecimal.valueOf(v1);
+        BigDecimal b2 = BigDecimal.valueOf(v2);
+        return b1.multiply(b2).doubleValue();
 
-	}
+    }
 
-	public static double divide(double v1, double v2, int len) {
-		BigDecimal b1 = new BigDecimal(v1);
-		BigDecimal b2 = new BigDecimal(v2);
-		return b1.divide(b2, len, BigDecimal.ROUND_HALF_UP).doubleValue();
-	}
+    public static double divide(double v1, double v2, int len) {
+        BigDecimal b1 = BigDecimal.valueOf(v1);
+        BigDecimal b2 = BigDecimal.valueOf(v2);
+        return b1.divide(b2, len, BigDecimal.ROUND_HALF_UP).doubleValue();
+    }
 
 }

+ 38 - 0
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/FileUtil.java

@@ -5,8 +5,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Date;
+import java.util.Random;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -41,6 +44,21 @@ public class FileUtil {
         });
     }
 
+    /**
+     * 获取文件名(包含后缀名)
+     */
+    public static String getFileName(String filePath) {
+        if (filePath == null) {
+            return "";
+        }
+        filePath = filePath.replace("\\", "/");
+        int index = filePath.lastIndexOf("/");
+        if (index > -1) {
+            return filePath.substring(index + 1);
+        }
+        return filePath;
+    }
+
     /**
      * 获取文件后缀名,包括"."
      */
@@ -91,4 +109,24 @@ public class FileUtil {
         return true;
     }
 
+    /**
+     * 生成日期目录:yyyyMMdd
+     */
+    public static String dateDir() {
+        return new SimpleDateFormat("yyyyMMdd").format(new Date());
+    }
+
+    /**
+     * 生成文件名(15位时间戳+3位随机字母)
+     */
+    public static String generateFileName() {
+        Random r = new Random();
+        StringBuffer xyz = new StringBuffer();
+        for (int i = 0; i < 3; i++) {
+            // 小写字母
+            xyz.append((char) (r.nextInt(26) + 97));
+        }
+        return new SimpleDateFormat("yyMMddHHmmssSSS").format(new Date()) + xyz;
+    }
+
 }

+ 46 - 5
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/JsonMapper.java

@@ -14,10 +14,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * 简单封装Jackson,实现JSON 与 Java Object互相转换的Mapper
@@ -82,6 +79,7 @@ public class JsonMapper {
 
         //设置输入时忽略在JSON字符串中存在但Java对象实际没有的属性
         mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
         //忽略无法转换的对象
         mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
 
@@ -107,6 +105,24 @@ public class JsonMapper {
         }
     }
 
+    /**
+     * Object 转换为 Json(美化版)
+     * 若对象为 null 或 转换异常时,返回null
+     * 若集合为空集合,返回[]
+     */
+    public String toPrettyJson(Object object) {
+        if (object == null) {
+            return null;
+        }
+
+        try {
+            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+        } catch (IOException e) {
+            LOG.error("toPrettyJson error! " + e.getMessage(), e);
+            return null;
+        }
+    }
+
     /**
      * 反序列化POJO或简单Collection如List<String>
      * 如果JSON字符串为Null或"null"字符串, 返回Null
@@ -158,6 +174,15 @@ public class JsonMapper {
         }
     }
 
+    /**
+     * Json 转换为 T[]
+     * 若Json字符串为 null 或 empty 或 转换异常时,返回null
+     */
+    public <T> T[] toArray(String jsonStr, Class<T> clazz) {
+        JavaType javaType = this.constructArrayType(clazz);
+        return this.parseJson(jsonStr, javaType);
+    }
+
     /**
      * Json to List
      */
@@ -175,10 +200,19 @@ public class JsonMapper {
         }
     }
 
+    /**
+     * Json 转换为 Set<T>
+     * 若Json字符串为 null 或 empty 或 转换异常时,返回null
+     */
+    public <T> Set<T> toSet(String jsonStr, Class<T> clazz) {
+        JavaType javaType = this.constructCollectionType(Set.class, clazz);
+        return this.parseJson(jsonStr, javaType);
+    }
+
     /**
      * Json to HashMap
      */
-    public <T> Map<String, T> toHashMap(String jsonStr, Class<T> bean) {
+    public <T> Map<String, T> toMap(String jsonStr, Class<T> bean) {
         if (StringUtils.isEmpty(jsonStr)) {
             return null;
         }
@@ -192,6 +226,13 @@ public class JsonMapper {
         }
     }
 
+    /**
+     * 构造Array类型
+     */
+    public JavaType constructArrayType(Class<?> elementClass) {
+        return mapper.getTypeFactory().constructArrayType(elementClass);
+    }
+
     /**
      * 构造Collection类型
      */

+ 294 - 291
examcloud-commons/src/main/java/cn/com/qmth/examcloud/commons/util/OKHttpUtil.java

@@ -1,25 +1,18 @@
 package cn.com.qmth.examcloud.commons.util;
 
+import cn.com.qmth.examcloud.commons.helpers.FormFilePart;
+import okhttp3.*;
+import okhttp3.Request.Builder;
+import org.apache.commons.collections.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.commons.collections.CollectionUtils;
-
-import cn.com.qmth.examcloud.commons.helpers.FormFilePart;
-import okhttp3.FormBody;
-import okhttp3.MediaType;
-import okhttp3.MultipartBody;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Request.Builder;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 /**
  * OKHttp
  *
@@ -29,282 +22,292 @@ import org.slf4j.LoggerFactory;
  */
 public class OKHttpUtil {
 
-	private static final Logger LOG = LoggerFactory.getLogger(OKHttpUtil.class);
-
-	public static final class MediaTypes {
-
-		public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
-	}
-
-	/**
-	 * 请求体构建器
-	 *
-	 * @author WANGWEI
-	 * @date 2019年4月10日
-	 * @Copyright (c) 2018-2020 http://www.qmth.com.cn/ All Rights Reserved.
-	 */
-	public static interface RequestBodyBuilder {
-		RequestBody build();
-	}
-
-	/**
-	 * json请求体构建器
-	 *
-	 * @author WANGWEI
-	 * @date 2019年4月10日
-	 * @Copyright (c) 2018-2020 http://www.qmth.com.cn/ All Rights Reserved.
-	 */
-	public static final class JsonBodyBuilder implements RequestBodyBuilder {
-
-		private String json;
-
-		public JsonBodyBuilder(String json) {
-			super();
-			this.json = json;
-		}
-
-		@Override
-		public RequestBody build() {
-			return RequestBody.create(MediaTypes.JSON, json);
-		}
-
-		@Override
-		public String toString() {
-			return json;
-		}
-	}
-
-	private static OkHttpClient okHttpClient;
-
-	static {
-		okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)
-				.readTimeout(20, TimeUnit.SECONDS).build();
-	}
-
-	public static OkHttpClient getOkHttpClient() {
-		return okHttpClient;
-	}
-
-	/**
-	 * 发送请求 (带json请求体)
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @param headers
-	 * @param jsonBody
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
-								String jsonBody) {
-		return call(httpMethod, url, headers, new JsonBodyBuilder(jsonBody));
-	}
-
-	/**
-	 * 发送请求 (带请求体)
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @param headers
-	 * @param requestBodyBuilder
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
-								RequestBodyBuilder requestBodyBuilder) {
-
-		LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
-		LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
-		LOG.info("[okhttp3] body: " + requestBodyBuilder);
-
-		Builder builder = null;
-		if (httpMethod.equals(HttpMethod.GET)) {
-			builder = new Request.Builder().url(url).get();
-		} else if (httpMethod.equals(HttpMethod.POST)) {
-			builder = new Request.Builder().url(url).post(requestBodyBuilder.build());
-		} else if (httpMethod.equals(HttpMethod.PUT)) {
-			builder = new Request.Builder().url(url).put(requestBodyBuilder.build());
-		} else if (httpMethod.equals(HttpMethod.DELETE)) {
-			builder = new Request.Builder().url(url).delete(requestBodyBuilder.build());
-		}
-
-		if (null != headers && 0 != headers.size()) {
-			for (Entry<String, String> entry : headers.entrySet()) {
-				builder.addHeader(entry.getKey(), entry.getValue());
-			}
-		}
-
-		Request request = builder.build();
-
-		Response response = null;
-		try {
-			response = okHttpClient.newCall(request).execute();
-			return response;
-		} catch (IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	/**
-	 * 发送请求
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url) {
-		return call(httpMethod, url, null);
-	}
-
-	/**
-	 * 发送请求
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @param headers
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers) {
-
-		LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
-		LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
-
-		Builder builder = null;
-		if (httpMethod.equals(HttpMethod.GET)) {
-			builder = new Request.Builder().url(url).get();
-		} else if (httpMethod.equals(HttpMethod.POST)) {
-			builder = new Request.Builder().url(url).post(new FormBody.Builder().build());
-		} else if (httpMethod.equals(HttpMethod.PUT)) {
-			builder = new Request.Builder().url(url).put(new FormBody.Builder().build());
-		} else if (httpMethod.equals(HttpMethod.DELETE)) {
-			builder = new Request.Builder().url(url).delete(new FormBody.Builder().build());
-		}
-
-		if (null != headers && 0 != headers.size()) {
-			for (Entry<String, String> entry : headers.entrySet()) {
-				builder.addHeader(entry.getKey(), entry.getValue());
-			}
-		}
-
-		Request request = builder.build();
-
-		Response response = null;
-		try {
-			response = okHttpClient.newCall(request).execute();
-			return response;
-		} catch (IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	/**
-	 * 发送请求 (表单)
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @param headers
-	 * @param params
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
-								Map<String, String> params) {
-
-		LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
-		LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
-		LOG.info("[okhttp3] params: " + JsonUtil.toJson(params));
-
-		Builder builder = null;
-		if (httpMethod.equals(HttpMethod.GET)) {
-			url = UrlUtil.joinParams(url, params);
-			builder = new Request.Builder().url(url).get();
-		} else {
-			okhttp3.FormBody.Builder formBody = new FormBody.Builder();
-			if (null != params && 0 != params.size()) {
-				for (Entry<String, String> entry : params.entrySet()) {
-					formBody.add(entry.getKey(), entry.getValue());
-				}
-			}
-			if (httpMethod.equals(HttpMethod.POST)) {
-				builder = new Request.Builder().url(url).post(formBody.build());
-			} else if (httpMethod.equals(HttpMethod.PUT)) {
-				builder = new Request.Builder().url(url).put(formBody.build());
-			} else if (httpMethod.equals(HttpMethod.DELETE)) {
-				builder = new Request.Builder().url(url).delete(formBody.build());
-			}
-		}
-
-		if (null != headers && 0 != headers.size()) {
-			for (Entry<String, String> entry : headers.entrySet()) {
-				builder.addHeader(entry.getKey(), entry.getValue());
-			}
-		}
-
-		Request request = builder.build();
-
-		Response response = null;
-		try {
-			response = okHttpClient.newCall(request).execute();
-			return response;
-		} catch (IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	/**
-	 * 发送请求 (包含文件表单)
-	 *
-	 * @author WANGWEI
-	 * @param httpMethod
-	 * @param url
-	 * @param headers
-	 * @param params
-	 * @param formFilePartList
-	 * @return
-	 */
-	public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
-								Map<String, String> params, List<FormFilePart> formFilePartList) {
-
-		LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
-		LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
-		LOG.info("[okhttp3] params: " + JsonUtil.toJson(params));
-
-		okhttp3.MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
-				.setType(MultipartBody.ALTERNATIVE);
-
-		if (null != params) {
-			for (Entry<String, String> entry : params.entrySet()) {
-				multipartBodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
-			}
-		}
-
-		if (CollectionUtils.isNotEmpty(formFilePartList)) {
-			MediaType type = MediaType.parse("application/octet-stream");
-			for (FormFilePart part : formFilePartList) {
-				RequestBody fileBody = RequestBody.create(type, part.getFile());
-				multipartBodyBuilder.addFormDataPart(part.getParamName(), part.getFilename(),
-						fileBody);
-			}
-		}
-
-		Builder builder = new Request.Builder().url(url).post(multipartBodyBuilder.build());
-		if (null != headers && 0 != headers.size()) {
-			for (Entry<String, String> entry : headers.entrySet()) {
-				builder.addHeader(entry.getKey(), entry.getValue());
-			}
-		}
-
-		Request request = builder.build();
-
-		Response response = null;
-		try {
-			response = okHttpClient.newCall(request).execute();
-			return response;
-		} catch (IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
+    private static final Logger LOG = LoggerFactory.getLogger(OKHttpUtil.class);
+
+    public static final class MediaTypes {
+
+        public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+    }
+
+    /**
+     * 请求体构建器
+     *
+     * @author WANGWEI
+     * @date 2019年4月10日
+     * @Copyright (c) 2018-2020 http://www.qmth.com.cn/ All Rights Reserved.
+     */
+    public static interface RequestBodyBuilder {
+
+        RequestBody build();
+
+    }
+
+    /**
+     * json请求体构建器
+     *
+     * @author WANGWEI
+     * @date 2019年4月10日
+     * @Copyright (c) 2018-2020 http://www.qmth.com.cn/ All Rights Reserved.
+     */
+    public static final class JsonBodyBuilder implements RequestBodyBuilder {
+
+        private String json;
+
+        public JsonBodyBuilder(String json) {
+            super();
+            this.json = json;
+        }
+
+        @Override
+        public RequestBody build() {
+            return RequestBody.create(MediaTypes.JSON, json);
+        }
+
+        @Override
+        public String toString() {
+            return json;
+        }
+
+    }
+
+    private static OkHttpClient okHttpClient;
+
+    static {
+        okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)
+                .readTimeout(20, TimeUnit.SECONDS).build();
+    }
+
+    public static OkHttpClient getOkHttpClient() {
+        return okHttpClient;
+    }
+
+    /**
+     * 发送请求 (带json请求体)
+     *
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param jsonBody
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+                                String jsonBody) {
+        return call(httpMethod, url, headers, new JsonBodyBuilder(jsonBody));
+    }
+
+    /**
+     * 发送请求 (带请求体)
+     *
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param requestBodyBuilder
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+                                RequestBodyBuilder requestBodyBuilder) {
+
+        LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
+        LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
+        LOG.info("[okhttp3] body: " + requestBodyBuilder);
+
+        Builder builder;
+        if (httpMethod.equals(HttpMethod.GET)) {
+            builder = new Request.Builder().url(url).get();
+        } else if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(requestBodyBuilder.build());
+        } else {
+            builder = new Request.Builder().url(url);
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求
+     *
+     * @param httpMethod
+     * @param url
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url) {
+        return call(httpMethod, url, null);
+    }
+
+    /**
+     * 发送请求
+     *
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers) {
+
+        LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
+        LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
+
+        Builder builder;
+        if (httpMethod.equals(HttpMethod.GET)) {
+            builder = new Request.Builder().url(url).get();
+        } else if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(new FormBody.Builder().build());
+        } else {
+            builder = new Request.Builder().url(url);
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (表单)
+     *
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+                                Map<String, String> params) {
+
+        LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
+        LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
+        LOG.info("[okhttp3] params: " + JsonUtil.toJson(params));
+
+        Builder builder;
+        if (httpMethod.equals(HttpMethod.GET)) {
+            url = UrlUtil.joinParams(url, params);
+            builder = new Request.Builder().url(url).get();
+        } else {
+            okhttp3.FormBody.Builder formBody = new FormBody.Builder();
+            if (null != params && 0 != params.size()) {
+                for (Entry<String, String> entry : params.entrySet()) {
+                    formBody.add(entry.getKey(), entry.getValue());
+                }
+            }
+            if (httpMethod.equals(HttpMethod.POST)) {
+                builder = new Request.Builder().url(url).post(formBody.build());
+            } else if (httpMethod.equals(HttpMethod.PUT)) {
+                builder = new Request.Builder().url(url).put(formBody.build());
+            } else if (httpMethod.equals(HttpMethod.DELETE)) {
+                builder = new Request.Builder().url(url).delete(formBody.build());
+            } else {
+                builder = new Request.Builder().url(url);
+            }
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (包含文件表单)
+     *
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @param formFilePartList
+     * @return
+     * @author WANGWEI
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+                                Map<String, String> params, List<FormFilePart> formFilePartList) {
+
+        LOG.info("[okhttp3] new call: " + httpMethod + " " + url);
+        LOG.info("[okhttp3] headers: " + JsonUtil.toJson(headers));
+        LOG.info("[okhttp3] params: " + JsonUtil.toJson(params));
+
+        okhttp3.MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
+                .setType(MultipartBody.ALTERNATIVE);
+
+        if (null != params) {
+            for (Entry<String, String> entry : params.entrySet()) {
+                multipartBodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(formFilePartList)) {
+            MediaType type = MediaType.parse("application/octet-stream");
+            for (FormFilePart part : formFilePartList) {
+                RequestBody fileBody = RequestBody.create(type, part.getFile());
+                multipartBodyBuilder.addFormDataPart(part.getParamName(), part.getFilename(),
+                        fileBody);
+            }
+        }
+
+        Builder builder = new Request.Builder().url(url).post(multipartBodyBuilder.build());
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 
 }

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

@@ -68,7 +68,7 @@ public class GeetestServiceImpl implements GeetestService {
         resp.setNew_captcha(true);
         resp.setGt(properties.getId());
         if (StringUtils.isEmpty(challenge) || "0".equals(challenge)) {
-            // 请求极验register接口失败,后续流程走宕机模式
+            LOG.info("请求极验register接口失败,后续流程走宕机模式!user_id = " + req.getUser_id());
             resp.setChallenge(UUID.randomUUID().toString().replaceAll("-", ""));
             resp.setSuccess(false);
         } else {
@@ -104,9 +104,12 @@ public class GeetestServiceImpl implements GeetestService {
         // 校验会话信息
         Boolean status = sessionManager.getSession(req.getUser_id());
         if (status == null) {
+            LOG.info("验证码已过期,请重试!user_id = " + req.getUser_id());
             return new ValidateResp(false, "验证码已过期,请重试!");
         }
+
         if (status == false) {
+            LOG.info("宕机通过!user_id = " + req.getUser_id());
             return new ValidateResp(true, "宕机通过");
         }
 

+ 23 - 3
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/Constants.java

@@ -18,17 +18,17 @@ public interface Constants {
     /**
      * 系统错误
      */
-    String OE_CODE_500 = "OE-000500";
+    String OE_CODE_500 = "000500";
 
     /**
      * 参数错误
      */
-    String OE_CODE_400 = "OE-000400";
+    String OE_CODE_400 = "000400";
 
     /**
      * 权限错误
      */
-    String OE_CODE_403 = "OE-000403";
+    String OE_CODE_403 = "000403";
 
     /**
      * 考试控制锁
@@ -133,6 +133,11 @@ public interface Constants {
     // 抓拍照片的又拍云id
     String CAPTURE_PHOTO_UPYUN_SITEID = "capturePhoto";
 
+    /**
+     * 网考考试资源文件siteId
+     */
+    String OE_SITEID = "oe";
+
     /**
      * 处理照片高优先级
      */
@@ -198,4 +203,19 @@ public interface Constants {
      */
     String ELECTRON_EXAM_SHELL = "electron-exam-shell";
 
+    /**
+     * 虚拟摄像头信息
+     */
+    String VM_CAMERA_WARN = "[{\"name\":\"虚拟摄像头信息超长!\",\"pid\":\"\",\"vid\":\"\"}]";
+
+    /**
+     * 虚拟摄像头信息 长度限制值
+     */
+    int VM_CAMERA_SIZE_LIMIT = 800;
+
+    /**
+     * 人脸活体检测时间随机范围,最大分钟数
+     */
+    int MAX_FACE_LIVE_VERIFY_MINUTE = 4;
+
 }

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/ExamProperties.java

@@ -56,6 +56,7 @@ public enum ExamProperties {
 	EXAM_CYCLE_TIME_RANGE("考试周期时间段设置"),
 	SHOW_UNDERTAKING("显示设置-显示考生承诺书"),
 	UNDERTAKING("显示设置-考生承诺书"),
+	MAX_SWITCH_SCREEN_COUNT("控制设置-切屏次数限制"),
 	;
 
 	private ExamProperties(String desc){

+ 14 - 6
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/enums/FaceBiopsyScheme.java

@@ -7,15 +7,24 @@ package cn.com.qmth.examcloud.support.enums;
  * @Version 1.0
  */
 public enum FaceBiopsyScheme {
+
     /**
      * FaceID活体检测方案(即旧活体检测方案)
      */
     FACE_ID("S1", "FaceID活体检测方案"),
+
+    /**
+     * Electron Client 自研活体检测方案
+     */
+    FACE_MOTION("S2", "自研活体检测方案"),
+
     /**
-     * 新活体检测方案(暂时无法给出具体命名,以后有需要再改动)
+     * C端活体检测方案
      */
-    NEW("S2", "新活体检测方案");
+    FACE_CLIENT("S3", "C端活体检测方案");
+
     private String code;
+
     private String desc;
 
     FaceBiopsyScheme(String code, String desc) {
@@ -25,17 +34,16 @@ public enum FaceBiopsyScheme {
 
     /**
      * 获取活检方案代码
-     * @return
      */
-    public String getCode(){
+    public String getCode() {
         return this.code;
     }
 
     /**
      * 获取活检方案描述
-     * @return
      */
-    public String getDesc(){
+    public String getDesc() {
         return this.desc;
     }
+
 }

+ 15 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java

@@ -228,6 +228,11 @@ public class ExamRecordData implements JsonSerializable {
      * 切屏次数
      */
     private Integer switchScreenCount;
+    
+    /**
+     * 是否超过切屏限制
+     */
+    private Boolean exceedMaxSwitchScreenCount;
 
     public Long getId() {
         return id;
@@ -625,4 +630,14 @@ public class ExamRecordData implements JsonSerializable {
                 ", switchScreenCount=" + switchScreenCount +
                 '}';
     }
+
+	public Boolean getExceedMaxSwitchScreenCount() {
+		return exceedMaxSwitchScreenCount;
+	}
+
+	public void setExceedMaxSwitchScreenCount(Boolean exceedMaxSwitchScreenCount) {
+		this.exceedMaxSwitchScreenCount = exceedMaxSwitchScreenCount;
+	}
+    
+    
 }

+ 29 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java

@@ -114,6 +114,16 @@ public class ExamingSession implements JsonSerializable {
      * 定点交卷的时间
      */
     private Date fixedSubmitTime;
+    
+    /**
+     * 是否计算切屏次数
+     */
+    private Boolean recordSwitchScreen;
+    
+    /**
+     * 限制切屏次数
+     */
+    private Integer maxSwitchScreenCount;
 
     /**
      * 构建key
@@ -286,4 +296,23 @@ public class ExamingSession implements JsonSerializable {
     public void setTimingEnd(boolean timingEnd) {
         this.timingEnd = timingEnd;
     }
+
+	public Integer getMaxSwitchScreenCount() {
+		return maxSwitchScreenCount;
+	}
+
+	public void setMaxSwitchScreenCount(Integer maxSwitchScreenCount) {
+		this.maxSwitchScreenCount = maxSwitchScreenCount;
+	}
+
+	public Boolean getRecordSwitchScreen() {
+		return recordSwitchScreen;
+	}
+
+	public void setRecordSwitchScreen(Boolean recordSwitchScreen) {
+		this.recordSwitchScreen = recordSwitchScreen;
+	}
+
+    
+    
 }

+ 251 - 250
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/excel/ExcelExportUtil.java

@@ -1,339 +1,340 @@
 package cn.com.qmth.examcloud.support.excel;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.UUID;
+import cn.com.qmth.examcloud.support.util.FileDisposeUtil;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.StringEscapeUtils;
-
-import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
-import cn.com.qmth.examcloud.commons.util.UUID;
-import cn.com.qmth.examcloud.support.util.FileDisposeUtil;
+import java.util.*;
 
 /*
  * excel导出工具
  */
 public class ExcelExportUtil {
-	
 
-	private static final String TEMP_FILE_EXP = "excelExport/";
-	
-	private static final String DEFALUT_CONTENT_TYPE = "application/vnd.ms-excel";
 
-	private static final String DEFALUT_EXT = ".xlsx";
+    private static final String TEMP_FILE_EXP = "excelExport/";
 
-	private static final String ZIP_SUFFIX = ".zip";
+    private static final String DEFALUT_CONTENT_TYPE = "application/vnd.ms-excel";
 
+    private static final String DEFALUT_EXT = ".xlsx";
 
-	public static void exportExcel(String fileName, Class<?> dataClass, List<?> dataset,
-			HttpServletResponse response) throws Exception {
-		File directory = new File(TEMP_FILE_EXP + File.separator + UUID.randomUUID());
-		ServletOutputStream outputStream = null;
-		InputStream in=null;
-		try {
-			response.setHeader("Content-Disposition",
-					"inline;filename=" + URLEncoder.encode(fileName, "UTF-8") + DEFALUT_EXT);
-			response.setContentType(DEFALUT_CONTENT_TYPE);
-			outputStream=response.getOutputStream();
-			File file=createExcelFile(UUID.randomUUID(), dataClass, dataset, directory);
-			in=new FileInputStream(file);
-			int len=0;
-			byte[] buffer=new byte[1024];
-			while((len=in.read(buffer))>0){
-				outputStream.write(buffer,0,len);
-			}
-		}finally {
-			if(in!=null)in.close();
-			if(outputStream!=null)outputStream.close();
-			FileUtils.deleteDirectory(directory);
-		}
-	}
-	
-	public static void exportExcel(Class<?> dataClass, List<?> dataset,OutputStream outputStream) throws Exception {
-		File directory = new File(TEMP_FILE_EXP + File.separator + UUID.randomUUID());
-		InputStream in=null;
-		try {
-			File file=createExcelFile(UUID.randomUUID(), dataClass, dataset, directory);
-			in=new FileInputStream(file);
-			int len=0;
-			byte[] buffer=new byte[1024];
-			while((len=in.read(buffer))>0){
-				outputStream.write(buffer,0,len);
-			}
-		}finally {
-			if(in!=null)in.close();
-			if(outputStream!=null)outputStream.close();
-			FileUtils.deleteDirectory(directory);
-		}
-	}
+    private static final String ZIP_SUFFIX = ".zip";
 
 
-	private static File createExcelFile(String fileName,Class<?> dataClass, List<?> dataset, File directory) throws Exception{
-		File excelTargetDir = new File(directory.getAbsolutePath() + "/excel/");
-		File exceloriginalZip=new File(directory.getAbsolutePath() + "/excel/exceloriginal.zip");
-		File excelfile=null;
-		try {
-			excelTargetDir.mkdirs();
-			exceloriginalZip.createNewFile();
-			InputStream is=ExcelExportUtil.class.getClass().getResourceAsStream("/exceloriginal/exceloriginal.zip");
-			FileUtils.copyInputStreamToFile(is, exceloriginalZip);
-			FileDisposeUtil.unZip(excelTargetDir, exceloriginalZip);
-			exceloriginalZip.delete();
-			dispose(excelTargetDir, dataClass,dataset);
-			File zipfile = new File(
-					excelTargetDir.getParentFile().getAbsolutePath() + File.separator + fileName + ZIP_SUFFIX);
-			FileDisposeUtil.createZip(excelTargetDir.getAbsolutePath(), zipfile.getAbsolutePath());
-			excelfile = new File(
-					excelTargetDir.getParentFile().getAbsolutePath() + File.separator + fileName + DEFALUT_EXT);
-			zipfile.renameTo(excelfile);
-		}finally {
-//			try {
-//				FileUtils.deleteDirectory(excelTargetDir);
-//			} catch (IOException e) {
-//			}
-		}
-		return excelfile;
+    public static void exportExcel(String fileName, Class<?> dataClass, List<?> dataset,
+                                   HttpServletResponse response) throws Exception {
+        File directory = new File(TEMP_FILE_EXP + File.separator + UUID.randomUUID());
+        ServletOutputStream outputStream = null;
+        InputStream in = null;
+        try {
+            response.setHeader("Content-Disposition",
+                    "inline;filename=" + URLEncoder.encode(fileName, "UTF-8") + DEFALUT_EXT);
+            response.setContentType(DEFALUT_CONTENT_TYPE);
+            outputStream = response.getOutputStream();
+            File file = createExcelFile(UUID.randomUUID(), dataClass, dataset, directory);
+            in = new FileInputStream(file);
+            int len = 0;
+            byte[] buffer = new byte[1024];
+            while ((len = in.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, len);
+            }
+        } finally {
+            if (in != null) in.close();
+            if (outputStream != null) outputStream.close();
+            FileUtils.deleteDirectory(directory);
+        }
+    }
 
-	}
+    public static void exportExcel(Class<?> dataClass, List<?> dataset, OutputStream outputStream) throws Exception {
+        File directory = new File(TEMP_FILE_EXP + File.separator + UUID.randomUUID());
+        InputStream in = null;
+        try {
+            File file = createExcelFile(UUID.randomUUID(), dataClass, dataset, directory);
+            in = new FileInputStream(file);
+            int len = 0;
+            byte[] buffer = new byte[1024];
+            while ((len = in.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, len);
+            }
+        } finally {
+            if (in != null) in.close();
+            if (outputStream != null) outputStream.close();
+            FileUtils.deleteDirectory(directory);
+        }
+    }
 
-	private static void dispose(File excelTargetDir, Class<?> dataClass, List<?> dataset)
-			throws IOException, NoSuchMethodException, SecurityException,
-			IllegalAccessException, IllegalArgumentException, InvocationTargetException {
-		//excel列名A B C AA AB
-		List<String> colName=new ArrayList<String>();
-		//单元格值
-		Map<String,Integer> vmap=new LinkedHashMap<String, Integer>();
-		int vmapSize=0;
-		ExcelSheet sheet=new ExcelSheet();
-		int index = 1;//第一行开始
-		//处理头
-		List<ColumnSetting> columnSettings = getColumnSettings(dataClass);
-		ExcelRow headrow=new ExcelRow();
-		headrow.setR(String.valueOf(index));
-		for(int i=0;i<columnSettings.size();i++) {
-			colName.add(getExcelColumnName(i));
-			ExcelCell cell=new ExcelCell();
-			cell.setR(colName.get(i)+index);
-			cell.setS("1");
-			Integer cellV=vmap.get(columnSettings.get(i).getHeader());
-			if(cellV==null) {
-				vmapSize++;
-				cellV=vmapSize;
-				vmap.put(columnSettings.get(i).getHeader(), cellV);
-			}
-			cell.setV(cellV.toString());
-			headrow.addCell(cell);
-		}
-		colName.add(getExcelColumnName(colName.size()));
-		sheet.addRow(headrow);
-		sheet.setDimension(colName.get(0)+1+":"+colName.get(colName.size()-1)+(dataset.size()+1));
-		File sheetFile=writeSheetHead(excelTargetDir, sheet, columnSettings);
-		//处理数据
-		for(int k=0;k<dataset.size();k++) {
-			index++;
-        	Object obj=dataset.get(k);
-            ExcelRow row=new ExcelRow();
+
+    private static File createExcelFile(String fileName, Class<?> dataClass, List<?> dataset, File directory) throws Exception {
+        File excelTargetDir = new File(directory.getAbsolutePath() + "/excel/");
+        File exceloriginalZip = new File(directory.getAbsolutePath() + "/excel/exceloriginal.zip");
+        File excelfile = null;
+        try {
+            excelTargetDir.mkdirs();
+            exceloriginalZip.createNewFile();
+            InputStream is = ExcelExportUtil.class.getClass().getResourceAsStream("/exceloriginal/exceloriginal.zip");
+            FileUtils.copyInputStreamToFile(is, exceloriginalZip);
+            FileDisposeUtil.unZip(excelTargetDir, exceloriginalZip);
+            exceloriginalZip.delete();
+            dispose(excelTargetDir, dataClass, dataset);
+            File zipfile = new File(
+                    excelTargetDir.getParentFile().getAbsolutePath() + File.separator + fileName + ZIP_SUFFIX);
+            FileDisposeUtil.createZip(excelTargetDir.getAbsolutePath(), zipfile.getAbsolutePath());
+            excelfile = new File(
+                    excelTargetDir.getParentFile().getAbsolutePath() + File.separator + fileName + DEFALUT_EXT);
+            zipfile.renameTo(excelfile);
+        } finally {
+            //			try {
+            //				FileUtils.deleteDirectory(excelTargetDir);
+            //			} catch (IOException e) {
+            //			}
+        }
+        return excelfile;
+
+    }
+
+    private static void dispose(File excelTargetDir, Class<?> dataClass, List<?> dataset)
+            throws IOException, NoSuchMethodException, SecurityException,
+            IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        //excel列名A B C AA AB
+        List<String> colName = new ArrayList<String>();
+        //单元格值
+        Map<String, Integer> vmap = new LinkedHashMap<String, Integer>();
+        int vmapSize = 0;
+        ExcelSheet sheet = new ExcelSheet();
+        int index = 1;//第一行开始
+        //处理头
+        List<ColumnSetting> columnSettings = getColumnSettings(dataClass);
+        ExcelRow headrow = new ExcelRow();
+        headrow.setR(String.valueOf(index));
+        for (int i = 0; i < columnSettings.size(); i++) {
+            colName.add(getExcelColumnName(i));
+            ExcelCell cell = new ExcelCell();
+            cell.setR(colName.get(i) + index);
+            cell.setS("1");
+            Integer cellV = vmap.get(columnSettings.get(i).getHeader());
+            if (cellV == null) {
+                vmapSize++;
+                cellV = vmapSize;
+                vmap.put(columnSettings.get(i).getHeader(), cellV);
+            }
+            cell.setV(cellV.toString());
+            headrow.addCell(cell);
+        }
+        colName.add(getExcelColumnName(colName.size()));
+        sheet.addRow(headrow);
+        sheet.setDimension(colName.get(0) + 1 + ":" + colName.get(colName.size() - 1) + (dataset.size() + 1));
+        File sheetFile = writeSheetHead(excelTargetDir, sheet, columnSettings);
+        //处理数据
+        for (int k = 0; k < dataset.size(); k++) {
+            index++;
+            Object obj = dataset.get(k);
+            ExcelRow row = new ExcelRow();
             row.setR(String.valueOf(index));
             // 利用反射,根据javabean属性的先后顺序,动态调用getXxx()方法得到属性值
             for (short i = 0; i < columnSettings.size(); i++) {
                 String methodName = columnSettings.get(i).getGetMethodName();
                 Method method = dataClass.getMethod(methodName, new Class[]{});
-                Object value = method.invoke(obj, new Object[] {});
-                ExcelCell cell=new ExcelCell();
-                cell.setR(colName.get(i)+index);
+                Object value = method.invoke(obj, new Object[]{});
+                ExcelCell cell = new ExcelCell();
+                cell.setR(colName.get(i) + index);
                 cell.setS("0");
-                if(value!=null) {
-                	String encodeValue=StringEscapeUtils.escapeXml(value.toString());
-	    			Integer cellV=vmap.get(encodeValue);
-	    			if(cellV==null) {
-	    				vmapSize++;
-	    				cellV=vmapSize;
-	    				vmap.put(encodeValue, cellV);
-	    			}
-	    			cell.setV(cellV.toString());
-                }else {
-                	cell.setV(String.valueOf(0));
+                if (value != null) {
+                    String encodeValue = StringEscapeUtils.escapeXml(value.toString());
+                    Integer cellV = vmap.get(encodeValue);
+                    if (cellV == null) {
+                        vmapSize++;
+                        cellV = vmapSize;
+                        vmap.put(encodeValue, cellV);
+                    }
+                    cell.setV(cellV.toString());
+                } else {
+                    cell.setV(String.valueOf(0));
                 }
                 row.addCell(cell);
             }
             sheet.addRow(row);
             //早点回收内存
             dataset.set(k, null);
-            if(sheet.getRowCount()>=10000) {
-            	writeSheetBody(sheetFile, sheet);
-            	sheet.clearRows();
+            if (sheet.getRowCount() >= 10000) {
+                writeSheetBody(sheetFile, sheet);
+                sheet.clearRows();
             }
         }
-		if(sheet.getRowCount()>0) {
-        	writeSheetBody(sheetFile, sheet);
-        	sheet.clearRows();
+        if (sheet.getRowCount() > 0) {
+            writeSheetBody(sheetFile, sheet);
+            sheet.clearRows();
         }
-		writeSheetTail(sheetFile);
-        
-        writeSharedStrings(excelTargetDir, vmap,index*(colName.size()-1));
-        vmap=null;
-	}
+        writeSheetTail(sheetFile);
+
+        writeSharedStrings(excelTargetDir, vmap, index * (colName.size() - 1));
+        vmap = null;
+    }
 
-	private static void writeSharedStrings(File excelTargetDir, Map<String,Integer> vmap,Integer count) throws IOException {
-		File file = new File(excelTargetDir.getAbsolutePath() + "/xl/sharedStrings.xml");
-		FileOutputStream outputStream = null;
+    private static void writeSharedStrings(File excelTargetDir, Map<String, Integer> vmap, Integer count) throws IOException {
+        File file = new File(excelTargetDir.getAbsolutePath() + "/xl/sharedStrings.xml");
+        FileOutputStream outputStream = null;
         try {
             file.createNewFile();//创建文件
-            outputStream = new FileOutputStream(file,true);
+            outputStream = new FileOutputStream(file, true);
             String data1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
             outputStream.write(data1.getBytes("utf-8"));
-            String data2 = "<sst count=\""+count+"\" uniqueCount=\""+(vmap.size()+1)+"\"\n";
+            String data2 = "<sst count=\"" + count + "\" uniqueCount=\"" + (vmap.size() + 1) + "\"\n";
             outputStream.write(data2.getBytes("utf-8"));
-            String data3="xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><si><t/></si>\n";
+            String data3 = "xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"><si><t/></si>\n";
             outputStream.write(data3.getBytes("utf-8"));
-            for(String k:vmap.keySet()) {
-            	String data4="<si><t>"+k+"</t></si>\n";
-            	outputStream.write(data4.getBytes("utf-8"));
+            for (String k : vmap.keySet()) {
+                String data4 = "<si><t>" + k + "</t></si>\n";
+                outputStream.write(data4.getBytes("utf-8"));
             }
-            String data5="</sst>";
-        	outputStream.write(data5.getBytes("utf-8"));
-        	outputStream.flush();
+            String data5 = "</sst>";
+            outputStream.write(data5.getBytes("utf-8"));
+            outputStream.flush();
         } catch (Exception e) {
-        	throw new ExamCloudRuntimeException(e);
+            throw new ExamCloudRuntimeException(e);
         } finally {
-            outputStream.close();
+            if (outputStream != null) {
+                outputStream.close();
+            }
         }
-	}
-	private static File writeSheetHead(File excelTargetDir, ExcelSheet sheet,List<ColumnSetting> columnSettings) throws IOException {
-		File file = new File(excelTargetDir.getAbsolutePath() + "/xl/worksheets/sheet1.xml");
-		FileOutputStream outputStream = null;
+    }
+
+    private static File writeSheetHead(File excelTargetDir, ExcelSheet sheet, List<ColumnSetting> columnSettings) throws IOException {
+        File file = new File(excelTargetDir.getAbsolutePath() + "/xl/worksheets/sheet1.xml");
+        FileOutputStream outputStream = null;
         try {
-            outputStream = new FileOutputStream(file,true);
+            outputStream = new FileOutputStream(file, true);
             String data1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
             outputStream.write(data1.getBytes("utf-8"));
             String data2 = "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n";
             outputStream.write(data2.getBytes("utf-8"));
-            String data3="<dimension ref=\""+sheet.getDimension()+"\"/>\n";
+            String data3 = "<dimension ref=\"" + sheet.getDimension() + "\"/>\n";
             outputStream.write(data3.getBytes("utf-8"));
-            String data4="<sheetViews><sheetView workbookViewId=\"0\" tabSelected=\"true\"/></sheetViews><sheetFormatPr defaultRowHeight=\"15.0\" baseColWidth=\"15\"/><cols>\n";
+            String data4 = "<sheetViews><sheetView workbookViewId=\"0\" tabSelected=\"true\"/></sheetViews><sheetFormatPr defaultRowHeight=\"15.0\" baseColWidth=\"15\"/><cols>\n";
             outputStream.write(data4.getBytes("utf-8"));
-            int index=0;
-            for(ColumnSetting col:columnSettings) {
-            	index++;
-            	if(col.getWidth()!=0) {
-            		String data10 = "<col min=\""+index+"\" max=\""+index+"\" width=\""+col.getWidth()+"\" customWidth=\"true\"/>\n";
+            int index = 0;
+            for (ColumnSetting col : columnSettings) {
+                index++;
+                if (col.getWidth() != 0) {
+                    String data10 = "<col min=\"" + index + "\" max=\"" + index + "\" width=\"" + col.getWidth() + "\" customWidth=\"true\"/>\n";
+                    outputStream.write(data10.getBytes("utf-8"));
+                } else {
+                    String data10 = "<col min=\"" + index + "\" max=\"" + index + "\" width=\"" + 15 + "\" customWidth=\"true\"/>\n";
                     outputStream.write(data10.getBytes("utf-8"));
-            	}else {
-            		String data10 = "<col min=\""+index+"\" max=\""+index+"\" width=\""+15+"\" customWidth=\"true\"/>\n";
-	                outputStream.write(data10.getBytes("utf-8"));
-            	}
+                }
             }
             String data11 = "</cols><sheetData>\n";
             outputStream.write(data11.getBytes("utf-8"));
-        	outputStream.flush();
-        	return file;
+            outputStream.flush();
+            return file;
         } catch (Exception e) {
-        	throw new ExamCloudRuntimeException(e);
+            throw new ExamCloudRuntimeException(e);
         } finally {
-            outputStream.close();
+            if (outputStream != null) {
+                outputStream.close();
+            }
         }
-	}
-	private static void writeSheetBody(File file, ExcelSheet sheet) throws IOException {
-		FileOutputStream outputStream = null;
+    }
+
+    private static void writeSheetBody(File file, ExcelSheet sheet) throws IOException {
+        FileOutputStream outputStream = null;
         try {
-            outputStream = new FileOutputStream(file,true);
-            for(ExcelRow row:sheet.getRows()) {
-            	String data5="<row r=\""+row.getR()+"\">\n";
-            	outputStream.write(data5.getBytes("utf-8"));
-	            for(ExcelCell cell:row.getCells()) {
-	            	String data6="<c r=\""+cell.getR()+"\" s=\""+cell.getS()+"\" t=\"s\"><v>"+cell.getV()+"</v></c>\n";
-	            	outputStream.write(data6.getBytes("utf-8"));
-	            }
-	            String data7="</row>\n";
-            	outputStream.write(data7.getBytes("utf-8"));
-            	row=null;
+            outputStream = new FileOutputStream(file, true);
+            for (ExcelRow row : sheet.getRows()) {
+                String data5 = "<row r=\"" + row.getR() + "\">\n";
+                outputStream.write(data5.getBytes("utf-8"));
+                for (ExcelCell cell : row.getCells()) {
+                    String data6 = "<c r=\"" + cell.getR() + "\" s=\"" + cell.getS() + "\" t=\"s\"><v>" + cell.getV() + "</v></c>\n";
+                    outputStream.write(data6.getBytes("utf-8"));
+                }
+                String data7 = "</row>\n";
+                outputStream.write(data7.getBytes("utf-8"));
+                row = null;
             }
-        	outputStream.flush();
+            outputStream.flush();
         } catch (Exception e) {
-        	throw new ExamCloudRuntimeException(e);
+            throw new ExamCloudRuntimeException(e);
         } finally {
-            outputStream.close();
+            if (outputStream != null) {
+                outputStream.close();
+            }
         }
-	}
-	
-	private static void writeSheetTail(File file) throws IOException {
-		FileOutputStream outputStream = null;
+    }
+
+    private static void writeSheetTail(File file) throws IOException {
+        FileOutputStream outputStream = null;
         try {
-            outputStream = new FileOutputStream(file,true);
-            String data7="</sheetData><pageMargins bottom=\"0.75\" footer=\"0.3\" header=\"0.3\" left=\"0.7\" right=\"0.7\" top=\"0.75\"/></worksheet>";
-        	outputStream.write(data7.getBytes("utf-8"));
-        	outputStream.flush();
+            outputStream = new FileOutputStream(file, true);
+            String data7 = "</sheetData><pageMargins bottom=\"0.75\" footer=\"0.3\" header=\"0.3\" left=\"0.7\" right=\"0.7\" top=\"0.75\"/></worksheet>";
+            outputStream.write(data7.getBytes("utf-8"));
+            outputStream.flush();
         } catch (Exception e) {
-        	throw new ExamCloudRuntimeException(e);
+            throw new ExamCloudRuntimeException(e);
         } finally {
-            outputStream.close();
+            if (outputStream != null) {
+                outputStream.close();
+            }
         }
-	}
-	
-	 /**
+    }
+
+    /**
      * 提取ExcelProperty注解类的字段信息
+     *
      * @param dataClass 需要解析 写入excel的数据类型
      * @return
      */
-    private static List<ColumnSetting> getColumnSettings(Class<?> dataClass){
+    private static List<ColumnSetting> getColumnSettings(Class<?> dataClass) {
         List<ColumnSetting> columnSettings = new ArrayList<>();
         //先在方法上找ExcelProperty注解
         Method[] methods = dataClass.getDeclaredMethods();
-        for(Method method : methods){
+        for (Method method : methods) {
             ExcelProperty exportProperty = method.getAnnotation(ExcelProperty.class);
-            if(exportProperty != null && exportProperty.name().trim().length() > 0){
-                ColumnSetting columnSetting = new ColumnSetting(StringEscapeUtils.escapeXml(exportProperty.name()),method.getName(),
-                        exportProperty.width(),exportProperty.index());
+            if (exportProperty != null && exportProperty.name().trim().length() > 0) {
+                ColumnSetting columnSetting = new ColumnSetting(StringEscapeUtils.escapeXml(exportProperty.name()), method.getName(),
+                        exportProperty.width(), exportProperty.index());
                 columnSettings.add(columnSetting);
             }
         }
         //如果方法上找不到注解,再到属性上找 
-        if(columnSettings.size() == 0){
-        	Field[] fields = dataClass.getDeclaredFields();
-        	for(Field field:fields){
-        		ExcelProperty exportProperty = field.getAnnotation(ExcelProperty.class);
-        		if(exportProperty != null && exportProperty.name().trim().length() > 0){
-                    ColumnSetting columnSetting = new ColumnSetting(StringEscapeUtils.escapeXml(exportProperty.name()),"get"+toUpperCaseFirstOne(field.getName()),
-                            exportProperty.width(),exportProperty.index());
+        if (columnSettings.size() == 0) {
+            Field[] fields = dataClass.getDeclaredFields();
+            for (Field field : fields) {
+                ExcelProperty exportProperty = field.getAnnotation(ExcelProperty.class);
+                if (exportProperty != null && exportProperty.name().trim().length() > 0) {
+                    ColumnSetting columnSetting = new ColumnSetting(StringEscapeUtils.escapeXml(exportProperty.name()), "get" + toUpperCaseFirstOne(field.getName()),
+                            exportProperty.width(), exportProperty.index());
                     columnSettings.add(columnSetting);
                 }
-        	}
+            }
         }
         Collections.sort(columnSettings);
         return columnSettings;
     }
-    
-    private static String toUpperCaseFirstOne(String s){
-	  if(Character.isUpperCase(s.charAt(0)))
-	    return s;
-	  else
-	    return (new StringBuilder()).append(Character.toUpperCase(s.charAt(0))).append(s.substring(1)).toString();
-	}
-    
+
+    private static String toUpperCaseFirstOne(String s) {
+        if (Character.isUpperCase(s.charAt(0)))
+            return s;
+        else
+            return (new StringBuilder()).append(Character.toUpperCase(s.charAt(0))).append(s.substring(1)).toString();
+    }
+
     private static String getExcelColumnName(int celNum) {
-		int num = celNum+1;//celNum是从0算起
-		String tem = "";
-		while(num > 0) {
-			int lo = (num - 1) % 26;//取余,A到Z是26进制,
-			tem = (char)(lo + 'A') + tem;
-			num = (num - 1) / 26;//取模
-		}
-		return tem;
-	}
+        int num = celNum + 1;//celNum是从0算起
+        String tem = "";
+        while (num > 0) {
+            int lo = (num - 1) % 26;//取余,A到Z是26进制,
+            tem = (char) (lo + 'A') + tem;
+            num = (num - 1) / 26;//取模
+        }
+        return tem;
+    }
+
 }

+ 22 - 1
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java

@@ -69,12 +69,17 @@ public class FileStorageUtil {
     public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5) {
         return saveFile(siteId, env, file, md5, false);
     }
+    
+    public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5,Long cacheAge) {
+        FileStorageType fsType = getFileStorageType();
+        return saveFile(siteId, env, file, fsType, md5, false,cacheAge);
+    }
 
     public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5, boolean refreshCDN) {
         FileStorageType fsType = getFileStorageType();
         return saveFile(siteId, env, file, fsType, md5, refreshCDN);
     }
-
+    
     /**
      * 保存文件
      *
@@ -155,6 +160,22 @@ public class FileStorageUtil {
         FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
         return fs.saveFile(siteId, env, file, md5, refreshCDN);
     }
+    
+    private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, FileStorageType fsType,
+            String md5, boolean refreshCDN,Long cacheAge) {
+		if (siteId == null) {
+		throw new StatusException("2000", "siteId是空");
+		}
+		if (file == null) {
+		throw new StatusException("2001", "文件是空");
+		}
+		if (env == null) {
+		throw new StatusException("2002", "文件上传路径信息是空");
+		}
+		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+		return fs.saveFile(siteId, env, file, md5, refreshCDN,cacheAge);
+	}
 
     /**
      * 获取文件访问路径

+ 62 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/filestorage/UploadResult.java

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.support.filestorage;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+/**
+ * 文件上传结果
+ */
+public class UploadResult implements Serializable {
+
+    private static final long serialVersionUID = -4133740797472293134L;
+
+    @ApiModelProperty(value = "文件名")
+    private String fileName;
+
+    @ApiModelProperty(value = "文件存储地址")
+    private String filePath;
+
+    @ApiModelProperty(value = "文件访问地址")
+    private String fileUrl;
+
+    public UploadResult(String fileName, String filePath, String fileUrl) {
+        this.fileName = fileName;
+        this.filePath = filePath;
+        this.fileUrl = fileUrl;
+    }
+
+    public UploadResult(String filePath, String fileUrl) {
+        this.filePath = filePath;
+        this.fileUrl = fileUrl;
+    }
+
+    public UploadResult() {
+
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getFilePath() {
+        return filePath;
+    }
+
+    public void setFilePath(String filePath) {
+        this.filePath = filePath;
+    }
+
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    public void setFileUrl(String fileUrl) {
+        this.fileUrl = fileUrl;
+    }
+
+}

+ 60 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/QuestionBodyHandler.java

@@ -0,0 +1,60 @@
+package cn.com.qmth.examcloud.support.handler;
+
+import cn.com.qmth.examcloud.commons.util.JsonMapper;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestion;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionOption;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionUnit;
+import cn.com.qmth.examcloud.support.handler.richtext2.RichTextConverter;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+
+public class QuestionBodyHandler {
+
+    public static void convertRichText(DefaultQuestion defaultQuestion) {
+        if (defaultQuestion == null) {
+            return;
+        }
+
+        DefaultQuestionStructure structure = defaultQuestion.getMasterVersion();
+        if (structure == null) {
+            return;
+        }
+
+        JsonMapper jsonMapper = JsonMapper.nonNullMapper();
+
+        if (StringUtils.isNotEmpty(structure.getBody())) {
+            structure.setBody(jsonMapper.toJson(RichTextConverter.parse(structure.getBody())));
+        }
+
+        List<DefaultQuestionUnit> questionUnits = structure.getQuestionUnitList();
+        if (CollectionUtils.isEmpty(questionUnits)) {
+            return;
+        }
+
+        for (DefaultQuestionUnit questionUnit : questionUnits) {
+            // 处理题干
+            questionUnit.setBody(jsonMapper.toJson(RichTextConverter.parse(questionUnit.getBody())));
+
+            List<DefaultQuestionOption> questionOptions = questionUnit.getQuestionOptionList();
+            if (CollectionUtils.isNotEmpty(questionOptions)) {
+                for (DefaultQuestionOption questionOption : questionOptions) {
+                    // 处理选项
+                    questionOption.setBody(jsonMapper.toJson(RichTextConverter.parse(questionOption.getBody())));
+                }
+            }
+
+            String[] rightAnswers = questionUnit.getRightAnswer();
+            if (ArrayUtils.isNotEmpty(rightAnswers)) {
+                for (int n = 0; n < rightAnswers.length; n++) {
+                    // 处理正确答案
+                    rightAnswers[n] = jsonMapper.toJson(RichTextConverter.parse(rightAnswers[n]));
+                }
+            }
+        }
+    }
+
+}

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/AudioTextHandler.java

@@ -17,6 +17,7 @@ import java.util.List;
  * @Date 2020/5/15 16:29
  * @Version 1.0
  */
+@Deprecated
 public class AudioTextHandler implements RichTextHandler{
     private static final Logger LOG = LoggerFactory.getLogger(AudioTextHandler.class);
 

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java

@@ -24,6 +24,7 @@ import java.util.Map;
  * @Date 2020/5/15 16:29
  * @Version 1.0
  */
+@Deprecated
 public class ComplexTextHandler implements RichTextHandler {
     private static final Logger LOG = LoggerFactory.getLogger(HtmlTextHandler.class);
 

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/HtmlTextHandler.java

@@ -24,6 +24,7 @@ import java.util.Map;
  * @Date 2020/5/15 16:28
  * @Version 1.0
  */
+@Deprecated
 public class HtmlTextHandler implements RichTextHandler {
 
     private static final Logger LOG = LoggerFactory.getLogger(HtmlTextHandler.class);

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/ImageTextHandler.java

@@ -26,6 +26,7 @@ import java.util.Map;
  * @Date 2020/5/15 16:28
  * @Version 1.0
  */
+@Deprecated
 public class ImageTextHandler implements RichTextHandler {
 
     private static final Logger LOG = LoggerFactory.getLogger(ImageTextHandler.class);

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandler.java

@@ -8,6 +8,7 @@ import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean
  * @Date 2020/5/15 17:00
  * @Version 1.0
  */
+@Deprecated
 public interface RichTextHandler {
     SectionCollectionBean handle(String richText);
 }

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandlerFactory.java

@@ -12,6 +12,7 @@ import java.util.Map;
  * @Date 2020/5/15 16:33
  * @Version 1.0
  */
+@Deprecated
 public class RichTextHandlerFactory {
     private static final Map<String,RichTextHandler> handlerMap=new HashMap<>();
 

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/BlockBean.java

@@ -9,6 +9,7 @@ import java.util.Map;
  * @Date 2020/3/2 18:35
  * @Version 1.0
  */
+@Deprecated
 public class BlockBean {
     public BlockBean(String type) {
         this.type = type;

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionBean.java

@@ -9,6 +9,7 @@ import java.util.List;
  * @Date 2020/3/30 17:00
  * @Version 1.0
  */
+@Deprecated
 public class SectionBean {
 
     public SectionBean() {

+ 1 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionCollectionBean.java

@@ -8,6 +8,7 @@ import java.util.List;
  * @Date 2020/3/30 16:59
  * @Version 1.0
  */
+@Deprecated
 public class SectionCollectionBean {
 
     private List<SectionBean> sections;

+ 100 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Block.java

@@ -0,0 +1,100 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+
+/**
+ * 内容块
+ */
+public class Block implements Serializable {
+
+    private static final long serialVersionUID = -8820818435363871114L;
+
+    private BlockType type;// 块类型
+
+    private String value;// 块内容
+
+    private Param param;// 样式参数
+
+    private Control control;// 控制参数
+
+    private Integer playTime;// 播放次数
+
+    private String ext1;
+
+    private String ext2;
+
+    private String ext3;
+
+    public BlockType getType() {
+        return type;
+    }
+
+    public void setType(BlockType type) {
+        this.type = type;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public Param getParam() {
+        return param;
+    }
+
+    public void setParam(Param param) {
+        this.param = param;
+    }
+
+    public Control getControl() {
+        return control;
+    }
+
+    public void setControl(Control control) {
+        this.control = control;
+    }
+
+    public Integer getPlayTime() {
+        return playTime;
+    }
+
+    public void setPlayTime(Integer playTime) {
+        this.playTime = playTime;
+    }
+
+    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;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 28 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/BlockType.java

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+/**
+ * 内容块-类型
+ */
+public enum BlockType {
+
+    text("文本"),
+
+    image("图片"),
+
+    audio("音频"),
+
+    video("视频"),
+
+    link("链接");
+
+    private String description;
+
+    BlockType(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+}

+ 40 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Control.java

@@ -0,0 +1,40 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+
+/**
+ * 控制参数
+ */
+public class Control implements Serializable {
+
+    private static final long serialVersionUID = -8820818435363871114L;
+
+    private Integer maxPlayCount;//最大播放次数
+
+    private Integer fixedPlayInterval;//固定播放间隔,单位为秒
+
+    public Integer getMaxPlayCount() {
+        return maxPlayCount;
+    }
+
+    public void setMaxPlayCount(Integer maxPlayCount) {
+        this.maxPlayCount = maxPlayCount;
+    }
+
+    public Integer getFixedPlayInterval() {
+        return fixedPlayInterval;
+    }
+
+    public void setFixedPlayInterval(Integer fixedPlayInterval) {
+        this.fixedPlayInterval = fixedPlayInterval;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 100 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Param.java

@@ -0,0 +1,100 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+
+/**
+ * 样式参数
+ */
+public class Param implements Serializable {
+
+    private static final long serialVersionUID = -8820818435363871114L;
+
+    private String width;//宽
+
+    private String height;//高
+
+    private Boolean bold;//加粗
+
+    private Boolean underline;//下划线
+
+    private Boolean italic;//斜体
+
+    private Boolean sup;//上标
+
+    private Boolean sub;//下标
+
+    private Integer duration;//时长,单位为秒
+
+    public String getWidth() {
+        return width;
+    }
+
+    public void setWidth(String width) {
+        this.width = width;
+    }
+
+    public String getHeight() {
+        return height;
+    }
+
+    public void setHeight(String height) {
+        this.height = height;
+    }
+
+    public Boolean getBold() {
+        return bold;
+    }
+
+    public void setBold(Boolean bold) {
+        this.bold = bold;
+    }
+
+    public Boolean getUnderline() {
+        return underline;
+    }
+
+    public void setUnderline(Boolean underline) {
+        this.underline = underline;
+    }
+
+    public Boolean getItalic() {
+        return italic;
+    }
+
+    public void setItalic(Boolean italic) {
+        this.italic = italic;
+    }
+
+    public Boolean getSup() {
+        return sup;
+    }
+
+    public void setSup(Boolean sup) {
+        this.sup = sup;
+    }
+
+    public Boolean getSub() {
+        return sub;
+    }
+
+    public void setSub(Boolean sub) {
+        this.sub = sub;
+    }
+
+    public Integer getDuration() {
+        return duration;
+    }
+
+    public void setDuration(Integer duration) {
+        this.duration = duration;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 42 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Result.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Result implements Serializable {
+
+    private static final long serialVersionUID = -8820818435363871114L;
+
+    private List<Section> sections;
+
+    public void addSection(Section section) {
+        if (section == null) {
+            return;
+        }
+        if (sections == null) {
+            sections = new ArrayList<>();
+        }
+        sections.add(section);
+    }
+
+    public List<Section> getSections() {
+        if (sections == null) {
+            sections = new ArrayList<>();
+        }
+        return sections;
+    }
+
+    public void setSections(List<Section> sections) {
+        this.sections = sections;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 284 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/RichTextConverter.java

@@ -0,0 +1,284 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML结构转换为“富文本”JSON结构
+ * 注:特殊定制,只处理业务所需的标签和结构,如:试题题干内容
+ *
+ * @author: Deason
+ * @since: 2021/9/10
+ */
+public class RichTextConverter implements TagConstant {
+
+    private final static Logger log = LoggerFactory.getLogger(RichTextConverter.class);
+
+    public static Result parse(String html) {
+        if (StringUtils.isEmpty(html)) {
+            return new Result();
+        }
+
+        Document document = Jsoup.parse(html);
+        return parse(document);
+    }
+
+    public static Result parse(Document document) {
+        Element body = document.body();
+        List<Node> nodes = body.childNodes();
+
+        Result result = new Result();
+        result.setSections(splitSections(nodes));
+        return result;
+    }
+
+    private static List<Section> splitSections(List<Node> nodes) {
+        if (CollectionUtils.isEmpty(nodes)) {
+            return new ArrayList<>();
+        }
+
+        List<List<Node>> groups = new ArrayList<>();
+
+        if (nodes.size() == 1) {
+            groups.add(nodes);
+        } else {
+            List<Node> tempNodes = new ArrayList<>();
+
+            for (int n = 0; n < nodes.size(); n++) {
+                Node node = nodes.get(n);
+
+                if (TAG_P.equals(node.nodeName())) {
+                    if (!CollectionUtils.isEmpty(tempNodes)) {
+                        // 先将“当前p标签”之前的元素作为一组
+                        groups.add(new ArrayList<>(tempNodes));
+                        tempNodes.clear();
+                    }
+
+                    // 再将“当前p标签”的元素作为一组
+                    tempNodes.add(node);
+                    groups.add(new ArrayList<>(tempNodes));
+                    tempNodes.clear();
+                } else {
+                    // 非“p标签”元素
+                    tempNodes.add(node);
+                }
+
+                if (n == (nodes.size() - 1) && !CollectionUtils.isEmpty(tempNodes)) {
+                    // 最后一个元素 且 非“p标签”元素
+                    groups.add(tempNodes);
+                }
+            }
+        }
+
+        List<Section> sections = new ArrayList<>();
+        for (List<Node> curNodes : groups) {
+            List<Block> blocks = new ArrayList<>();
+            splitBlocks(curNodes, blocks);
+            if (blocks.isEmpty()) {
+                continue;
+            }
+
+            Section section = new Section();
+            section.setBlocks(blocks);
+            sections.add(section);
+        }
+        groups.clear();
+
+        return sections;
+    }
+
+    private static void splitBlocks(List<Node> nodes, List<Block> blocks) {
+        if (nodes.isEmpty()) {
+            return;
+        }
+
+        for (Node node : nodes) {
+            if (node instanceof Element) {
+                Element element = (Element) node;
+
+                if (TAG_IMG.equals(node.nodeName())) {
+                    // 处理图片
+                    processImg(element, blocks);
+                } else if (TAG_A.equals(node.nodeName())) {
+                    // 处理音频
+                    processAudio(element, blocks);
+
+                    if (!node.childNodes().isEmpty()) {
+                        splitBlocks(node.childNodes(), blocks);
+                    }
+                } else if (TAG_B.equals(node.nodeName()) || TAG_STRONG.equals(node.nodeName())) {
+                    // 处理文本 加粗
+                    Param param = new Param();
+                    param.setBold(true);
+                    processText(element.text(), param, blocks);
+                } else if (TAG_I.equals(node.nodeName()) || TAG_EM.equals(node.nodeName())) {
+                    // 处理文本 斜体
+                    Param param = new Param();
+                    param.setItalic(true);
+                    processText(element.text(), param, blocks);
+                } else if (TAG_U.equals(node.nodeName())) {
+                    // 处理文本 下划线
+                    Param param = new Param();
+                    param.setUnderline(true);
+                    processText(element.text(), param, blocks);
+                } else if (TAG_SUP.equals(node.nodeName())) {
+                    // 处理文本 上标
+                    Param param = new Param();
+                    param.setSup(true);
+                    processText(element.text(), param, blocks);
+                } else if (TAG_SUB.equals(node.nodeName())) {
+                    // 处理文本 下标
+                    Param param = new Param();
+                    param.setSub(true);
+                    processText(element.text(), param, blocks);
+                } else {
+                    splitBlocks(node.childNodes(), blocks);
+                }
+            } else {
+                if (node instanceof TextNode) {
+                    TextNode element = (TextNode) node;
+                    // 处理文本
+                    processText(element.text(), null, blocks);
+                } else {
+                    log.debug("Ignore node {}", node.nodeName());
+                }
+            }
+        }
+    }
+
+    private static void processImg(Element element, List<Block> blocks) {
+        String src = element.attr(ATTR_SRC);
+        if (StringUtils.isBlank(src)) {
+            return;
+        }
+
+        String width = element.attr(ATTR_WIDTH);
+        String height = element.attr(ATTR_HEIGHT);
+        String style = element.attr(ATTR_STYLE);
+        if (StringUtils.isBlank(width)) {
+            width = extractStyle(style, ATTR_WIDTH);
+        }
+        if (StringUtils.isBlank(height)) {
+            height = extractStyle(style, ATTR_HEIGHT);
+        }
+
+        Param param = new Param();
+        param.setWidth(extractNumber(width));
+        param.setHeight(extractNumber(height));
+
+        Block block = new Block();
+        block.setType(BlockType.image);
+        block.setValue(src);
+        block.setParam(param);
+        blocks.add(block);
+    }
+
+    private static void processAudio(Element element, List<Block> blocks) {
+        String id = element.attr(ATTR_ID);
+        String name = element.attr(ATTR_NAME);
+        if (StringUtils.isBlank(id) || StringUtils.isBlank(name) || !name.endsWith(AUDIO_SUFFIX)) {
+            // 按链接处理
+            // processLink(element, blocks);
+            return;
+        }
+
+        String url = element.attr(ATTR_URL);
+        Block block = new Block();
+        block.setType(BlockType.audio);
+        block.setValue(url);
+        block.setExt1(id);
+        block.setExt2(name);
+
+        String playTime = extractNumber(element.attr(PLAY_TIME));
+        if (StringUtils.isNotEmpty(playTime)) {
+            block.setPlayTime(Integer.parseInt(playTime));
+        }
+
+        blocks.add(block);
+    }
+
+    private static void processLink(Element element, List<Block> blocks) {
+        String href = element.attr(ATTR_HREF);
+        String text = element.text();
+        if (StringUtils.isBlank(href) || StringUtils.isBlank(text)) {
+            return;
+        }
+
+        Block block = new Block();
+        block.setType(BlockType.link);
+        block.setValue(href);
+        block.setExt1(text);
+        blocks.add(block);
+    }
+
+    private static void processText(String value, Param param, List<Block> blocks) {
+        if (StringUtils.isBlank(value)) {
+            return;
+        }
+
+        Block block = new Block();
+        block.setType(BlockType.text);
+        block.setValue(value);
+        block.setParam(param);
+        blocks.add(block);
+    }
+
+    private static String extractStyle(String style, String attributeName) {
+        if (StringUtils.isBlank(style)) {
+            return "";
+        }
+
+        style = style.toLowerCase();
+        if (!style.contains(attributeName)) {
+            return "";
+        }
+
+        // 截取样式内属性值
+        String[] attributes = style.split(";");
+        for (String attribute : attributes) {
+            if (attribute.trim().startsWith(attributeName)) {
+                return attribute.replaceFirst(attributeName, "")
+                        .replaceFirst(":", "")
+                        .trim();
+            }
+        }
+
+        return "";
+    }
+
+    private static String extractNumber(String str) {
+        if (StringUtils.isBlank(str)) {
+            return "";
+        }
+
+        // 截取数字
+        Pattern pattern = Pattern.compile("\\d+");
+        Matcher matcher = pattern.matcher(str);
+        while (matcher.find()) {
+            return matcher.group(0);
+        }
+        return "";
+    }
+
+    private static String replaceTags(String str) {
+        str = str.replace("&nbsp;", "");//消除空格
+        str = str.replace("&quot;", "\"");//将&quot;转换成"
+        str = str.replace("&lt;", "<");//将&lt;转换成<
+        str = str.replace("&gt;", ">");//将&gt;转换成>
+        str = str.replace("&amp;", "&");//将&amp;转换成&
+        return str;
+    }
+
+}

+ 45 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/Section.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 段落
+ */
+public class Section implements Serializable {
+
+    private static final long serialVersionUID = -8820818435363871114L;
+
+    private List<Block> blocks;
+
+    public void addBlock(Block block) {
+        if (block == null) {
+            return;
+        }
+        if (blocks == null) {
+            blocks = new ArrayList<>();
+        }
+        blocks.add(block);
+    }
+
+    public List<Block> getBlocks() {
+        if (blocks == null) {
+            blocks = new ArrayList<>();
+        }
+        return blocks;
+    }
+
+    public void setBlocks(List<Block> blocks) {
+        this.blocks = blocks;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+    }
+
+}

+ 50 - 0
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/handler/richtext2/TagConstant.java

@@ -0,0 +1,50 @@
+package cn.com.qmth.examcloud.support.handler.richtext2;
+
+/**
+ * HTML标签相关常量
+ */
+public interface TagConstant {
+
+    String TAG_P = "p";// p标签
+
+    String TAG_A = "a";// a标签
+
+    String TAG_STRONG = "strong";// 加粗标签
+
+    String TAG_B = "b";// 加粗标签
+
+    String TAG_EM = "em";// 斜体标签
+
+    String TAG_I = "i";// 斜体标签
+
+    String TAG_U = "u";// 下划线标签
+
+    String TAG_SUP = "sup";// 上标标签
+
+    String TAG_SUB = "sub";// 下标标签
+
+    String TAG_IMG = "img";// img标签
+
+    String ATTR_SRC = "src";
+
+    String ATTR_HREF = "href";
+
+    String ATTR_URL = "url";
+
+    String ATTR_STYLE = "style";
+
+    String ATTR_WIDTH = "width";
+
+    String ATTR_HEIGHT = "height";
+
+    String ATTR_ID = "id";
+
+    String ATTR_NAME = "name";
+
+    String AUDIO_SUFFIX = ".mp3";
+
+    String VIDEO_SUFFIX = ".mp4";
+
+    String PLAY_TIME = "playTime";
+
+}

+ 8 - 3
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/helper/FaceBiopsyHelper.java

@@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
  * @Version 1.0
  */
 public class FaceBiopsyHelper {
+
     /**
      * 获取活体检测方案
      *
@@ -28,12 +29,15 @@ public class FaceBiopsyHelper {
     public static FaceBiopsyScheme getFaceBiopsyScheme(Long rootOrgId) {
         OrgPropertyCacheBean orgProperty = CacheHelper.getOrgProperty(rootOrgId,
                 Constants.IDENTIFICATION_OF_LIVING_BODY_SCHEME_KEY);
+
         if (orgProperty.getHasValue()) {
-            if (FaceBiopsyScheme.FACE_ID.getCode().equals(orgProperty.getValue())) {
-                return FaceBiopsyScheme.FACE_ID;
+            if (FaceBiopsyScheme.FACE_CLIENT.getCode().equals(orgProperty.getValue())) {
+                return FaceBiopsyScheme.FACE_CLIENT;
+            } else if (FaceBiopsyScheme.FACE_MOTION.getCode().equals(orgProperty.getValue())) {
+                return FaceBiopsyScheme.FACE_MOTION;
             }
-            return FaceBiopsyScheme.NEW;
         }
+
         //默认使用旧活体检测方案(即faceId方案)
         return FaceBiopsyScheme.FACE_ID;
     }
@@ -157,4 +161,5 @@ public class FaceBiopsyHelper {
 
         return false;
     }
+
 }

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

@@ -28,6 +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);
 
     /**
      * 保存文件到存储服务

+ 60 - 4
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/filestorage/impl/AliyunFileStorageImpl.java

@@ -14,8 +14,6 @@ import org.apache.commons.codec.DecoderException;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.time.DateUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 import java.io.*;
@@ -26,8 +24,6 @@ import java.util.Map;
 @Service(value = "aliyunFileStorage")
 public class AliyunFileStorageImpl implements FileStorage {
 
-    private static final Logger LOG = LoggerFactory.getLogger(AliyunFileStorageImpl.class);
-
     // 文件最大大小(byte)
     private static int maxFileSize = 100 * 1024 * 1024;
 
@@ -494,4 +490,64 @@ 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);
+        } catch (Exception e) {
+            throw new StatusException("5001", "上传出错", e);
+        }
+	}
+	
+    private YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5, boolean refreshCDN,Long cacheAge) {
+        try {
+            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);
+            YunPathInfo result = new YunPathInfo(url, getTreatyPath(as.getAliyunId(), relativePath));
+
+            if (refreshCDN) {
+                AliyunRefreshCdn.refreshCDN(ac.getAccessKeyId(), ac.getAccessKeySecret(), result.getUrl());
+            }
+
+            return result;
+        } catch (Exception e) {
+            throw new StatusException("6001", "上传出错", e);
+        }
+    }
+    
+    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();
+        OSS oss = AliyunSiteManager.getAliYunClientBySiteId(siteId);
+        // 阿里云文件路径
+        String path = FreeMarkerUtil.process(as.getPath(), env);
+        path = disposePath(path);
+//        if (StringUtils.isNotBlank(md5)) {
+//            md5 = Base64.getEncoder().encodeToString(Hex.decodeHex(md5));
+//            ObjectMetadata meta = new ObjectMetadata();
+//            meta.setContentMD5(md5);
+//            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 );
+        }
+        oss.putObject(bucket, path, in, meta );
+
+        return path;
+    }
+
 }

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

@@ -113,4 +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);) {
+            return saveFile(siteId, env, in, md5, refreshCDN);
+        } catch (Exception e) {
+            throw new StatusException("1001", "上传出错", e);
+        }
+	}
+
 }

+ 4 - 2
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/support/CustomExceptionHandler.java

@@ -10,6 +10,7 @@ import cn.com.qmth.examcloud.web.jpa.DataIntegrityViolationTransverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -245,8 +246,9 @@ public class CustomExceptionHandler {
                 LOG.debug(err.getMessage(), err);
             }
         }
-
-        return new ResponseEntity<>(body, httpStatus);
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("Content-Type","application/json;charset=utf-8");
+        return new ResponseEntity<>(body, headers,httpStatus);
     }
 
     /**