keys, boolean fragment) {
+ UriComponentsBuilder template = UriComponentsBuilder.newInstance();
+ UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
+ URI redirectUri;
+ try {
+ // assume it's encoded to start with (if it came in over the wire)
+ redirectUri = builder.build(true).toUri();
+ } catch (Exception e) {
+ // ... but allow client registrations to contain hard-coded non-encoded values
+ redirectUri = builder.build().toUri();
+ builder = UriComponentsBuilder.fromUri(redirectUri);
+ }
+ template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
+ .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
+
+ if (fragment) {
+ StringBuilder values = new StringBuilder();
+ if (redirectUri.getFragment() != null) {
+ String append = redirectUri.getFragment();
+ values.append(append);
+ }
+ for (String key : query.keySet()) {
+ if (values.length() > 0) {
+ values.append("&");
+ }
+ String name = key;
+ if (keys != null && keys.containsKey(key)) {
+ name = keys.get(key);
+ }
+ values.append(name).append("={").append(key).append("}");
+ }
+ if (values.length() > 0) {
+ template.fragment(values.toString());
+ }
+ UriComponents encoded = template.build().expand(query).encode();
+ builder.fragment(encoded.getFragment());
+ } else {
+ for (String key : query.keySet()) {
+ String name = key;
+ if (keys != null && keys.containsKey(key)) {
+ name = keys.get(key);
+ }
+ template.queryParam(name, "{" + key + "}");
+ }
+ template.fragment(redirectUri.getFragment());
+ UriComponents encoded = template.build().expand(query).encode();
+ builder.query(encoded.getQuery());
+ }
+ return builder.build().toUriString();
+ }
+
+ public static String[] obtainBasicAuthorization(HttpServletRequest request) {
+ String clientId;
+ String clientSecret;
+ // 先从 Header 中获取
+ String authorization = request.getHeader("Authorization");
+ authorization = StrUtil.subAfter(authorization, "Basic ", true);
+ if (StringUtils.hasText(authorization)) {
+ authorization = Base64.decodeStr(authorization);
+ clientId = StrUtil.subBefore(authorization, ":", false);
+ clientSecret = StrUtil.subAfter(authorization, ":", false);
+ // 再从 Param 中获取
+ } else {
+ clientId = request.getParameter("client_id");
+ clientSecret = request.getParameter("client_secret");
+ }
+
+ // 如果两者非空,则返回
+ if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
+ return new String[]{clientId, clientSecret};
+ }
+ return null;
+ }
+
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java
new file mode 100644
index 00000000..ec3a885c
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/FileUtils.java
@@ -0,0 +1,84 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.io;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import lombok.SneakyThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(String data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeUtf8String(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(byte[] data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeBytes(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件,无内容
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile() {
+ // 创建文件,通过 UUID 保证唯一
+ File file = File.createTempFile(IdUtil.simpleUUID(), null);
+ // 标记 JVM 退出时,自动删除
+ file.deleteOnExit();
+ return file;
+ }
+
+ /**
+ * 生成文件路径
+ *
+ * @param content 文件内容
+ * @param originalName 原始文件名
+ * @return path,唯一不可重复
+ */
+ public static String generatePath(byte[] content, String originalName) {
+ String sha256Hex = DigestUtil.sha256Hex(content);
+ // 情况一:如果存在 name,则优先使用 name 的后缀
+ if (StrUtil.isNotBlank(originalName)) {
+ String extName = FileNameUtil.extName(originalName);
+ return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
+ }
+ // 情况二:基于 content 计算
+ return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java
new file mode 100644
index 00000000..30d64dfc
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/io/IoUtils.java
@@ -0,0 +1,28 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
+ *
+ * @author 芋道源码
+ */
+public class IoUtils {
+
+ /**
+ * 从流中读取 UTF8 编码的内容
+ *
+ * @param in 输入流
+ * @param isClose 是否关闭
+ * @return 内容
+ * @throws IORuntimeException IO 异常
+ */
+ public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+ return StrUtil.utf8Str(IoUtil.read(in, isClose));
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java
new file mode 100644
index 00000000..7a2a0b44
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/json/JsonUtils.java
@@ -0,0 +1,187 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class JsonUtils {
+
+ private static ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
+ objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
+ }
+
+ /**
+ * 初始化 objectMapper 属性
+ *
+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+ *
+ * @param objectMapper ObjectMapper 对象
+ */
+ public static void init(ObjectMapper objectMapper) {
+ JsonUtils.objectMapper = objectMapper;
+ }
+
+ @SneakyThrows
+ public static String toJsonString(Object object) {
+ return objectMapper.writeValueAsString(object);
+ }
+
+ @SneakyThrows
+ public static byte[] toJsonByte(Object object) {
+ return objectMapper.writeValueAsBytes(object);
+ }
+
+ @SneakyThrows
+ public static String toJsonPrettyString(Object object) {
+ return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+ }
+
+ public static T parseObject(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(text, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, String path, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ JsonNode treeNode = objectMapper.readTree(text);
+ JsonNode pathNode = treeNode.path(path);
+ return objectMapper.readValue(pathNode.toString(), clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, Type type) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将字符串解析成指定类型的对象
+ * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+ * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+ *
+ * @param text 字符串
+ * @param clazz 类型
+ * @return 对象
+ */
+ public static T parseObject2(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ return JSONUtil.toBean(text, clazz);
+ }
+
+ public static T parseObject(byte[] bytes, Class clazz) {
+ if (ArrayUtil.isEmpty(bytes)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(bytes, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", bytes, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, TypeReference typeReference) {
+ try {
+ return objectMapper.readValue(text, typeReference);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List parseArray(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return new ArrayList<>();
+ }
+ try {
+ return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List parseArray(String text, String path, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ JsonNode treeNode = objectMapper.readTree(text);
+ JsonNode pathNode = treeNode.path(path);
+ return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(String text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(byte[] text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static boolean isJson(String text) {
+ return JSONUtil.isTypeJSON(text);
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java
new file mode 100644
index 00000000..abbfc082
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/monitor/TracerUtils.java
@@ -0,0 +1,30 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author 芋道源码
+ */
+public class TracerUtils {
+
+ /**
+ * 私有化构造方法
+ */
+ private TracerUtils() {
+ }
+
+ /**
+ * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+ * 如果不存在的话为空字符串!!!
+ *
+ * @return 链路追踪编号
+ */
+ public static String getTraceId() {
+ return TraceContext.traceId();
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java
new file mode 100644
index 00000000..748ef823
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/MoneyUtils.java
@@ -0,0 +1,73 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.number;
+
+import cn.hutool.core.math.Money;
+import cn.hutool.core.util.NumberUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * 金额工具类
+ *
+ * @author 芋道源码
+ */
+public class MoneyUtils {
+
+ /**
+ * 计算百分比金额,四舍五入
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @return 百分比金额
+ */
+ public static Integer calculateRatePrice(Integer price, Double rate) {
+ return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue();
+ }
+
+ /**
+ * 计算百分比金额,向下传入
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @return 百分比金额
+ */
+ public static Integer calculateRatePriceFloor(Integer price, Double rate) {
+ return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue();
+ }
+
+ /**
+ * 计算百分比金额
+ *
+ * @param price 金额
+ * @param rate 百分比,例如说 56.77% 则传入 56.77
+ * @param scale 保留小数位数
+ * @param roundingMode 舍入模式
+ */
+ public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) {
+ return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
+ .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
+ }
+
+ /**
+ * 分转元
+ *
+ * @param fen 分
+ * @return 元
+ */
+ public static BigDecimal fenToYuan(int fen) {
+ return new Money(0, fen).getAmount();
+ }
+
+ /**
+ * 分转元(字符串)
+ *
+ * 例如说 fen 为 1 时,则结果为 0.01
+ *
+ * @param fen 分
+ * @return 元
+ */
+ public static String fenToYuanStr(int fen) {
+ return new Money(0, fen).toString();
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java
new file mode 100644
index 00000000..23ba3739
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/number/NumberUtils.java
@@ -0,0 +1,40 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.number;
+
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author 芋道源码
+ */
+public class NumberUtils {
+
+ public static Long parseLong(String str) {
+ return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+ }
+
+ /**
+ * 通过经纬度获取地球上两点之间的距离
+ *
+ * 参考 <DistanceUtil> 实现,目前它已经被 hutool 删除
+ *
+ * @param lat1 经度1
+ * @param lng1 纬度1
+ * @param lat2 经度2
+ * @param lng2 纬度2
+ * @return 距离,单位:千米
+ */
+ public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
+ double radLat1 = lat1 * Math.PI / 180.0;
+ double radLat2 = lat2 * Math.PI / 180.0;
+ double a = radLat1 - radLat2;
+ double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
+ double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+ + Math.cos(radLat1) * Math.cos(radLat2)
+ * Math.pow(Math.sin(b / 2), 2)));
+ distance = distance * 6378.137;
+ distance = Math.round(distance * 10000d) / 10000d;
+ return distance;
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java
new file mode 100644
index 00000000..cfe6f00b
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/BeanUtils.java
@@ -0,0 +1,37 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.object;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.chanko.yunxi.mes.heli.framework.common.pojo.PageResult;
+import com.chanko.yunxi.mes.heli.framework.common.util.collection.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * Bean 工具类
+ *
+ * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
+ * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
+ *
+ * @author 芋道源码
+ */
+public class BeanUtils {
+
+ public static T toBean(Object source, Class targetClass) {
+ return BeanUtil.toBean(source, targetClass);
+ }
+
+ public static List toBean(List source, Class targetType) {
+ if (source == null) {
+ return null;
+ }
+ return CollectionUtils.convertList(source, s -> toBean(s, targetType));
+ }
+
+ public static PageResult toBean(PageResult source, Class targetType) {
+ if (source == null) {
+ return null;
+ }
+ return new PageResult<>(toBean(source.getList(), targetType), source.getTotal());
+ }
+
+}
\ No newline at end of file
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java
new file mode 100644
index 00000000..52b1d6b2
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/ObjectUtils.java
@@ -0,0 +1,63 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.object;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * Object 工具类
+ *
+ * @author 芋道源码
+ */
+public class ObjectUtils {
+
+ /**
+ * 复制对象,并忽略 Id 编号
+ *
+ * @param object 被复制对象
+ * @param consumer 消费者,可以二次编辑被复制对象
+ * @return 复制后的对象
+ */
+ public static T cloneIgnoreId(T object, Consumer consumer) {
+ T result = ObjectUtil.clone(object);
+ // 忽略 id 编号
+ Field field = ReflectUtil.getField(object.getClass(), "id");
+ if (field != null) {
+ ReflectUtil.setFieldValue(result, field, null);
+ }
+ // 二次编辑
+ if (result != null) {
+ consumer.accept(result);
+ }
+ return result;
+ }
+
+ public static > T max(T obj1, T obj2) {
+ if (obj1 == null) {
+ return obj2;
+ }
+ if (obj2 == null) {
+ return obj1;
+ }
+ return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+ }
+
+ @SafeVarargs
+ public static T defaultIfNull(T... array) {
+ for (T item : array) {
+ if (item != null) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ @SafeVarargs
+ public static boolean equalsAny(T obj, T... array) {
+ return Arrays.asList(array).contains(obj);
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java
new file mode 100644
index 00000000..053316e3
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/object/PageUtils.java
@@ -0,0 +1,16 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.object;
+
+import com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam;
+
+/**
+ * {@link com.chanko.yunxi.mes.heli.framework.common.pojo.PageParam} 工具类
+ *
+ * @author 芋道源码
+ */
+public class PageUtils {
+
+ public static int getStart(PageParam pageParam) {
+ return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java
new file mode 100644
index 00000000..27614eaf
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package com.chanko.yunxi.mes.heli.framework.common.util;
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java
new file mode 100644
index 00000000..c18c140a
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/servlet/ServletUtils.java
@@ -0,0 +1,113 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.servlet;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import com.chanko.yunxi.mes.heli.framework.common.util.json.JsonUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Map;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+ /**
+ * 返回 JSON 字符串
+ *
+ * @param response 响应
+ * @param object 对象,会序列化成 JSON 字符串
+ */
+ @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+ public static void writeJSON(HttpServletResponse response, Object object) {
+ String content = JsonUtils.toJsonString(object);
+ ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+ }
+
+ /**
+ * 返回附件
+ *
+ * @param response 响应
+ * @param filename 文件名
+ * @param content 附件内容
+ */
+ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+ // 设置 header 和 contentType
+ response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ // 输出附件
+ IoUtil.write(response.getOutputStream(), false, content);
+ }
+
+ /**
+ * @param request 请求
+ * @return ua
+ */
+ public static String getUserAgent(HttpServletRequest request) {
+ String ua = request.getHeader("User-Agent");
+ return ua != null ? ua : "";
+ }
+
+ /**
+ * 获得请求
+ *
+ * @return HttpServletRequest
+ */
+ public static HttpServletRequest getRequest() {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ if (!(requestAttributes instanceof ServletRequestAttributes)) {
+ return null;
+ }
+ return ((ServletRequestAttributes) requestAttributes).getRequest();
+ }
+
+ public static String getUserAgent() {
+ HttpServletRequest request = getRequest();
+ if (request == null) {
+ return null;
+ }
+ return getUserAgent(request);
+ }
+
+ public static String getClientIP() {
+ HttpServletRequest request = getRequest();
+ if (request == null) {
+ return null;
+ }
+ return ServletUtil.getClientIP(request);
+ }
+
+ // TODO @疯狂:terminal 还是从 ServletUtils 里拿,更容易全局治理;
+
+ public static boolean isJsonRequest(ServletRequest request) {
+ return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+ }
+
+ public static String getBody(HttpServletRequest request) {
+ return ServletUtil.getBody(request);
+ }
+
+ public static byte[] getBodyBytes(HttpServletRequest request) {
+ return ServletUtil.getBodyBytes(request);
+ }
+
+ public static String getClientIP(HttpServletRequest request) {
+ return ServletUtil.getClientIP(request);
+ }
+
+ public static Map getParamMap(HttpServletRequest request) {
+ return ServletUtil.getParamMap(request);
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java
new file mode 100644
index 00000000..d384c01f
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringAopUtils.java
@@ -0,0 +1,46 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.spring;
+
+import cn.hutool.core.bean.BeanUtil;
+import org.springframework.aop.framework.AdvisedSupport;
+import org.springframework.aop.framework.AopProxy;
+import org.springframework.aop.support.AopUtils;
+
+/**
+ * Spring AOP 工具类
+ *
+ * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
+ */
+public class SpringAopUtils {
+
+ /**
+ * 获取代理的目标对象
+ *
+ * @param proxy 代理对象
+ * @return 目标对象
+ */
+ public static Object getTarget(Object proxy) throws Exception {
+ // 不是代理对象
+ if (!AopUtils.isAopProxy(proxy)) {
+ return proxy;
+ }
+ // Jdk 代理
+ if (AopUtils.isJdkDynamicProxy(proxy)) {
+ return getJdkDynamicProxyTargetObject(proxy);
+ }
+ // Cglib 代理
+ return getCglibProxyTargetObject(proxy);
+ }
+
+ private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
+ Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
+ AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
+ return advisedSupport.getTargetSource().getTarget();
+ }
+
+ private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
+ AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
+ AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
+ return advisedSupport.getTargetSource().getTarget();
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java
new file mode 100644
index 00000000..aae79b3d
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/spring/SpringExpressionUtils.java
@@ -0,0 +1,89 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Spring EL 表达式的工具类
+ *
+ * @author mashu
+ */
+public class SpringExpressionUtils {
+
+ /**
+ * Spring EL 表达式解析器
+ */
+ private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
+ /**
+ * 参数名发现器
+ */
+ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+ private SpringExpressionUtils() {
+ }
+
+ /**
+ * 从切面中,单个解析 EL 表达式的结果
+ *
+ * @param joinPoint 切面点
+ * @param expressionString EL 表达式数组
+ * @return 执行界面
+ */
+ public static Object parseExpression(JoinPoint joinPoint, String expressionString) {
+ Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
+ return result.get(expressionString);
+ }
+
+ /**
+ * 从切面中,批量解析 EL 表达式的结果
+ *
+ * @param joinPoint 切面点
+ * @param expressionStrings EL 表达式数组
+ * @return 结果,key 为表达式,value 为对应值
+ */
+ public static Map parseExpressions(JoinPoint joinPoint, List expressionStrings) {
+ // 如果为空,则不进行解析
+ if (CollUtil.isEmpty(expressionStrings)) {
+ return MapUtil.newHashMap();
+ }
+
+ // 第一步,构建解析的上下文 EvaluationContext
+ // 通过 joinPoint 获取被注解方法
+ MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+ Method method = methodSignature.getMethod();
+ // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
+ String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
+ // Spring 的表达式上下文对象
+ EvaluationContext context = new StandardEvaluationContext();
+ // 给上下文赋值
+ if (ArrayUtil.isNotEmpty(paramNames)) {
+ Object[] args = joinPoint.getArgs();
+ for (int i = 0; i < paramNames.length; i++) {
+ context.setVariable(paramNames[i], args[i]);
+ }
+ }
+
+ // 第二步,逐个参数解析
+ Map result = MapUtil.newHashMap(expressionStrings.size(), true);
+ expressionStrings.forEach(key -> {
+ Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
+ result.put(key, value);
+ });
+ return result;
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java
new file mode 100644
index 00000000..47b895ed
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/string/StrUtils.java
@@ -0,0 +1,69 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.string;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author 芋道源码
+ */
+public class StrUtils {
+
+ public static String maxLength(CharSequence str, int maxLength) {
+ return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
+ }
+
+ /**
+ * 给定字符串是否以任何一个字符串开始
+ * 给定字符串和数组为空都返回 false
+ *
+ * @param str 给定字符串
+ * @param prefixes 需要检测的开始字符串
+ * @since 3.0.6
+ */
+ public static boolean startWithAny(String str, Collection prefixes) {
+ if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
+ return false;
+ }
+
+ for (CharSequence suffix : prefixes) {
+ if (StrUtil.startWith(str, suffix, false)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static List splitToLong(String value, CharSequence separator) {
+ long[] longs = StrUtil.splitToLong(value, separator);
+ return Arrays.stream(longs).boxed().collect(Collectors.toList());
+ }
+
+ public static List splitToInteger(String value, CharSequence separator) {
+ int[] integers = StrUtil.splitToInt(value, separator);
+ return Arrays.stream(integers).boxed().collect(Collectors.toList());
+ }
+
+ /**
+ * 移除字符串中,包含指定字符串的行
+ *
+ * @param content 字符串
+ * @param sequence 包含的字符串
+ * @return 移除后的字符串
+ */
+ public static String removeLineContains(String content, String sequence) {
+ if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
+ return content;
+ }
+ return Arrays.stream(content.split("\n"))
+ .filter(line -> !line.contains(sequence))
+ .collect(Collectors.joining("\n"));
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java
new file mode 100644
index 00000000..08e200cc
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/util/validation/ValidationUtils.java
@@ -0,0 +1,55 @@
+package com.chanko.yunxi.mes.heli.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import org.springframework.util.StringUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author 芋道源码
+ */
+public class ValidationUtils {
+
+ private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$");
+
+ private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+ private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+ public static boolean isMobile(String mobile) {
+ return StringUtils.hasText(mobile)
+ && PATTERN_MOBILE.matcher(mobile).matches();
+ }
+
+ public static boolean isURL(String url) {
+ return StringUtils.hasText(url)
+ && PATTERN_URL.matcher(url).matches();
+ }
+
+ public static boolean isXmlNCName(String str) {
+ return StringUtils.hasText(str)
+ && PATTERN_XML_NCNAME.matcher(str).matches();
+ }
+
+ public static void validate(Object object, Class>... groups) {
+ Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ Assert.notNull(validator);
+ validate(validator, object, groups);
+ }
+
+ public static void validate(Validator validator, Object object, Class>... groups) {
+ Set> constraintViolations = validator.validate(object, groups);
+ if (CollUtil.isNotEmpty(constraintViolations)) {
+ throw new ConstraintViolationException(constraintViolations);
+ }
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java
new file mode 100644
index 00000000..f35da7df
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnum.java
@@ -0,0 +1,35 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+ ElementType.METHOD,
+ ElementType.FIELD,
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {InEnumValidator.class, InEnumCollectionValidator.class}
+)
+public @interface InEnum {
+
+ /**
+ * @return 实现 EnumValuable 接口的
+ */
+ Class extends IntArrayValuable> value();
+
+ String message() default "必须在指定范围 {value}";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java
new file mode 100644
index 00000000..05f06ea3
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumCollectionValidator.java
@@ -0,0 +1,42 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumCollectionValidator implements ConstraintValidator> {
+
+ private List values;
+
+ @Override
+ public void initialize(InEnum annotation) {
+ IntArrayValuable[] values = annotation.value().getEnumConstants();
+ if (values.length == 0) {
+ this.values = Collections.emptyList();
+ } else {
+ this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+ }
+ }
+
+ @Override
+ public boolean isValid(Collection list, ConstraintValidatorContext context) {
+ // 校验通过
+ if (CollUtil.containsAll(values, list)) {
+ return true;
+ }
+ // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+ context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+ context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+ .replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
+ return false;
+ }
+
+}
+
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java
new file mode 100644
index 00000000..804b3f02
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/InEnumValidator.java
@@ -0,0 +1,44 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import com.chanko.yunxi.mes.heli.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumValidator implements ConstraintValidator {
+
+ private List values;
+
+ @Override
+ public void initialize(InEnum annotation) {
+ IntArrayValuable[] values = annotation.value().getEnumConstants();
+ if (values.length == 0) {
+ this.values = Collections.emptyList();
+ } else {
+ this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+ }
+ }
+
+ @Override
+ public boolean isValid(Integer value, ConstraintValidatorContext context) {
+ // 为空时,默认不校验,即认为通过
+ if (value == null) {
+ return true;
+ }
+ // 校验通过
+ if (values.contains(value)) {
+ return true;
+ }
+ // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+ context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+ context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+ .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
+ return false;
+ }
+
+}
+
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java
new file mode 100644
index 00000000..ee864232
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Mobile.java
@@ -0,0 +1,28 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+ ElementType.METHOD,
+ ElementType.FIELD,
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = MobileValidator.class
+)
+public @interface Mobile {
+
+ String message() default "手机号格式不正确";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java
new file mode 100644
index 00000000..de65987d
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/MobileValidator.java
@@ -0,0 +1,25 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import com.chanko.yunxi.mes.heli.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator {
+
+ @Override
+ public void initialize(Mobile annotation) {
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ // 如果手机号为空,默认不校验,即校验通过
+ if (StrUtil.isEmpty(value)) {
+ return true;
+ }
+ // 校验手机
+ return ValidationUtils.isMobile(value);
+ }
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java
new file mode 100644
index 00000000..ba0b9969
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/Telephone.java
@@ -0,0 +1,28 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+ ElementType.METHOD,
+ ElementType.FIELD,
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = TelephoneValidator.class
+)
+public @interface Telephone {
+
+ String message() default "电话格式不正确";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java
new file mode 100644
index 00000000..5e458c2d
--- /dev/null
+++ b/mes-framework/mes-common/src/main/java/com/chanko/yunxi/mes/heli/framework/common/validation/TelephoneValidator.java
@@ -0,0 +1,25 @@
+package com.chanko.yunxi.mes.heli.framework.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.PhoneUtil;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class TelephoneValidator implements ConstraintValidator